Let's say I have some fairly simple data type Person
with a couple of fields, and a type that holds a collection of Person
s.
data Person = Person { _name :: String, _age :: Int }
data ProgramState = PS { _dict :: IntMap Person }
makeLenses ''Person
makeLenses ''ProgramState
I want to create a lens that allows me to access individual people by looking up their key
person :: Int -> Lens' ProgramState Person
It seems my two options for doing this are to use at
or ix
to index into the dictionary
-- Option 1, using 'at'
person :: Int -> Lens' ProgramState (Maybe Person)
person key = dict . at key
-- Option 2, using 'ix'
person :: Int -> Traversal' ProgramState Person
person key = dict . ix key
but neither of these options lets me do what I want, which is to have a Lens'
that accesses a Person
rather than a Maybe Person
. Option 1 doesn't compose nicely with other lenses, and option 2 means that I have to give up my getters.
I understand why ix
and at
are written like this. The key might not exist in the dict, so if you want a Lens'
which enables both getters and setters, it must access a Maybe a
. The alternative is to accept a Traversal'
which gives access to 0 or 1 values, but that means giving up your getters. But in my case, I know that the element I want will always be present, so I don't need to worry about missing keys.
Is there a way to write what I want to write - or should I be rethinking the structure of my program?
Combination of lenses An array of simple lenses with a common axis can be used to multiply the magnification of an image. The real image formed by one lens can be used as the object for another lens, combining magnifications.
A schematic of a simple telescope is a good example of the use of two lenses to focus the image of one lens: • If the lenses of focal lengths f1 and f2are “thin”, the combined focal length f of the lenses is given by 1/f=1/f1+1/f2
Since 1/f is the power of a lens, it can be seen that the powers of thin lenses in contact are additive. Separated lenses: If two thin lenses are separated in air by some distance d (where d is smaller than the focal length of the first lens), the focal length for the combined system is given by
You probably want to use at
together with the non
isomorphism. You can specify a default map entry with it to get rid of the Maybe
of the lookup.
non :: Eq a => a -> Iso' (Maybe a) a
person key = dict . at key . non defaultEntry
-- can get and set just like plain lenses
someProgramState & dict . at someKey . non defaultEntry .~ somePerson
You can look at more examples in the docs.
Based on András Kovács answer I ended up defining an unsafeFromJust
lens that witnesses the 'isomorphism' I require to compose these lenses
import Data.Maybe (fromJust)
unsafeFromJust :: Lens' (Maybe a) a
unsafeFromJust = lens fromJust setJust
where
setJust (Just _) b = Just a
setJust Nothing _ = error "setJust: Nothing"
An alternative definition is
unsafeFromJust :: Lens' (Maybe a) a
unsafeFromJust = anon (error "unsafeFromJust: Nothing") (\_ -> False)
but I felt that wasn't as clear as the first form. I didn't use non
as that requires an Eq
instance that is unnecessary in this case.
I can now write
person :: Lens' ProgramState Person
person key = dict . at key . unsafeFromJust
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With