0

I'm trying to write a function that compares two items using another function, then checks if the result is greater than some other value also provided to the function. I can write it like this:

const compareDifference = function(t1, t2, threshold) {
    return getDifference(t1, t2) > threshold;
};

... but this doesn't seem very functional. Every example I find for classical composition assumes that I'll know the value to be compared against before the function is called, in which case I could write it functionally like so:

const greaterThan = (x, y) => x > y;
const greaterThan10 = _.partial(greaterThan, _, 10);
const compareDifference = _.compose(greaterThan10, getDifference);

Since I'm relatively new to functional programming, I feel like I'm missing something easy or fundamental here. Is there a way to write the function so that it incorporates the parameter to be passed to greaterThan without me having to mention it explicitly? Ideally it would be something like:

const compareDifference = _.compose(_.partial(greaterThan, _), getDifference);
Matt Holtzman
  • 633
  • 7
  • 10

2 Answers2

3

I think LUH3417's answer is great for beginners. It touches on some basics but I think there's room for some other info

First, if you wanted the exact same API in your original question, you could break it down into parts like this.

const comp = f=> g=> x=> f (g (x))
const comp2 = comp (comp) (comp)
const flip = f=> x=> y=> f (y) (x)
const sub = x=> y=> y - x
const abs = x=> Math.abs
const diff = comp2 (Math.abs) (sub)
const gt = x=> y=> y > x

// your function ...
// compose greaterThan with difference
// compareDifference :: Number -> Number -> Number -> bool
const compareDifference = comp2 (flip(gt)) (diff)

console.log(compareDifference (3) (1) (10))
// = gt (10) (abs (sub (1) (3)))
// = Math.abs(1 - 3) > 10
// => false

console.log(compareDifference (5) (17) (10))
// = gt (10) (abs (sub (5) (17)))
// = Math.abs(17 - 5) > 10
// => true

But, you are right to have suspicion that your original code doesn't feel that functional. The code I gave you here works, but it still feels... off, right ? I think something that would greatly improve your function is if you made it a higher-order function, that is, a function that accepts a function as an argument (and/or returns a function).


The Yellow Brick Road

We could then make a generic function called testDifference that takes a threshold function t as input and 2 numbers to base the threshold computation on

// testDifference :: (Number -> bool) -> Number -> Number -> bool
const testDifference = t=> comp2 (t) (diff)

Looking at the implementation, it makes sense. To test the difference, we need a test (some function) and we need two numbers that compute a difference.

Here's an example using it

testDifference (gt(10)) (1) (3)
// = gt (10) (abs (sub (1) (3)))
// = Math.abs(1 - 3) > 10
// = Math.abs(-2) > 10
// = 2 > 10
// => false

This is a big improvement because > (or gt) is no longer hard-coded in your function. That makes it a lot more versatile. See, we can just as easily use it with lt

const lt = x=> y=> y < x

testDifference (lt(4)) (6) (5)
// = lt (4) (abs (sub (6) (5)))
// = Math.abs(5 - 6) < 4
// = Math.abs(-1) < 4
// = 1 < 4
// => true

Or let's define a really strict threshold that enforces the numbers have a exact difference of 1

const eq = x=> y=> y === x
const mustBeOne = eq(1)

testDifference (mustBeOne) (6) (5)
// = eq (1) (abs (sub (6) (5)))
// = Math.abs(5 - 6) === 1
// = Math.abs(-1) === 1
// = 1 === 1
// => true

testDifference (mustBeOne) (5) (8)
// = eq (1) (abs (sub (5) (8)))
// = Math.abs(8 - 5) === 1
// = Math.abs(3) === 1
// = 3 === 1
// => false

Because testDifference is curried, you can also use it as a partially applied function too

// return true if two numbers are almost the same
let almostSame = testDifference (lt(0.01))

almostSame (5.04) (5.06)
// = lt (0.01) (abs (sub (5.04) (5.06)))
// = Math.abs(5.06 - 5.04) < 0.01
// = Math.abs(0.02) < 0.01
// = 0.02 < 0.01
// => false

almostSame (3.141) (3.14)
// = lt (0.01) (abs (sub (3.141) (3.14)))
// = Math.abs(3.14 - 3.141) < 0.01
// = Math.abs(-0.001) < 0.01
// = 0.001 < 0.01
// => true

All together now

Here's a code snippet with testDifference implemented that you can run in your browser to see it work

// comp :: (b -> c) -> (a -> b) -> (a -> c)
const comp = f=> g=> x=> f (g (x))

// comp2 :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
const comp2 = comp (comp) (comp)

// sub :: Number -> Number -> Number
const sub = x=> y=> y - x

// abs :: Number -> Number
const abs = x=> Math.abs

// diff :: Number -> Number -> Number
const diff = comp2 (Math.abs) (sub)

// gt :: Number -> Number -> bool
const gt = x=> y=> y > x

// lt :: Number -> Number -> bool
const lt = x=> y=> y < x

// eq :: a -> a -> bool
const eq = x=> y=> y === x

// (Number -> bool) -> Number -> Number -> bool
const testDifference = f=> comp2 (f) (diff)

console.log('testDifference gt', testDifference (gt(10)) (1) (3))
console.log('testDifference lt', testDifference (lt(4)) (6) (5))
console.log('testDifference eq', testDifference (eq(1)) (6) (5))

// almostSame :: Number -> Number -> bool
let almostSame = testDifference (lt(0.01))

console.log('almostSame', almostSame (5.04) (5.06))
console.log('almostSame', almostSame (3.141) (3.14))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • +1, though I'd rather name that `threshold` function `testDifference` - the threshold is the first argument. – Bergi Jul 31 '16 at 11:31
  • Hey @naomik, this is - as always - a great answer. On second thought I've withdrawn mine, since it leaves _"room for some other info"_, as you put very lenient. –  Aug 01 '16 at 11:27
  • 1
    @Bergi thanks for the suggestion, I like it. I updated the answer. – Mulan Aug 01 '16 at 22:05
  • @LUH3417 I never intended for you to withdraw you answer. My answer was intended to be supplemental to yours ! I think yours did a great job of explaining some of the basic concepts and provided code examples with a comfortable and familiar syntax. – Mulan Aug 01 '16 at 22:06
  • @naomik Hey, your intention was absolutely fine! I screwed up the parameter order and didn't separate the sub function and when I fixed it, it was quite similar to your response. It's OK so. –  Aug 02 '16 at 05:41
  • Great info and very thorough -- using a higher order function is indeed the missing idea I think I was looking for. – Matt Holtzman Aug 04 '16 at 12:23
0

If I'm barking up the wrong tree with this, then tell me and I'll edit, but if I wanted to do something like this that was 'more functional' I would do the following:

let greaterThan = _.curry((x, y) => y > x); // notice the args are flipped
let difference = _.curry((x, y) => Math.abs(x - y));
let greaterThan5 = greaterThan(5); // this naming is why we ordered the args backwards
let differenceBetweenGreaterThan5 = _.compose(greaterThan5, difference);
differenceBetweenGreaterThan5(10, 34); // true
differenceBetweenGreaterThan5(10, 6); // false

We could then rewrite your original function like so:

let compareDiff = (threshold, x, y) => {
  return _.compose(greaterThan(threshold), difference)(x)(y);
};

Although I'd probably just use something like differenceBetweenGreaterThan5

Also I apologize for the comically long variable names, but I wanted it to be quite clear what I was naming. There's a few other things to note: I reordered the arguments to greaterThan to make the naming of the partial application more sensible and avoid the need for the _ placeholder. Although I curried difference and consider it good for something that generic, its not strictly necessary for this example.

As for what I think you were missing, the functional approach in this case (per my understanding of what 'functional approach' means) is that we break the complicated case of getting the difference between two numbers and seeing if a third falls in that range, broken it down into its atomic constituents, and structured it as a composition of those atomic elements like greaterThan and difference.

Its the breaking and rebuilding that's difficult: doing so cleanly requires reordering the arguments for flexibility, convenience, and clarity (even relative to the English version I spelt out in the paragraph above as I give the 'third' number first). The argument and piece reordering part seems to me to be what you were missing.

Jared Smith
  • 19,721
  • 5
  • 45
  • 83