phantom type
Pretty much taken entirely from this wikibooks page.
In a phantom type declaration, the type parameters on the LHS are not used on the RHS. So for example:
data FormData a = FormData String
What's the point of this? For example, we will show how the type parameter a
can be used to enforce, via the type system, certain properties of the FormData
value. To do that, let's define two other types:
data unvalidated data validated
Then, unvalidated
and validated
will be used as type parameters to earmark whether a FormData String
has been validated or not. We can kind of think of the phantom type as a bookkeeping tool. There's just one problem: what's to stop the user from doing something like:
FormData "Hello" :: FormData Validated
Our solution is to create a library with all the constructors as well as some other functions. Then, this library will only expose a few functions to the user. Importantly, the only methods we will expose for creating FormData
values will produce FormData Unvalidated
values:
-- our library will expose the following functions: -- this function will be the only way to create a FormData value from a String formData :: String -> FormData Unvalidated formData str = FormData str -- returns Nothing if the data can't be validated validate :: FormData Unvalidated -> Maybe (FormData Validated) validate (FormData str) = ... -- can only be called on valiated data useData :: FormData Validated -> IO () useData (FormData str) = ... -- this function allows users to define their own functions that operate on the form data, but can't turn unvalidated data into validated data and vice versa -- NOTE: don't get confused between (FormData a) in the function type and (FormData str) in the function definition. (FormData a) represents a type: either (FormData Validated) or (FormData Unvalidated). And (FormData str) is a data constructor. liftStringFn :: (String -> String) -> FormData a -> FormData a liftStringFn f (FormData str) = (FormData (f s))
According to the wikibooks page, we can use typeclasses to specify behavior based on "information that is non-existent at runtime":
class Sanitise a where sanitise :: FormData a -> FormData Validated -- nothing happens to already validated data instance Sanitise Validated where sanitise = id instance Sanitise Unvalidated where sanitise (FormData str) = FormData (filter isAlpha str)
I think what the wikibooks article meant is that in any context where sanitise
might be called, the specific version of the sanitise
method was already chosen at compile time based on whether the input is Validated
or Unvalidated
. And at runtime, the only information available is the data constructor. But I'm not sure why that's noteworthy.