-- This file demonstrates how the second form of phantom types
-- can restrict us to writing better and safer code
module KeyValueProblem
(
Attribute
, attribute
) where
newtype Attribute = Attribute { key :: String, value :: String }
-- convenience constructor
attribute :: String -> String -> Attribute
attribute key value = Attribute { key, value }
infix 4 attribute as :=
-- This will succesfully compile when it shouldn't, leading to problems
-- later on during runtime.
example :: Array Attribute
example =
[ "width" := "not a valid number"
, "height" := "not a valid number"
, "style" := "not a valid style"
]
-- The problem is that the `width` function should specify
-- a valid type for its second parameter, one that can also be turned
-- into a String.
-- Let's show an example that doesn't use Phantom Types first
-- before showing why they are a better solution
module FirstPossibleSolution
(
Attribute
, width
, height
, style
, Style(..)
) where
newtype Attribute = Attribute { key :: String, value :: String }
-- This time, we're not going to export the smart constructor 'attribute'
-- Rather, we'll change its type signture and export wrappers around it
-- assume there are instances of Show for Int and Style
class Show a where
show :: a -> String
-- Read "given a string and any value that can be turned into a string"
attribute :: forall a. Show a => String -> a -> Attribute
attribute key value = Attribute { key: key, value: show value }
-- new smart constructor
width :: Int -> Attribute
width = attribute "width"
-- new smart constructor
height :: Int -> Attribute
height = attribute "height"
data Style = Normal | Bold | Italic
-- new smart constructor
style :: Style -> Attribute
style = attribute "style"
-- This gets rid of our runtime issues, but it makes things less readable
-- as we have now lost the "key := value" syntax.
-- Now, we have to use a less-readible "key value" syntax
example :: Array Attribute
example =
[ width 40
, height 200
, style Bold
]
-- How can we get the "key := value" syntax back? We want a syntax like
-- `width := 400`. To get that, attribute needs to somehow force the second
-- parameter to be a specific type based on the first parameter it receives.
--
-- This is where Phantom Types come to the rescue
module PhantomTypes
(
Attribute
, width
, height
, style
, Style(..)
) where
newtype Attribute = Attribute { key :: String, value :: String }
-- assume there are instances of Show for Int and Style
class Show a where
show :: a -> String
-- A phantom type is a type defined in the type
-- but not used in its instances / constructors
data TheType phantomType = Constructor String
-- In our case, we can write:
newtype AttributeKey desiredValueType = AttributeKey String
-- Let's update attribute to use the phantom type to restrict
-- what the second parameter can be in our function. Recall that
-- "a" in this situation means "desired value type":
attribute :: forall a. Show a => AttributeKey a -> a -> Attribute
attribute (AttributeKey key) value =
Attribute { key: key, value: show value }
infix 4 attribute as :=
-- Now, we can update width's type signature to force its
-- expected argument to be an int
width :: AttributeKey Int
width = AttributeKey "width"
height :: AttributeKey Int
height = AttributeKey "height"
data Style = Normal | Bold | Italic
-- and the same for style
style :: AttributeKey Style
style = AttributeKey "style"
-- Alright!
example :: Array Attribute
example =
[ width := 40
, height := 200
, style := Bold
]