Search code examples
haskellhaskell-lens

Idiomatic style with lenses in Haskell


I'm still not fully understanding lenses.

applicationState = (
    'a',
    'b',
    (   M.fromList $ zip [1..3]  [11,22,33],
        M.fromList $ zip [4,5,6] [44,55,66],
        M.fromList $ zip [7,8,9] [M.fromList $ zip [2,3,4] ["77","777","7777"],
        M.fromList $ zip [2,3,4] ["88","888","8888"],
        M.fromList $ zip [2,3,4] ["99","999","9999"]] )
    )

Is there a more lens-idiomatic way of doing this:

λ> view (_3 . _3 . at 9) applicationState >>= view (at 4) >>= return . ('9':)
Just "99999"

Solution

  • You can use traverse :: Traversal (Maybe a) (Maybe b) a b and then use preview:

    preview (_3 . _3 . at 9 . traverse . at 4 . traverse . to ('9':)) applicationState
    

    Because ix i = at i . traverse, this can also be written as:

    preview (_3 . _3 . ix 9 . ix 4 . to ('9':)) applicationState
    

    You can also modify that way, but if any of the at's fail, nothing will be changed (you cannot insert new elements in your map using a Traversal).


    Another way is to use non :: a -> Lens' (Maybe a) a. This only works if the first map (accessed by the lens _3._3) never contains an empty map as a value and the second map never contains an empty string as a value. So the following value would be invalid for applicationState:

    ('a', 'b', 
      ( M.fromList $ zip [1..3] [11,22,33]
      , M.fromList $ zip [4,5,6] [44,55,66]
      , M.fromList $ zip [7..9] [M.empty] -- No value of this map may be the empty map
      )
    )
    

    With non, your example can be written as:

    view (_3 . _3 . at 9 . non M.empty . at 4 . non "" . to ('9':)) applicationState
    

    This also allows inserting new entries via setting (if you set and the keys 4 or 9 don't exist yet, they will be created) and deleting entries (if you set to the value "", the key 4 will be removed, and if the map is empty after removing that key, the key 9 will also be removed).