Search code examples
haskelllenseshaskell-lens

How do I combine lenses and functors?


I'm trying to get used to the lens library for Haskell, and find myself struggling at some simple problems. For instance, let's say (for convenience) that at and _1 have the following types (this is how I understand them, at least):

at :: Ord k => k -> Lens' (Map k v) (Maybe v)

_1 :: Lens' (a, b) a

How do I combine these lenses into a lens with the following type:

maybeFst :: Ord k => k -> Lens' (Map k (a, b)) (Maybe a)

Solution

  • You'd like a lens like

    Lens' (Maybe (a, b)) (Maybe a)
    

    but that can't quite be a Lens since putting back Nothing affects the b as well. It can be a Getter

    getA :: Getter (Maybe (a, b)) (Maybe a)
    getA = to (fmap fst)
    

    but then when you compose it you'll just wind up with a Getter as well, not a full Lens

    maybeFst :: Ord k => k -> Getter (Map k (a, b)) (Maybe a)
    maybeFst k = at k . getA
    

    Probably better than that is to use a Traversal instead

    maybeFstT :: Ord k => k -> Traversal' (Map k (a, b)) a
    maybeFstT k = at k . _Just . _1
    

    This will allow you to both get (using preview or toListOf) and set values at the fst of the values in your map, but you won't be able to modify its existence in the map: if the value does not exist you cannot add it and if it does exist you cannot remove it.


    Finally, we can jury-rig a fake Lens which has the appropriate type, though we have to give it a default value for b

    getA :: b -> Lens' (Maybe (a, b)) (Maybe a)
    getA b inj Nothing       = (\x -> (,b) <$> x) <$> inj Nothing
    getA _ inj (Just (a, b)) = (\x -> (,b) <$> x) <$> inj (Just a)
    

    but notice that it has some not-very-Lenslike behavior.

    >>> Just (1, 2) & getA 0 .~ Nothing & preview (_Just . _2)
    Nothing
    
    >>> Nothing & getA 0 .~ Just 1
    Just (1,0)
    

    so often it's better to avoid these pseudolenses to prevent mishaps.