Learn You a Haskell for Great Good! - Miran Lipovaca [112]
getCharList :: CharList -> [Char]
It takes a CharList value and converts it to a [Char] value. You can think of this as wrapping and unwrapping, but you can also think of it as converting values from one type to the other.
Using newtype to Make Type Class Instances
Many times, we want to make our types instances of certain type classes, but the type parameters just don’t match up for what we want to do. It’s easy to make Maybe an instance of Functor, because the Functor type class is defined like this:
class Functor f where
fmap :: (a -> b) -> f a -> f b
So we just start out with this:
instance Functor Maybe where
Then we implement fmap.
All the type parameters add up because Maybe takes the place of f in the definition of the Functor type class. Looking at fmap as if it worked on only Maybe, it ends up behaving like this:
fmap :: (a -> b) -> Maybe a -> Maybe b
Isn’t that just peachy? Now what if we wanted to make the tuple an instance of Functor in such a way that when we fmap a function over a tuple, it is applied to the first component of the tuple? That way, doing fmap (+3) (1, 1) would result in (4, 1). It turns out that writing the instance for that is kind of hard. With Maybe, we just say instance Functor Maybe where because only type constructors that take exactly one parameter can be made an instance of Functor. But it seems like there’s no way to do something like that with (a, b) so that the type parameter a ends up being the one that changes when we use fmap. To get around this, we can newtype our tuple in such a way that the second type parameter represents the type of the first component in the tuple:
newtype Pair b a = Pair { getPair :: (a, b) }
And now we can make it an instance of Functor so that the function is mapped over the first component:
instance Functor (Pair c) where
fmap f (Pair (x, y)) = Pair (f x, y)
As you can see, we can pattern match on types defined with newtype. We pattern match to get the underlying tuple, apply the function f to the first component in the tuple, and then use the Pair value constructor to convert the tuple back to our Pair b a. If we imagine what the type fmap would be if it worked only on our new pairs, it would look like this:
fmap :: (a -> b) -> Pair c a -> Pair c b
Again, we said instance Functor (Pair c) where, and so Pair c took the place of the f in the type class definition for Functor:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Now if we convert a tuple into a Pair b a, we can use fmap over it, and the function will be mapped over the first component:
ghci> getPair $ fmap (*100) (Pair (2, 3))
(200,3)
ghci> getPair $ fmap reverse (Pair ("london calling", 3))
("gnillac nodnol",3)
On newtype Laziness
The only thing that can be done with newtype is turning an existing type into a new type, so internally, Haskell can represent the values of types defined with newtype just like the original ones, while knowing that their types are now distinct. This means that not only is newtype usually faster than data, its pattern-matching mechanism is lazier. Let’s take a look at what this means.
As you know, Haskell is lazy by default, which means that only when we try to actually print the results of our functions will any computation take place. Furthemore, only those computations that are necessary for our function to tell us the result will be carried out. The undefined value in Haskell represents an erroneous computation. If we try to evaluate it (that is, force Haskell to actually compute it) by printing it to the terminal, Haskell will throw a hissy fit (technically referred to as an exception):
ghci> undefined
*** Exception: Prelude.undefined
However, if we make a list that has some undefined values in it but request only the head of the list, which is not undefined, everything will go smoothly. This is because Haskell doesn’t need to evaluate any other elements