Haskell - Generating lenses for third party libraries

Posted on June 9, 2020

This is going to be a short post, maybe even a tweet size post. I’m going to highlight how to generate lenses for third party libraries because when I was searching for this information it wasn’t that easy to find.

My concern was less on the mechanics on how to generate them but more on best practices, I wasn’t sure if generating lenses for data types I don’t own is good practice. When I asked in fp-chat, I was told to “Go hog wild!” and didn’t get any opposing views. So that gave me the peace of mind to do this.

I’m going to use the purescript language library as an example since it’s the most recent library I had to work with.

I’m going to use makeLenses from Control.Lens.TH. The default configuration of makeLenses is it will only generate lenses for record fields that are prefixed with an underscore.

data Person = Person
  { firstName :: Text -- does not generate lens for this field.
  , _lastName :: Text -- generates the lens for this field.
  } deriving ( Eq, Show )

makeLenses ''Person

Before I can start generating lenses for the types I don’t own, I have to change the makeLenses configuration a little bit.

module Util where

import           Language.Haskell.TH
import           Control.Lens.Operators
import           Control.Lens.TH

mkCustomLenses :: Name -> DecsQ
mkCustomLenses = makeLensesWith
  $ lensRules
  & lensField
  .~ (\_ _ name -> [TopName ( mkName $ nameBase name ++ "L" )]) -- you can append whatever suits your code base here, I chose to append "L"

It has to be in a separate module from where you generate the lenses because the compiler will complain with this error

GHC stage restriction:
      `mkCustomLenses' is used in a top-level splice, quasi-quote, or annotation,
      and must be imported, not defined locally

After that setup you can start generating lenses!

module Optics where

-- util
import           Util

-- purescript
import           Language.PureScript.CST.Types

makePrisms ''Declaration
makePrisms ''Guarded
makePrisms ''Expr

mkCustomLenses ''ValueBindingFields
mkCustomLenses ''Name
mkCustomLenses ''Ident
mkCustomLenses ''Labeled
mkCustomLenses ''Where
mkCustomLenses ''Wrapped
mkCustomLenses ''Separated
mkCustomLenses ''Module

Now, if I want to manipulate Separated I can do something like

updateToEmptyList :: Separated a -> Separated a
updateToEmptyList s = s & sepTailL .~ []

When it comes to sum types you can just use makePrisms to generate prisms.

Sources

Optics by Example - Chris Penner

Lens Tutorial