Newtype coercion

Contents
  • 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:

type Name = String
type Color = String

myName :: Name
myName = "Alonzo"

gray :: Color
gray = "Gray"

data Person = Person
  { personName :: Name
  , favoriteColor :: Color
  } deriving Show
λ> 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:

newtype Name = Name String
  deriving Show

newtype Color = Color String
  deriving Show

myName :: Name
myName = Name "Alonzo"

gray :: Color
gray = Color "Gray"

data Person = Person
  { personName :: Name
  , favoriteColor :: Color
  } deriving Show
λ> 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 :: Name -> Name
abbreviateName x = take 3 x ++ "."
λ> 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:

abbreviateName :: Name -> Name
abbreviateName (Name x) =       -- 1
    Name (take 3 x ++ ".")      -- 2
  1. Use the Name constructor as a pattern in the function’s argument to coerce the argument from Name to String.
  2. Use the Name constructor as an expression in the function body to coerce the abbreviated String to a Name.

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
    coerce a = N a

instance Coercible B A
  where
    coerce (N a) = a

Sign up for access to the full page, plus the complete archive and all the latest content.