Search code examples
haskellfunctional-programmingfunctor

Making a datatype an instance of Functor to map on a field which is of parametric type


Follow up on this question about Learn You a Haskell for Great Good.

The author, at the end of Chapter 8 declares this datatype (slightly simplified, I hope it's fine)

data Barry t k p = BarryV p (t k) deriving (Show)

and then makes it an instance of Functor

instance Functor (Barry a b) where
  fmap f (BarryV x y) = BarryV (f x) y

then concluding

There we go! We just mapped the f over the first field.

Yes. The first. So my question is: what if I want to map over, say, the second field?

Actually the second field cannot be of a type as simple as Int, Char, Float, and so on; it has to be of a type which can be obtained as a type constructor applied to a concrete type (the italicised text is the same as "parametric type", right? no, it is parametrized type), such as Just 3, Right "hello", "hello", [1..10], and so on; therefore mapping on the second field and mapping on the content of the second field seems different.

I'm really confused, but I guess the last paragraph is enough of an effort that I show.


Solution

  • The Functor type class is too general to apply a map over the type t k of the second field, but it could apply a map over the concrete type k within the type of the second field. So, using the terminology from your question, we can't use Functor to map over the second field of type t k, but we can use it to map over the content of type k within the second field of type t k (provided t is the sort of structure that allows mapping over its contents).

    With respect to trying to use Functor to map over the type t k, the problem is that it allows transformations that would violate the definition of the Barry type. The following function:

    censor :: (Functor f) => f a -> f ()
    censor = (() <$)
    

    should apply to any functor instance, replacing fields of the targetted type a with unit (). For example:

    > censor (Just 5)
    Just ()
    > censor [1..5]
    [(),(),(),(),()]
    

    If Barry was somehow a functor in the type t k of its second field, then I would be able to take a valid Barry value:

    > let myBarry = BarryV 10 "hello" :: Barry [] Char Int
    

    and apply censor to it to censor its second field:

    > censor myBarry
    BarryV 10 ()
    

    But what is the type of this value? It's clearly Barry t k Int for some t and k such that t k = (), but that's impossible. There's no way to "split" the type () into two parts t and k. So, BarryV 10 () isn't a value of a valid Barry type, and it's existence would mean we'd constructed an invalid Barry type in our program.

    On the other hand, we could create a Functor instance for Barry in the k parameter. We can't do this directly, because Haskell syntax only allows us to define Functor instances for a type expression that target its "last" parameter. So Barry t k p can be made a Functor in the last parameter p by defining a Functor instance for Barry t k, but it can't be made a Functor in the middle parameter k.

    If we had a variant with the parameters in a different order:

    data Larry p t k = LarryV p (t k) deriving (Show)
    

    then we could define the Functor instance:

    instance Functor (Larry p t) where
      fmap f (LarryV p tk) = LarryV p (fmap f tk)
    

    This gives a type error, saying that there's no Functor instance for t, but if we restrict ourselves to defining this instance only when we have Functor t, it works fine:

    instance Functor t => Functor (Larry p t) where
      fmap f (LarryV p tk) = LarryV p (fmap f tk)
    

    Now, as long as t is a Functor, we have Larry p t a Functor. For example:

    > let myLarry = LarryV 10 "hello"
    > :t myLarry
    myLarry :: Num p => Larry p [] Char
    > import Data.Char
    > fmap toUpper myLarry
    LarryV 10 "HELLO"
    

    This works because t = [] is a Functor, so we get the instance we need.

    Note that in practical code, instead of introducing a new type Larry, the standard way of defining a Functor instance in a "middle" parameter is to use a newtype wrapper, something like:

    newtype Barry' p t k = Barry' (Barry t k p)
    instance Functor t => Functor (Barry' p t) where
      fmap f (Barry' (BarryV p tk)) = Barry' (BarryV p (fmap f tk))