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 aThis 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 bIn 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 aWe 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)