Search code examples
haskellcompositionhaskell-lensarrows

Horizontal composition of Lenses


Arrows have two kinds of composition, vertical:

(.) :: Arrow cat => cat b c -> cat a b -> cat a c

and horizontal:

(***) :: Arrow cat => cat a b -> cat a' b' -> cat (a,a') (b,b')

(I apologize to any category theoryists for any abuse of terminology)

Lenses can also compose vertically:

(.) :: Lens s t q r -> Lens q r a b -> Lens s t a b

Can they compose horizontally?


Using the normal definition of van Laarhoven lenses and concrete lenses:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a

data ALens s t a b = ALens
  { get :: s -> a
  , set :: b -> s -> t
  }
type ALens' s a = ALens s s a a

fromALens :: ALens s t a b -> Lens s t a b
toALens :: Lens s t a b -> ALens s t a b

I figured out how to horizontally compose simple concrete lenses:

along :: ALens' s a -> ALens' s b -> ALens' s (a,b)
along l0 l1 = ALens
  { get = \s -> (get l0 s, get l1 s)
  , set = \(a,c) -> set l1 c . set l0 a
  }

And how to use that definition to horizontally compose simple van Laarhoven lenses:

zip :: Lens' s a -> Lens' s b -> Lens' s (a,b)
zip l0 l1 = fromALens (toALens l0 `along` toALens l1)

Without changing the implementation, I can even relax the type signatures of along and zip to allow one of the lenses to be polymorphic:

along :: ALens' s a -> ALens s t b c -> ALens s t (a,b) (a,c)
zip :: Lens' s a -> Lens s t b c -> Lens s t (a,b) (a,c)

And after some manual equational reasoning, I managed to implement zip without conversion to concrete lenses and back:

zip l0 l1 h s = h (s ^. l0, s ^. l1) <&> \(a,c) -> s & (l0 .~ a) & (l1 .~ c)

(though I'm certainly not confident that it's the most elegant or law-abiding implementation)

What I'm still wondering is whether there's an equivalent horizontal composition for non-simple lenses:

(***) :: Lens r s a b -> Lens s t c d -> Lens r t (a,a') (b,b')

Solution

  • Lenses cannot have proper horizontal composition. All the lens laws are trivial to violate when combining lawful lenses horizontally.

    Your zip breaks down whenever the lenses overlap. What's the result of 10 & zip id id .~ (1, 2)? What you have will work when the lens targets are completely disjoint from each other, so it's not wrong. It's just not general horizontal composition. There are additional constraints on the input values.

    The situation gets worse when you want to combine non-simple lenses. Now you have a type problem as well, even if the lenses are disjoint. Given disjoint lenses lens1 :: Lens s1 t1 a1 b1 and lens2 :: Lens s2 t2 a2 b2, what would the type of their horizontal composition be? Lens s t (a1, a2) (b1, b2) is the ultimate goal, of course, for some s and t. From the way a lens operates, we know (s ~ s1, s ~ s2), so that's not a problem. But what is t? There's no way to calculate it. You'd want some sort of operation along the lines of "apply the changes made to the type by each", but that's not well-defined. Best case, you might be able to create a type family to cover it for specific combinations of s, t1, and t2.