Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composing lenses with `at` and `ix`

Let's say I have some fairly simple data type Person with a couple of fields, and a type that holds a collection of Persons.

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?

like image 963
Chris Taylor Avatar asked Jan 03 '14 10:01

Chris Taylor


People also ask

What is the use of combination of lenses?

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.

How do you use two lenses to focus the image?

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

Are the powers of thin lenses in contact lenses additive?

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


2 Answers

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.

like image 164
András Kovács Avatar answered Oct 01 '22 15:10

András Kovács


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
like image 33
Chris Taylor Avatar answered Oct 01 '22 14:10

Chris Taylor