Search code examples
haskelltypeclassaesonhigher-kinded-types

What are FromJSON1 and ToJSON1 used for in aeson?


Aeson provides FromJSON1 and ToJSON1 type classes. These are similar to the Eq1 and Show1 classes defined in the Data.Functor.Classes module.

My understanding of the Eq1 and Show1 classes is that they are needed to be able to express constraints on arguments of transformers without using extensions like FlexibleContexts and UndecidableInstances.

The example from the documentation in the Data.Functor.Classes module is as follows:

Assume we have a data type that acts as a transformer: T. For an example, let's have it be isomorphic to IdentityT:

data T f a = T (f a)

The kind of T is as follows:

T :: (* -> *) -> * -> *

If there is an Eq1 instance for f, it is possible to use it when writing the Eq1 instance for T f:

instance Eq1 f => Eq1 (T f) where
  liftEq :: (a -> b -> Bool) -> T f a -> T f b -> Bool
  liftEq eq (T fa1) (T fa2) = liftEq eq fa1 fa2

If we have an Eq1 instance for f, an Eq instance for a, and the Eq1 instance for T f above is in scope, we can easily write the Eq instance for T f a:

instance (Eq1 f, Eq a) => Eq (T f a) where
  (==) :: T f a -> T f a -> Bool
  (==) = eq1

The type of eq1 is defined as follows:

eq1 :: (Eq1 h, Eq a) => h a -> h a -> Bool

In our instance above, h becomes T f, so the type of eq1 can be thought of as the following:

eq1 :: Eq a => T f a -> T f a -> Bool

Now, the Eq1, Show1, etc classes make sense. It seems like it makes it easier to write instances of Eq, Show, etc for transformers.

However, I'm wondering what types FromJSON1 and ToJSON1 are used for in Aeson? I rarely have transformers that I want to turn to and from JSON.

Most of the data types I end up changing to JSON are normal types (not type constructors). That is to say, types with the kind *. I also uses types like Maybe with a kind of * -> *.

However, I don't think I often create ToJSON or FromJSON instances for transformers, like the T above. What is a transformer that is often used to go to and from JSON? Am I missing out on some helpful transformers?


Solution

  • Eq1 offers another feature that you haven't discussed in your exposition: it lets you write a function that calls (==) at many different types, without necessarily knowing ahead of time which types you will use it on.

    I'll give a toy example; hopefully you can see through the apparent uselessness of this example to the reason Eq1 gives you some interesting powers.

    Imagine you want to make a tree that is parameterized on the branching factor, so you parameterize it by the child container. So values might look like this:

    {-# LANGUAGE GADTs #-}
    data Tree m a where
        Branch :: Tree m (m a) -> Tree m a
        Leaf :: a -> Tree m a
    

    For example, I can get binary trees with Tree Pair, trinary trees with Tree Triple, finger trees with Tree TwoThree, and rose trees with Tree [], where data Pair a = Pair a a, data Triple a = Triple a a a, and data TwoThree a = Two a a | Three a a a. Now I would like to write an Eq instance for this. If we only rely on Eq constraints, we can't get where we want to go. Let's try:

    instance Eq (Tree m a) where
        Leaf a == Leaf a' = a == a'
        Branch t == Branch t' = t == t'
        _ == _ = False
    

    Naturally, GHC complains that it doesn't know how to compare a and a' for equality. So add Eq a to the context:

    instance Eq a => Eq (Tree m a) where ...
    

    Now GHC complains that it doesn't know how to compare m as for equality in the Branch case. Makes sense.

    instance (Eq a, Eq (m a)) => Eq (Tree m a) where ...
    

    Still no go! Now the implementation of (==) :: Tree m a -> Tree m a -> Bool has a recursive call to (==) :: Tree m (m a) -> Tree m (m a) -> Bool in its Branch case, hence must provide the context (Eq (m a), Eq (m (m a))) to make that recursive call. Okay, let's add that to the instance context...

    instance (Eq a, Eq (m a), Eq (m (m a))) => Eq (Tree m a) where ...
    

    Still no good. Now the recursive call has to prove even more stuff! What we'd really like to say is that if we have Eq b, then we have Eq (m b), for all bs and not just for the specific a being used as Tree's second parameter.

    instance (Eq a, (forall b. Eq b => Eq (m b))) => Eq (Tree m a) where ...
    

    Of course that's totally not a thing in Haskell. But Eq1 gives us that:

    instance Eq1 m => Eq1 (Tree m) where
        liftEq (==) (Leaf a) (Leaf a') = a == a'
        liftEq (==) (Branch t) (Branch t') = liftEq (liftEq (==)) t t'
        liftEq (==) _ _ = False
    
    instance (Eq1 m, Eq a) => Eq (Tree m a) where
        (==) = eq1
    

    Here the Eq1 m constraint is serving the role we asked for before, namely, that all of (Eq a, Eq (m a), Eq (m (m a)), ...) are possible.

    The ToJSON1 and FromJSON1 classes serve a similar role: they give you a single constraint that you can give that amounts to a potentially infinite collection of ToJSON and FromJSON constraints, so that you can choose which ToJSON or FromJSON constraint you need in a data-driven way and be guaranteed that it's available.