UP | HOME

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.

1 useful links

Created: 2021-09-14 Tue 21:44