Haskell records and generic-lens

Posted on October 29, 2020

Haskell is known to have some peculiarities when records are involved. In particular, it can be problematic when we want the same field name for two different records.

Imagine that we have the following situation:

data Person = Person { name :: Text, age :: Int }
data Company = Company { name :: Text, manager :: Person }

This will generate three functions:

name :: Person -> Text
age :: Person -> Int
name :: Company -> Text
manager :: Company -> Person

The compiler disagrees — you can’t do that!

Multiple declarations of ‘name’

We can fix this by enabling the DuplicateRecordFields language extension. However, when we now try to use name in any way, the compiler cannot decide which one we want.

Ambiguous occurrence ‘name’

Here is where lens and generic-lens packages come into play. More specifically, the latter allows us to use labels with the same name to access parts of records with different types.

In addition to enabling the two packages in our project, we also need to:

{-# LANGUAGE OverloadedLabels      #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DuplicateRecordFields #-}

import Control.Lens
import Data.Generics.Labels
import GHC.Generics

data Person = Person
    { name :: Text
    , age  :: Int
    } deriving (Generic)

data Company = Company
    { name    :: Text
    , manager :: Person
    } deriving (Generic)

Once we do this, we can finally query our objects:

λ> p1 = Person "John" 25
λ> c1 = Company "Google" p1

λ> p1 ^. #name
"John"

λ> c1 ^. #name
"Google"

λ> c1 ^. #manager . #name
"John"

A note on the language extensions here: technically, OverloadedLabels is not needed — it is just to enable the syntactic sugar (hashtag). The alternative is to enable TypeApplications and write field @"name" instead of #name.

Do we really need labels?

Usually lens is used in conjunction with makeFields which generates the accessors using TemplateHaskell. Here is what our example from above would look like:

{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE TemplateHaskell        #-}
{-# LANGUAGE TypeSynonymInstances   #-}

import Control.Lens

data Person = Person
    { _personName :: Text
    , _personAge  :: Int
    }

data Company = Company
    { _companyName    :: Text
    , _companyManager :: Person
    }

makeFields ''Person
makeFields ''Company

This generates a bit more sophisticated functions (only three – name, age and manager) than the plain ones above, which means we cannot use them “as is” (we can, of course, still use the prefixed ones). Accessing the fields is similar – this time without hashtags:

λ> c1 ^. manager . name
"John"

So what do we have:

I wouldn’t say that one is better than the other, but I do prefer the labels approach. It just seems cleaner, especially when defining the records.

Consider also the ergonomics of each solution with RecordWildCards enabled — the long prefixed field names get tedious pretty quickly.

Conclusion

With labels you don’t need to prefix each accessor with the record’s name, which can get too wordy and, frankly, ugly. You shouldn’t be afraid of lens — at the very beginning you won’t need anything other than view (or ^. in operator form). When you get more comfortable, you will inevitably start looking into the additional bells and whistles this package provides.

I think this is the cleanest way to work with records in Haskell so far, even if you haven’t used lenses before. I encourage you to watch this great video by Paweł Szulc, where you will see what the hashtag is and why it actually works.