02-Restricting-Argument-Types.purs

-- 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
  ]