Unsound-but-awesome lens combinators: lensProduct and adjoin

🎉I am writing these notes at Brick, a magical mystery no-bullshit publishing platform. Turns out writing goes much faster when I don't have to hit “Publish” or do git commit.

You can use it too — check it out at Brick.do.


I don't use these combinators often, but when I do, they save me from writing a bunch of boring code.

Recently-ish lens got supplemented with an enticingly named module Control.Lens.Unsound. It provides:

-- UNION OF LENSES!
lensProduct :: ALens' s a -> ALens' s b -> Lens' s (a, b)

-- UNION OF TRAVERSALS!!
adjoin :: Traversal' s a -> Traversal' s a -> Traversal' s a

⚠️ The module gives plenty of warning, but we are here for the good news, not the bad news. But yeah — these operations will lead to wrong results if used on overlapping subsets of the structure.

I won't cover lensProduct because I haven't used it recently. Let's look at adjoin.

I have an AST with a bunch of top-level declarations:

data Module = Module
  { functions :: [Function]
  , types :: [Type]
  , classes :: [Class]
  }

Each of Function, Type, Class has a field name. Yeah, this is with -XDuplicateRecordFields enabled. I want to write a traversal that goes through names of all top-level entities — maybe so that you can gather them, maybe so that you can rename them all at once.

Without adjoin, it's kinda meh. Life becomes dull, washed out.

With adjoin, this is super easy. 

import Data.Generics.Labels () -- to get #blah lenses

allTopLevelNames :: Traversal' Module String
allTopLevelNames =
  (#functions . each . #name) `adjoin`
  (#types . each . #name) `adjoin`
  (#classes . each . #name)