Newtype coercion
- The
Coercibleclass - Newtype wrapping/unwrapping
- Newtypes for anything
- Some types seem strange
- Coercibility is transitive
- Coercion via type parameters
- Coercions of functions
- Deriving via
- Monad transformers
- Constructor visibility
- Type roles
- Phantom type parameters
- Nominal type parameters
- History
The newtype keyword allows us to define a new type that has the same runtime representation as another type.
Consider this in contrast with a type alias, which merely creates a new name to refer to the same type. This doesn’t provide any additional type safety, as illustrated by the mistake in the following example:
λ> Person gray myName
Person {personName = "Gray", favoriteColor = "Alonzo"}
A newtype definition actually creates a different type, so when we make the same mistake again in the follow example, the typechecker refuses to allow the erroneous code to compile:
λ> Person gray myName
error:
• Couldn't match expected type ‘Name’ with actual type ‘Color’
• In the first argument of ‘Person’, namely ‘gray’
In the expression: Person gray myName
In an equation for ‘it’: it = Person gray myName
error:
• Couldn't match expected type ‘Color’ with actual type ‘Name’
• In the second argument of ‘Person’, namely ‘myName’
In the expression: Person gray myName
In an equation for ‘it’: it = Person gray myName
This is good – The compiler caught our mistake. But there is a downside! Sometimes we may want to be able to treat Names like Strings and vice versa. In the type alias version, we could write this:
λ> abbreviateName myName
"Alo."
But in the newtyped version, abbreviateName won’t typecheck, because Name and String are not the same type.
error:
• Couldn't match expected type ‘Name’ with actual type ‘[Char]’
• In the expression: take 3 x ++ "."
In an equation for ‘abbreviateName’:
abbreviateName x = take 3 x ++ "."
error:
• Couldn't match expected type ‘[Char]’ with actual type ‘Name’
• In the second argument of ‘take’, namely ‘x’
In the first argument of ‘(++)’, namely ‘take 3 x’
In the expression: take 3 x ++ "."To make the abbreviateName code above compile, we have to introduce coercions in two places:
- Use the
Nameconstructor as a pattern in the function’s argument to coerce the argument fromNametoString. - Use the
Nameconstructor as an expression in the function body to coerce the abbreviatedStringto aName.
This can become more cumbersome as the code grows more complicated or as we add more layers of newtypes. Fortunately, GHC gives us some tools that help us express these trivial type conversions more easily.
The Coercible class
GHC provides a function called coerce that helps us perform these trivial conversions between types. It can convert between two types A and B as long as the constraint Coercible A B is satisfied.
Although CoercibleData.Coerce in the base package is a magic feature of GHC, you can safely understand it as a typeclass defined as follows:You do not need to enable any language extensions to use the coerce function or to use the Coercible class in constraints. Coercible is a multi-parameter typeclass (it has two type parameters), but the MultiParamTypeClasses extension is only required to define multi-parameter typeclasses, not to use them.
class Coercible a b
where
coerce :: a -> bYou cannot define your own instances of Coercible, but you get a lot of instances for free.
Newtype wrapping/unwrapping
The most basic circumstance in which Coercible instances arise is whenever a newtype is defined.
If we have a definition that looks like this:
newtype B = N AThen we automatically receive the following two Coercible instances:
instance Coercible A B
where
coerce a = N a
instance Coercible B A
where
coerce (N a) = a
