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:
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
Strings and vice versa. In the type alias version, we could write this:
But in the newtyped version,
abbreviateName won’t typecheck, because
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 from
- Use the
Nameconstructor as an expression in the function body to coerce the abbreviated
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.
GHC provides a function called
coerce that helps us perform these trivial conversions between types. It can convert between two types
B as long as the constraint
Coercible A B is satisfied.
Data.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.
You cannot define your own instances of
Coercible, but you get a lot of instances for free.
The most basic circumstance in which
Coercible instances arise is whenever a
newtype is defined.
If we have a definition that looks like this:
Then we automatically receive the following two