1

Suppose I have two functions with the following types.

f :: (a, b) -> c
g :: (a, b, c) -> d

I can compose them as follows.

function h(a, b) {
    const c = f(a, b);
    const d = g(a, b, c);
    return d;
}

Here, h is a composition of g and f. However, this looks a lot like imperative code with the constant declarations and the return statement. How can I compose any two such functions in a functional style?

2
  • Please use correct tags for your question. [python] tag isn't needed here Commented Feb 19, 2021 at 10:30
  • I would expect that the implementation of g would simply use f. Instead of g :: (a, b, d) --> <do something with a, b, and d> it would look like g :: (a, b) -> < do something with a, b, and f(a, b)> Commented Feb 19, 2021 at 15:47

3 Answers 3

2

You can define h in a single line as follows.

const h = (a, b) => g(a, b, f(a, b));

Then you can generalize this composition for all such functions as follows.

const ss = (g, f) => (a, b) => g(a, b, f(a, b));

const h = ss(g, f);

This is actually the same as the S combinator but extended to two inputs. Hence, the name ss.

Alternatively, we could generalize the S combinator to any number of inputs using the spread operator as Scott Sauyet suggested.

const s = (g, f) => (...args) => g(...args, f(...args));

const h = s(g, f);

Personally, I'd stay away from polyvariadic functions.

By the way, you original example is not imperative even though it uses constant declarations.

Sign up to request clarification or add additional context in comments.

Comments

1

This looks like a problem of how to combine two functions in a somewhat different way than by simple composition. We can write combinators that do this for us.

In this case, we essentially want to use our initial a and b arguments in f to generate d and use a, b, and d in g. We can write a function that combines arbitrary f and g to do this, keeping our original arguments and adding an additional one in the call to g. Maybe we'd call it addArg:

const addArg = (g, f) => (...args) => 
  g (...args, f (...args))

This version attempts to be a bit more generic; we don't handle only two parameters. We can have any number of arguments to f, and we add one more to the call to g.

Here is a quick demo:

const addArg = (g, f) => (...args) => 
  g (...args, f (...args))

const f = (a, b) => `f (${a}, ${b})`
const g = (a, b, d) => `g (${a}, ${b}, ${d})`

const h = addArg (g, f) 

console .log (h ('a', 'b'))

Comments

1

I don't think there is anything wrong with the previous answers but I wanted to add an alternative and also to warn you about combinators.

I feel uncomfortable with the use of custom combinators because the reader must look up their definition, which reduces the value of using them.

I am familiar with lift2 and my first instinct would be to bend it to apply it to your pattern (your functions must be curried for it to work)

const lift2 = f => g => h => x => f (g (x)) (h (x));
const id = x => x;

const h = (a, b) => lift2 (g (a)) (id) (f (a)) (b);

So we're partially applying f and g with a and using id to store b until it's fed to g.

If you have trouble parsing the above, we can express lift2 like so, to make it more clear what is happening:

const lift2 = ga => id => fa => b => ga (id (b)) (fa (b));

Honestly I would go for the first one-liner suggested by Aadit M Shah, or for the second one, but named h, to sort of convey "it's just the h you would expect, but with dependencies injected"... or your implementation. Yours even provides explanatory variable names. I mean why not!

Even known combinators sometimes only obfuscate intent. It all depends with whom you work. And that includes your future self ;)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.