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
mMistakeworks...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
_StateRunand_Justare prisms. The_Justprism focuses on theapart of aMaybe awhole. Such anamay or may not be present. If theMaybe avalue isJust xfor somex :: a, the partais present and has valuex, and that's what_Justfocuses on. If theMaybe avalue isNothing, then the partais not present, and_Justdoesn't focus on anything.It's somewhat similar for your prism
_StateRun. If the wholeStateStepis aStateRun x yvalue, then_StateRunfocuses on that "part", represented as a tuple of the fields of theStateRunconstructor, namely(x, y) :: (Int, Maybe Text). On the other hand, if the wholeStateStepis aStatePause, that part isn't present, and the prism doesn't focus on anything.When you compose prisms, like
_StateRunand_Just, and lenses, likestStepand_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
lenslibrary 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):
This optic views a whole of type
State. The first lensstStepfocuses on itsStateStepfield. If thatStateStepis aStateRun x (Just y)value, then the_StateRunprism focuses on the(x, Just y)part, while the_2lens further focuses on theJust ypart, and the_Justprism further focuses on they :: Textpart.On the other hand, if the
StateStepfield is aStatePause, the opticoptic1doesn't focus on anything (because the second component prism_StateRundoesn't focus on anything), and if it's aStateRun x Nothing, the opticoptic1still doesn't focus on anything, because even though_StateRuncan focus on(x, Nothing)and_2can focus onNothing, that final_Justdoesn't focus on anything, so the whole optic fails to focus.In particular, there's no danger that the lens
_2will "misfire" when processing aStatePauseand try to reference a missing second field or anything like that. The fact that you've used_StateRunto focus on the tuple of fields of aStateRunconstructor ensures that the desired field will be present if the whole optic focuses.Now, here's why your second optic:
doesn't work...
There are actually two problems. First,
stStep . _StateRuntakes a wholeStateand focuses on a part(Int, Maybe Text). This isn't aMaybevalue, so it can't compose with the_Justprism yet. You want to select theMaybe Textfield first, then apply the_Justprism, so what you actually want is something more like:This looks like it really should work, right? The
stSteplens focuses on aStateStep, the_StateRunprism should focus only when aStateRun x yvalue is present, and the lensstMMistakeought to let you focus on they :: Maybe Text, leaving the_Justto focus on theText.Unfortunately, this isn't how the prisms created with
makePrismswork. The_StateRunprism focuses on a plain old tuple with unnamed fields, and those fields need to be further selected with_1,_2, etc., notstMMistakewhich 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 thelenslibrary is concerned, just a traversal) that takes a wholeStateStepand focuses on the_stMMistakefield part directly, without having to specify the constructor. So, you can actually usestMMistakein place of_StateStepRun . _2, and the following should work identically:This isn't some fundamental theoretical property of lenses or anything. It's just the naming and typing convention used by
makeLensesandmakePrisms. WithmakeLenses, you create optics that focus on named fields of data structures. If there's only one constructor:or if there are multiple constructors but the field is present in all constructors:
then the field optic (
xin 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:then the field optic (
xhere) is an optic (traversal) that focuses on the field, but only when it's present (i.e., when the value is aBaror aBazbut not when it's aQuux).On the other hand
makePrismsalways 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?