My understanding of newtypes is that they are compiled out by GHC. However, this can't be the whole story because phantom types can hold information.
From here:
you can wrap [a type] in a newtype and it'll be considered distinct to the type-checker, but identical at runtime. You can then use all sorts of deep trickery like phantom or recursive types without worrying about GHC shuffling buckets of bytes for no reason.
For example, imagine a newtype representing arithmetic modulo q:
newtype Zq q = Zq Int
class Modulus q where
getModulus :: q -> Int
addZq :: (Modulus q) => Zq q -> Zq q -> Zq q
addZq (Zq a) (Zq b) = Zq $ (a+b) `mod` (getModulus (undefined :: q))
addZq
can't be compiled down to
addZq :: Int -> Int -> Int
so in what sense is the newtype compiled out, and where does the phantom type information get stored?
The thing to keep in mind is that you don't "compile down" to Haskell; you compile down to some other, more explicit language -- in GHC's case, the next well-known step down is core. And although you can't compile addZq
down to something of type Int -> Int -> Int
in Core, you can compile it down to something whose type you might write as Modulus q => Int -> Int -> Int
. In this more explicit language, =>
has a different meaning than in Haskell; in this language, c => t
is the type of a function which takes evidence (in this case, a class dictionary) for the claim c
and produces something of type t
. So Modulus q => Int -> Int -> Int
is roughly the same as (q -> Int) -> Int -> Int -> Int
, and addZq
certainly can be given that type, even in Haskell.