Search code examples
javascriptramda.jspointfree

Pointfree composed function with spreaded arguments


I'm trying to figure out if there is a pattern for writing pointfree composed function when the arguments should be spreaded in curried composing functions
i.e (with Ramda):

add_1_and_multiply = (add, mul) => R.compose(R.multiply(mul), R.add(1))(add)
add_1_and_multiply(3, 5)  // 20

How to write add_1_and_multiply in pointfree style?


Solution

  • I'm not sure if you can easily combine pointfree style and non-unary arity. Think first what should be the type of the resulting and composed functions:

    // Compose:         (B  ->  C) -> (A  ->  B) ->  A  ->  C
    const compose = f => g => x => f(g(x))
    // Add:              A  ->  A  ->  A
    const add = x => y => x + y
    // Mul:              A  ->  A  ->  A
    const mul = x => y => x * y
    
    // Add1:             A  ->  A
    const add1 = add(1)
    
    // Add1AndMul:       A  ->         A  ->  A
    //   because:
    //     Add1:         A  ->  A
    //     Mul:                 A  ->  A  ->  A
    const add_1_and_mul = compose(mul)(add1)
    
    // Mul4:             A  ->  A
    const mul_4 = add_1_and_mul(3)
    const result = mul_4(5) //> 20
    

    Ramda has uncurryN so you can wrap it around the compose and get rid of the currying of the resulting function.

    const add_1_and_multiply = R.uncurryN(2, R.compose(R.multiply, R.add(1)))
    let result2 = add_1_and_multiply(3, 5) //> 20
    

    To add another function to the "chain" you need to compose it with previous function.

    // Add1AndMul:          A -> A -> A
    const add1_mul = compose(mul)(add1)
    

    This is our desired signature.

    //                      1         2         3
    // Add1AndMulAndAdd:    A ->      A ->      A -> A
    //  which is:           |         |         |
    //      Add1:           A -> A    |         |
    //      Mul:                 A -> A -> A    |
    //      Add:                           A -> A -> A
    

    So somehow we have to pass those A2 and A3 without any "points". Let's try just simple composition and analyze it:

    let add1_mul_add = compose(add)(add1_mul)
    

    Remeber signature of compose: (E -> F) -> (D -> E) -> D -> F! Analyzing it in steps:

    1. We supply our add function signature instead of (E -> F)

       (E -> F     )
       (A -> A -> A)
      

      We conclude that

       E = A
       F = A -> A
      
    2. We do the same to (D -> E) and add1_mul

       (D -> E     )
       (A -> A -> A)
      

      We conclude that

       D = A
       E = A -> A
      

    But we can already see a contradiction there! Conclusion in step 2 contradicts conclusion in step 1: E cannot be A and A -> A at the same time.

    Therefore we cannot compose add and add1_mul and our add1_mul_add will throw an error.

    Let's try to workaround the problem and fix it breaking our promise of pointfree style.

    const add1_mul_add = x => compose(add)(add1_mul(x))
    

    I'm going to break some rules and mix signatures with code to illustrate my point:

    x -> (A -> A -> A) -> (x -> A -> A) -> A -> A -> A
                           ||
                           \/
    x -> (A -> A -> A) -> (A -> A) -> A -> A -> A
         (E -> F     ) -> (D -> E) -> D -> F
    

    So we got our correct compose signature! How to get rid of the x variable to go back to pointfree? We can try to look for obvious patterns, like for example... our ye olde function composition!

    f(g(x)) => compose(f)(g)
    

    And we find this pattern in our new add1_mul_add -

    f = compose(add)
    g = add1_mul
    f(g(x)) = compose(add)(add1_mul(x))
    

    And we reduce it to pointfree and we got our new add1_mul_add function:

    const add1_mul_add = compose(compose(add))(add1_mul)
    

    But hey - we can reduce it even more!

    const add1_mul_add = compose(compose)(compose)(add)(add1_mul)
    

    And there we have found something that already exists in haskell under the name of The Owl.

    We can define it in Javascript simply as:

    const owl = compose(compose)(compose)
    

    But now, with every new function in the chain, you would have to create a higher order of the owl operator.

    const owl2 = compose(compose)(owl)
    const add1_mul_add_mul = owl2(mul)(add1_mul_add)
    
    const owl3 = compose(compose)(owl2)
    const add1_mul_add_mul_add = owl3(add)(add1_mul_add_mul)
    

    So I really recommend having your functions unary in pointfree style. Or use other constructs like lists:

    const actions = [ add, mul, add, mul ]
    const values  = [ 1,   2,   3,   4   ]
    const add_mul_add_mul = (...values) => zip(actions, values).reduce((acc, [action, value]) => action(acc, value), 0)