2

I know this is quite possible since my Haskell friends seem to be able to do this kind of thing in their sleep, but I can't wrap my head around more complicated functional composition in JS.

Say, for example, you have these three functions:

const round = v => Math.round(v);

const clamp = v => v < 1.3 ? 1.3 : v;

const getScore = (iteration, factor) =>
    iteration < 2 ? 1 :
    iteration === 2 ? 6 :
    (getScore(iteration - 1, factor) * factor);

In this case, say iteration should be an integer, so we would want to apply round() to that argument. And imagine that factor must be at least 1.3, so we would want to apply clamp() to that argument.

If we break getScore into two functions, this seems easier to compose:

const getScore = iteration => factor =>
    iteration < 2 ? 1 :
    iteration === 2 ? 6 :
    (getScore(iteration - 1)(factor) * factor);

The code to do this probably looks something like this:

const getRoundedClampedScore = compose(round, clamp, getScore);

But what does the compose function look like? And how is getRoundedClampedScore invoked? Or is this horribly wrong?

Andrew
  • 14,204
  • 15
  • 60
  • 104
  • 1
    I think _"What does the compose function look like"_ is the wrong question. There is only one `compose` function and one shouldn't invent new `compose` functions to suit a specific situation. – JLRishe Jul 19 '18 at 05:43
  • @JLRishe I disagree. Normal function composition (i.e. `(b -> c) -> (a -> b) -> a -> c`) is just the most basic form of function composition. There are more complex forms of function composition too such as `(c -> d) -> (a -> b -> c) -> a -> b -> d`. In this particular question, the form of function composition is `(a -> b -> c) -> (d -> a) -> (e -> b) -> d -> e -> c`. Now, more complex forms of function composition [can be decomposed into simpler forms](https://stackoverflow.com/q/20279306/783743). Asking what the compose function looks like (and how to simplify it) is a reasonable question. – Aadit M Shah Jul 19 '18 at 07:39

3 Answers3

3

The compose function should probably take the core function to be composed first, using rest parameters to put the other functions into an array, and then return a function that calls the ith function in the array with the ith argument:

const round = v => Math.round(v);

const clamp = v => v < 1.3 ? 1.3 : v;

const getScore = iteration => factor =>
    iteration < 2 ? 1 :
    iteration === 2 ? 6 :
    (getScore(iteration - 1)(factor) * factor);

const compose = (fn, ...transformArgsFns) => (...args) => {
  const newArgs = transformArgsFns.map((tranformArgFn, i) => tranformArgFn(args[i]));
  return fn(...newArgs);
}

const getRoundedClampedScore = compose(getScore, round, clamp);

console.log(getRoundedClampedScore(1)(5))
console.log(getRoundedClampedScore(3.3)(5))
console.log(getRoundedClampedScore(3.3)(1))
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    Typically, a `compose` function feeds data through the provided functions in the reverse of the order that they are provided. So in the case of `compose(getScore, round, clamp)` one would expect this to execute `clamp`, then `round`, then `getScore`. A utility function that feeds data through the functions from left to right would typically be called `pipe`. – JLRishe Jul 19 '18 at 04:52
  • 3
    Now that I look at your code a little more closely, the implementation you have there is nothing like `compose`. It's applying all of the functions except the first to each respective argument then feeding those results into the first function. That's not what a `compose` function does. – JLRishe Jul 19 '18 at 05:04
  • 1
    Simpler: `const compose = (f, ...fs) => (...xs) => f(...fs.map((f, i) => f(xs[i])));`. – Aadit M Shah Jul 19 '18 at 08:19
  • This is the first time I've felt unqualified to accept any answer. My feeling is that my question was probably off target: I did not fully grasp the precise definition of `compose()` and was more asking generally about composition. That was sloppy. I like the function signature here and its simplicity (and especially @AaditMShah's shorthand), so this is probably the code I will end up using. I also very respectfully note @JLRishe's comment of dissent above. I'm genuinely grateful for the time you all put into this and I honestly want to award the answer to all three of you. – Andrew Jul 19 '18 at 14:58
3

I think part of the trouble you're having is that compose isn't actually the function you're looking for, but rather something else. compose feeds a value through a series of functions, whereas you're looking to pre-process a series of arguments, and then feed those processed arguments into a final function.

Ramda has a utility function that's perfect for this, called converge. What converge does is produce a function that applies a series of functions to a series of arguments on a 1-to-1 correspondence, and then feeds all of those transformed arguments into another function. In your case, using it would look like this:

var saferGetScore = R.converge(getScore, [round, clamp]);

If you don't want to get involved in a whole 3rd party library just to use this converge function, you can easily define your with a single line of code. It looks a lot like what CaptainPerformance is using in their answer, but with one fewer ... (and you definitely shouldn't name it compose, because that's an entirely different concept):

const converge = (f, fs) => (...args) => f(...args.map((a, i) => fs[i](a)));

const saferGetScore = converge(getScore, [round, clamp]);
const score = saferGetScore(2.5, 0.3);
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • Simpler: `const converge = (f, fs) => (...args) => f(...fs.map((f, i) => f(args[i])));`. – Aadit M Shah Jul 19 '18 at 07:46
  • @AaditMShah Thanks, I was shying away from cramming it all into one line, but seeeing it written out like you have it there, it's not so bad as a single line. – JLRishe Jul 19 '18 at 07:52
3

Haskell programmers can often simplify expressions similar to how you'd simplify mathematical expressions. I will show you how to do so in this answer. First, let's look at the building blocks of your expression:

round    :: Number -> Number
clamp    :: Number -> Number
getScore :: Number -> Number -> Number

By composing these three functions we want to create the following function:

getRoundedClampedScore :: Number -> Number -> Number
getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)

We can simplify this expression as follows:

getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)
getRoundedClampedScore iteration        = getScore (round iteration) . clamp
getRoundedClampedScore iteration        = (getScore . round) iteration . clamp
getRoundedClampedScore iteration        = (. clamp) ((getScore . round) iteration)
getRoundedClampedScore                  = (. clamp) . (getScore . round)
getRoundedClampedScore                  = (. clamp) . getScore . round

If you want to convert this directly into JavaScript then you could do so using reverse function composition:

const pipe = f => g => x => g(f(x));

const compose2 = (f, g, h) => pipe(g)(pipe(f)(pipe(h)));

const getRoundedClampedScore = compose2(getScore, round, clamp);

// You'd call it as follows:

getRoundedClampedScore(iteration)(factor);

That being said, the best solution would be to simply define it in pointful form:

const compose2 = (f, g, h) => x => y => f(g(x))(h(y));

const getRoundedClampedScore = compose2(getScore, round, clamp);

Pointfree style is often useful but sometimes pointless.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Thank you for this. I genuinely appreciate the Haskell explanation -- it's not a strong language for me, but as time permits I try to learn. – Andrew Jul 19 '18 at 08:25