I am trying to access a nested record using lenses and prisms in Haskell:
import Data.Text (Text)
import Control.Lens.TH
data State = State
{ _stDone :: Bool
, _stStep :: StateStep
}
data StateStep
= StatePause
| StateRun
{ _stCounter :: Int
, _stMMistake :: Maybe Text
}
makeLenses ''State
makeLenses ''StateStep
makePrisms ''StateStep
main :: IO ()
main = do
let st = State False $ StateRun 0 Nothing
-- works, but the `_2` seems weird
mMistake = st ^? stStep . _StateStepRun . _2 . _Just
-- why not something like (the following does not compile)
mMistake = st ^. stStep . _StateStepRun . _Just . stMMistake
The line that works leaves some questions open. I am unsure whether or not the type match by coincidence. The field _stMMistake
has type Maybe Text
, but what about
let st = State False StatePause
? I am missing the explicit join
.
And I am clueless about how prisms work. While it seems logical for the prism to give me a tuple, at the same time I expected something composable in the sense that I can go deeper into my nested structure, using lenses. Do I have to derive my instances manually for this, maybe?
Updated: As per comments, I've fixed some errors and added a few asides in [[double square brackets]].
Here's how/why your first mMistake
works...
A prism is an optic that focus on a "part" that may or may not be present in the "whole". [[Technically, it focuses on the sort of part that can be used to reconstruct an entire whole, so it really pertains to a whole that can come in several alternative forms (as in the case of a sum type), with the "part" being one of those alternative forms. However, if you're only using a prism for viewing and not setting, this added functionality isn't too important.]]
In your example, both _StateRun
and _Just
are prisms. The _Just
prism focuses on the a
part of a Maybe a
whole. Such an a
may or may not be present. If the Maybe a
value is Just x
for some x :: a
, the part a
is present and has value x
, and that's what _Just
focuses on. If the Maybe a
value is Nothing
, then the part a
is not present, and _Just
doesn't focus on anything.
It's somewhat similar for your prism _StateRun
. If the whole StateStep
is a StateRun x y
value, then _StateRun
focuses on that "part", represented as a tuple of the fields of the StateRun
constructor, namely (x, y) :: (Int, Maybe Text)
. On the other hand, if the whole StateStep
is a StatePause
, that part isn't present, and the prism doesn't focus on anything.
When you compose prisms, like _StateRun
and _Just
, and lenses, like stStep
and _2
, you create a new optic that combines the composed series of focusing operations.
[[As was pointed out in the comments, this new optic isn't a prism; it's "only" traversal. In fact, it's a specific kind of traversal, called an "affine traversal". A run-of-the-mill traversal can focus on zero or more parts, while an affine traversal focuses on exactly zero (part not present) or one (unique part present). The lens
library doesn't make the distinction between affine traversals and other sorts of traversals, though. The reason the new optic is "only" an affine traversal instead of a prism relates to that earlier technical point. Once you add lenses, you remove your ability to reconstruct the entire "whole" from a single "part". Again, if you're only using the optics for viewing, not setting, it won't really matter.]]
Anyway, consider the optic (affine traversal):
optic1 = stStep . _StateRun . _2 . _Just
This optic views a whole of type State
. The first lens stStep
focuses on its StateStep
field. If that StateStep
is a StateRun x (Just y)
value, then the _StateRun
prism focuses on the (x, Just y)
part, while the _2
lens further focuses on the Just y
part, and the _Just
prism further focuses on the y :: Text
part.
On the other hand, if the StateStep
field is a StatePause
, the optic optic1
doesn't focus on anything (because the second component prism _StateRun
doesn't focus on anything), and if it's a StateRun x Nothing
, the optic optic1
still doesn't focus on anything, because even though _StateRun
can focus on (x, Nothing)
and _2
can focus on Nothing
, that final _Just
doesn't focus on anything, so the whole optic fails to focus.
In particular, there's no danger that the lens _2
will "misfire" when processing a StatePause
and try to reference a missing second field or anything like that. The fact that you've used _StateRun
to focus on the tuple of fields of a StateRun
constructor ensures that the desired field will be present if the whole optic focuses.
Now, here's why your second optic:
optic2 = stStep . _StateRun . _Just . stMMistake
doesn't work...
There are actually two problems. First, stStep . _StateRun
takes a whole State
and focuses on a part (Int, Maybe Text)
. This isn't a Maybe
value, so it can't compose with the _Just
prism yet. You want to select the Maybe Text
field first, then apply the _Just
prism, so what you actually want is something more like:
optic3 = stStep . _StateRun . stMMistake . _Just
This looks like it really should work, right? The stStep
lens focuses on a StateStep
, the _StateRun
prism should focus only when a StateRun x y
value is present, and the lens stMMistake
ought to let you focus on the y :: Maybe Text
, leaving the _Just
to focus on the Text
.
Unfortunately, this isn't how the prisms created with makePrisms
work. The _StateRun
prism focuses on a plain old tuple with unnamed fields, and those fields need to be further selected with _1
, _2
, etc., not stMMistake
which is trying to select a named field.
In fact, if you take a careful look at stMMistake
, you'll discover that -- all by itself -- it's an optic (an affine traversal, or as far as the lens
library is concerned, just a traversal) that takes a whole StateStep
and focuses on the _stMMistake
field part directly, without having to specify the constructor. So, you can actually use stMMistake
in place of _StateStepRun . _2
, and the following should work identically:
mMistake = st ^? stStep . _StateStepRun . _2 . _Just
mMistake = st ^? stStep . stMMistake . _Just
This isn't some fundamental theoretical property of lenses or anything. It's just the naming and typing convention used by makeLenses
and makePrisms
. With makeLenses
, you create optics that focus on named fields of data structures. If there's only one constructor:
data Foo = Bar { _x :: Int, _y :: Double }
or if there are multiple constructors but the field is present in all constructors:
data Foo = Bar { _x :: Int, _y :: Double }
| Baz { _x :: Int, _z :: Char }
then the field optic (x
in this example) is a lens that always focuses on that field. If there are multiple constructors and some have the field and some don't:
data Foo = Bar { _x :: Int, _y :: Double }
| Baz { _x :: Int, _z :: Char }
| Quux { _f :: Int -> Double }
then the field optic (x
here) is an optic (traversal) that focuses on the field, but only when it's present (i.e., when the value is a Bar
or a Baz
but not when it's a Quux
).
On the other hand makePrisms
always creates constructor prisms that focus on the fields as unnamed tuples, and those fields will need to be referenced with _1
, _2
, etc., rather than any names those fields happen to have within that constructor.
Maybe that answers your question?