Home > Back-end >  How to use lens to access a record field behind a sum type
How to use lens to access a record field behind a sum type

Time:03-01

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?

  • Related