Newtype coercion
- The
Coercible
class - 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 Name
s like String
s 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
Name
constructor as a pattern in the function’s argument to coerce the argument fromName
toString
. - Use the
Name
constructor as an expression in the function body to coerce the abbreviatedString
to 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 Coercible
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.
class Coercible a b
where
coerce :: a -> b
You 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 A
Then we automatically receive the following two Coercible
instances:
instance Coercible A B
where
= N a
coerce a
instance Coercible B A
where
N a) = a coerce (