As far as usage and syntax go, newtype
is basically the same as data
except that a newtype
can have only one field and one constructor. Easy
enough.
For me, the part that took some time to sink in is that it’s possible for the field to be a function.
λ> newtype Foo s a = Foo { runFoo :: s -> (s, a) }
It’s important to note that the right hand side defines the constructor of the
type. In this case the constructor takes a function that takes a single arg
s
and returns a tuple (s, a)
.
λ> :t Foo
Foo :: (s -> (s, a)) -> Foo s a
This matches the way that data constructors work - the right hand side defines
the constructor function. This is the part that caused me some confusion. I
knew how to define and use data
but the penny hadn’t fully dropped that the
parameters on the left hand side were not used to define the constructor. I
thought of the right hand side as purely for defining the accessor functions.
λ> data A a b = A {aGetter :: String, bGetter :: String}
λ> :t A
A :: String -> String -> A a b
In hindsight this is obvious but isn’t that always the case?
As a dumb example we can create a silly function that makes a tuple and use that in the constructor
λ> let makeTuple mul a = (a * mul, a * mul +2)
λ> let s = Foo (makeTuple 34)
λ> :t s
s :: Num a => Foo a a
We now have an instance of the Foo
type wrapped around our (partially
applied) makeTuple
function. If we compare Foo
and runFoo
we can see
that that mirror each other and can be used to wrap and unwrap the function s -> (s, a)
λ> :t Foo
Foo :: (s -> (s, a)) -> Foo s a
λ> :t runFoo
runFoo :: Foo s a -> s -> (s, a)
Note that when applying runFoo
to s
(our Foo
instance) we can see
that we need one more argument to be passed in order to get back the tuple.
λ> :t runFoo s
runFoo s :: Num a => a -> (a, a)
λ> print $ runFoo s 3
(102,104)
and the one line version
λ> print $ runFoo (Foo $ makeTuple 10) 4
(40,42)