Search code examples
haskelltraversalhaskell-lens

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?


Solution

  • 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.