Type walk with me

2 minute read

Published:

I lost count of how many times I’ve seen types/functions/families and another first-class abstractions that shouldn’t exist if we want to use more universal constructions. A little bit of theory can reduce and beautify our production code, and now I will try to demonstrate that.

Problem description

Let’s suppose you write a JSON API server that operates on some entities, users, for example. What are you going to do first? Right, define a type:

data User = User { name :: String, age :: Int }

We have a type, then, we need to write an instance of Aeson to convert a value into JSON:

instance ToJSON User where
   user = object ["name" .= name user, "age" .= age user]

So far so good. But someday, a frontend guy says: “Oh, man, on this endpoint, I need an additional information to user object: a list of followers”. Okay, you think. So, I should change my User type and add a field with a list of followers, not a big deal:

type Followers = [User]

data User = User { name :: String, age :: Int, followers :: Followers }

instance ToJSON User where
   toJSON user = object ["name" .= name user, "age" .= age user, "followers" .= followers user]

And yet another day, the frontend guy says: “On that endpoint, I need to get some stats with the user object: a view counter”. You think: “Hmmm, that’s not good, I should create the same datatype that differs from the previous one in just one field.”

But there is a better way.

Open product types

A field of a record syntax is just a multiplier. Can we manipulate these multipliers the way we want and pay nothing for that? Yes. Let’s create an open product type:

{-# language TypeOperators #-}

data (:&:) a b = a :&: b

Looks very easy, right? Let’s decompose the user object and the additional information in this style:

{-# language FlexibleInstances #-}

instance ToJSON (User :&: Followers) where
   toJSON (user :&: followers) = object
      ["user" .= toJSON user, "followers" .= toJSON followers]

But then the frontend guy appears again saying “Man, I need a new endpoint with user, followers and view counter, as fast as possible, please”. And you reply: “No, problem, here you go!”

{-# language FlexibleInstances #-}

type Counter = Int

instance ToJSON (User :&: Followers :&: Counter) where
   toJSON (user :&: followers :&: counter) = object
      ["user" .= toJSON user, "followers" .= toJSON followers, "counter" .= toJSON counter]

There is a little zero dependency package with this type definition called with that I created recently.

Leave a Comment