Search code examples
haskellhaskell-lenslenses

Using lenses to compose functions


Consider "composition" functions with the following (pseudo) signature:

(a1 -> a2 -> ... -> an -> r) -> 
(s -> ai) -> 
a1 -> a2 -> ... -> ai -> ... an -> r
where i in [1..n]

Of course we can't write what's above in Haskell, but here's a concrete example:

f4change3 :: 
  (a1 -> a2 -> a3 -> a4 -> r) -> 
  (s -> a3) -> 
  a1 -> a2 -> s -> a4 -> r
f4change3 f g x1 x2 x3 x4 = f x1 x2 (g x3) x4

As you can see, there's a collection of n functions for each function of arity n, so the number of functions we need grows quadratically with arity.

I could just write the ones I need, but firstly, I don't want to reinvent the wheel, so it would be nice to know if library has already done this. But also, whilst I have barely used lenses, I've read a bit about them, and this sort of problem seems right up their ally, but I'm not sure exactly how to do it. Some example code would be great if it's possible.


Solution

  • As mentioned in the comments, Conal Elliott's semantic editor combinators give a quite beautiful tool for writing these functions. Let's review just two of his combinators:

    argument :: (a' -> a) -> ((a -> b) -> (a' -> b))
    argument = flip (.)
    
    result :: (b -> b') -> ((a -> b) -> (a -> b'))
    result = (.)
    

    In words: argument modifies a function's argument before the function is called, and result modifies a function's return value after it is called. (See the blog post itself for further intuition-bolstering explanations of these combinators.) Let's say we have a function with a type like

    a1 -> a2 -> a3 -> a4 -> a5 -> b
    

    and we want to change a5. Note that we can parenthesize this, of course:

    a1 -> (a2 -> (a3 -> (a4 -> (a5 -> b))))
    

    So if we want to reach a5, how should we reach into this structure? Well, we will do it by repeatedly reaching into the result, then acting on the argument: the result type of this function is a2 -> (a3 -> (a4 -> (a5 -> b))), whose result is a3 -> (a4 -> (a5 -> b)), whose result is a4 -> (a5 -> b), whose result is a5 -> b, whose argument is a5. This gives us the code directly:

    arg5 :: (a5' -> a5)
         -> (a1 -> a2 -> a3 -> a4 -> a5  -> b)
         -> (a1 -> a2 -> a3 -> a4 -> a5' -> b)
    arg5 = result . result . result . result . argument
    

    Hopefully it is quite clear how to generalize this to modify other arguments: just vary the number of times you call result.