Lenses characterise the has-a relationship; Prisms characterise the is-a relationship.
A Lens s a
says "s
has an a
"; it has methods to get exactly one a
from an s
and to overwrite exactly one a
in an s
. A Prism s a
says "a
is an s
"; it has methods to upcast an a
to an s
and to (attempt to) downcast an s
to an a
.
Putting that intuition into code gives you the familiar "get-set" (or "costate comonad coalgebra") formulation of lenses,
data Lens s a = Lens {
get :: s -> a,
set :: a -> s -> s
}
and an "upcast-downcast" representation of prisms,
data Prism s a = Prism {
up :: a -> s,
down :: s -> Maybe a
}
up
injects an a
into s
(without adding any information), and down
tests whether the s
is an a
.
In lens
, up
is spelled review
and down
is preview
. There’s no Prism
constructor; you use the prism'
smart constructor.
What can you do with a Prism
? Inject and project sum types!
_Left :: Prism (Either a b) a
_Left = Prism {
up = Left,
down = either Just (const Nothing)
}
_Right :: Prism (Either a b) b
_Right = Prism {
up = Right,
down = either (const Nothing) Just
}
Lenses don't support this - you can't write a Lens (Either a b) a
because you can't implement get :: Either a b -> a
. As a practical matter, you can write a Traversal (Either a b) a
, but that doesn't allow you to create an Either a b
from an a
- it'll only let you overwrite an a
which is already there.
Aside: I think this subtle point about Traversal
s is the source of your confusion about partial record fields.
^?
with plain lenses allows getting Nothing
if the field in question doesn't belong to the branch the entity represents
Using ^?
with a real Lens
will never return Nothing
, because a Lens s a
identifies exactly one a
inside an s
.
When confronted with a partial record field,
data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
makeLenses
will generate a Traversal
, not a Lens
.
wobble :: Traversal' Wibble Int
wubble :: Traversal' Wibble Bool
For an example of this how Prism
s can be applied in practice, look to Control.Exception.Lens
, which provides a collection of Prism
s into Haskell's extensible Exception
hierarchy. This lets you perform runtime type tests on SomeException
s and inject specific exceptions into SomeException
.
_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
(These are slightly simplified versions of the actual types. In reality these prisms are overloaded class methods.)
Thinking at a higher level, certain whole programs can be thought of as being "basically a Prism
". Encoding and decoding data is one example: you can always convert structured data to a String
, but not every String
can be parsed back:
showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
up = show,
down = listToMaybe . fmap fst . reads
}
To summarise, Lens
es and Prism
s together encode the two core design tools of object-oriented programming, composition and subtyping. Lens
es are a first-class version of Java's .
and =
operators, and Prism
s are a first-class version of Java's instanceof
and implicit upcasting.
One fruitful way of thinking about Lens
es is that they give you a way of splitting up a composite s
into a focused value a
and some context c
. Pseudocode:
type Lens s a = exists c. s <-> (a, c)
In this framework, a Prism
gives you a way to look at an s
as being either an a
or some context c
.
type Prism s a = exists c. s <-> Either a c
(I'll leave it to you to convince yourself that these are isomorphic to the simple representations I demonstrated above. Try implementing get
/set
/up
/down
for these types!)
In this sense a Prism
is a co-Lens
. Either
is the categorical dual of (,)
; Prism
is the categorical dual of Lens
.
You can also observe this duality in the "profunctor optics" formulation - Strong
and Choice
are dual.
type Lens s t a b = forall p. Strong p => p a b -> p s t
type Prism s t a b = forall p. Choice p => p a b -> p s t
This is more or less the representation which lens
uses, because these Lens
es and Prism
s are very composable. You can compose Prism
s to get bigger Prism
s ("a
is an s
, which is a p
") using (.)
; composing a Prism
with a Lens
gives you a Traversal
.