ADT prisms

Lenses are a neat, if theoretically-convoluted, set of operations that allow us to easily manipulate information in nested contexts. Records are the obvious use case for these constructs, but they shine across algebraic data types as well.

What follows is not a lens tutorial: just my notes on a specific case involving ADTs. What I've written here should be obvious to experienced Haskellers, but it wasn't immediately clear to me. As with all my other Haskell pieces, I'm actually using Purescript here. There are small syntax differences and a lot more boilerplate.

Data

Consider an algebraic data type F with two instances: A and B. A contains one value of type X, and B contains types of X and Y respectively. The actual nature of these constituent types is irrelevant, but I've made them strings here.

import Prelude (($), (<<<))
import Data.Lens (Lens', Prism', lens, preview, prism, set, view)
import Data.Maybe (Maybe(..))

Type X = String
Type Y = String

Data F = A X | B X Y

Lens

Since both instances of F contain an X value, a lens can be declared over it, allowing us to access and modify this value without explicit deconstruction of the data type in every procedure. A lens is a pair of two functions: one to access a context's value, and one to modify it. A lens over X in F requires defining these set/get functions for both A and B.

x ∷ Lens' F X
x = lens get set where
  get (A x  )   = x
  get (B x _)   = x
  set (A _  ) x = (A x)
  set (B _ y) x = (B x y)

The view function allows one to retrieve the value of X from either instance of F, while the set function modifies it.

> view x (A "hey")
"hey"

> set x "yo" (B "hey" "there")
(B "yo" "there")

Prism

This is all well and good for X, which exists in both instances, but what about Y, which can only be found in B? A prism is needed. This construct offers a view on a specific value much like a lens does, but focuses on a single instance of an ADT. It is used alongside the preview function, which will return Nothing if the instance doesn't match.

Long story short: Y must first be viewed through a prism on B, being returned as Just Y if F is shaped like B, and Nothing if F is an A.

The problem I ran into, however, is that the prism constructor expects only one argument. I needed to declare the body of B as a record type B' first.

type B' = {x ∷ X, y ∷ Y}

I was then able to write a prism. Much like a lens expects set/get, a prism needs review/preview defined, so that it can create a B from B', or potentially return a B' from F.

b ∷ Prism' F B'
b = prism' review preview where
  review  {x, y}  = B x y
  preview (B x y) = Just {x, y}
  preview  _      = Nothing

As seen above, the prism over b will yield its entire body as a record if an F is B, and Nothing otherwise. Returning a record meant that I also had to declare a lens on the field y.

y ∷ Lens' B' Y
y = lens _.y $ _ {y = _}

The true strength of lenses and prisms is revealed once you start composing them. It is now possible to access Y without regard to the shape of an F; if it's B, we'll get something, but it won't blow up otherwise.

> preview (b <<< y) (B "hey" "there")
(Just "there")

> preview (b <<< y) (A "hey")
Nothing

Imagine an ADT in a nested record structure behind some Eithers or whatever, and you can see how a chain of lenses and prisms can provide unparalleled focus upon what would otherwise be a mess of conditional logic.

The lesson

If you want to avoid using a bridging structure like B', it makes more sense to give every instance of an algebraic data type exactly one value: a record.