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?
CodePudding user response:
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". 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 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 prism that combines the composed series of focusing operations. So consider the prism:
prism1 = stStep . _StateRun . _2 . _Just
This prism 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 prism prism1
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 prism prism1
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 prism 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 prism focuses.
Now, here's why your second prism:
prism2 = 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:
prism3 = 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 a prism 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 a prism 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?