Search code examples
javascriptfunctional-programmingfunction-compositionpointfree

Is there a way to write this Javascript function without listing the arguments?


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);

Solution

  • 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))