skip to Main Content

The first book I have bought on JavaScript was unfortunately not for beginners.
The book is "the joy of JavaScript" from Luis Atencio. I am still trying to understand some concept in this book 2 years after…

There is a code I still don’t understand today. Can you lehp me to understand how is this possible?

const identity = a => a;
const fortyTwo = () => 42;

const notNull = a => a !== null;
const square  = a => a ** 2;

const safeOperation = (operation, guard, recover) =>
  input => guard(input, operation) || recover();

const onlyIf = validator => (input, operation) =>
  validator(input) ? operation(input) : NaN;

const orElse = identity;

const safeSquare = safeOperation(
  square,
  onlyIf(notNull),
  orElse(fortyTwo)
);

console.log( safeSquare(2) ); // 4
console.log( safeSquare(null) ); // 42

How is this possible to do safeSquare(2) when safeSquare is a function with 3 arguments (square, onlyIf(null), orElse(fortyTwo)). I think the 2 here is the input in safeOperation, but how the 2 is passed in the safeOperation function. I just don’t figure this out.

I know the basic of currying, but here I don’t get it.

2

Answers


  1. A bit of theory

    First, let’s talk about a bit of theory. Namely, what is beta reduction. Wikipedia has a lot of theory but the simple explanation is that you can replace a function with its application. For example if you have

    const fn = x => x + 1;
    
    const result = fn(2);
    

    the last part can be replaced with the function body:

    const fn = x => x + 1;
    //              ^^^^^^ -----+ for f(2)
    //                          | then x = 2
    const result = 2 + 1; // <--+ therefore we can substitute here
    

    The two pieces of code are equivalent. The name of beta reduction is less important than the principle here. The call of the function is replaceable with the body but with the parameter replaced.

    This holds true even when the function returns another function:

    const add = a => b => a + b;
    

    This is a function that takes one parameter, returns a second function which takes another parameter. When the second function is called, then the final result is the first parameter a added to the second parameter b.

    For clarity, that is the same as this longer way of writing the same:

    function add(a) {
      return function(b) {
        return a + b;
      }
    }
    

    However, if we apply beta reduction the same rules apply as before, calling add(2) can be replaced with the body where a = 2:

    const add = a => b => a + b;
    //               ^^^^^^^^^^^ -----+ for add(2)
    //                                | a = 2
    const result1 = b => 2 + b; // <--+ therefore we can substitute
    const result2 = add(2); // same as the above
    
    console.log( result1(3) ); // 2 + 3 = 5
    console.log( result2(3) ); // 2 + 3 = 5

    Now we can look at how this can be used.

    Now some practice

    The principle of beta reduction can now inform how this code works:

    How is this possible to do safeSquare(2) when safeSquare is a function with 3 arguments (square, onlyIf(null), orElse(fortyTwo))

    The safeOperation function is the one that takes three parameters. Therefore, its call can be substituted with the body

    const safeOperation = (operation, guard, recover) =>
      input => guard(input, operation) || recover();
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the body that we can substitute
    

    Making

    const safeSquare = safeOperation(
      square,          // ------------------------------------------------+
      onlyIf(notNull), // ---------------------------------------------+  |
      orElse(fortyTwo) // ------------------------------------------+  |  |
    ); //                                                           |  |  |
    // equivalent to:                                               |  |  |
    input => onlyIf(notNull)(input, square) || orElse(fortyTwo)()// |  |  |
    //       ^^^^^^^^^^^^^^^        ^^^^^^     ^^^^^^^^^^^^^^^^     |  |  |
    //              ^                 ^                ^            |  |  |
    //              |                 |                |            |  |  |
    //              |                 |                +------------+  |  |
    //              |                 +--------------------------------+  |
    //              +-----------------------------------------------------+
    

    We can continue beta reducing the other functions. However, that is left for an exercise to the reader. The main point here is that when safeOperation is called with three parameters it returns a function with only one parameter. That one parameter function is assigned to safeSquare, which is why safeSquare(2) is then a legal call.

    Back to theory and nomenclature

    First-class functions

    The thing that enables this behaviour is called first-class functions (see also on Wikipedia). Simply put "first-class" means this is an type of value the programming language can handle directly – it can be assigned to variables, it can be passed into function calls, etc. Other first class types are strings or numbers, for example. We can do the same things with them:

    const str = "hello";
    const num = 4;
    
    someFunction( "a string" );
    otherFunction( 42 );
    

    When first-class functions are possible, we also get the following:

    Higher order functions

    These are functions that do at least one of: take a function as parameter or return a function as a result. These are the basis of writing reusable code where one part of the operation can change. Good examples where higher order functions are used are Array#filter() and Array#map() (among other array methods). These two go over an array and do something with each member: .filter() will include or exclude it, while .map() will change it. The logic for going over the array is the same for all calls to .filter() or .map() but the way the item is checked is determined by the callback:

    const fruits = [ 1, 2, 3, 4, 5 ];
    
    const onlyOdd = fruits.filter( x => x % 2 === 1 );
    console.log(onlyOdd); // [ 1, 3, 5 ]
    
    const allPlus20 = fruits.map( x => x + 20 );
    console.log(allPlus20); // [ 21, 22, 23, 24, 25 ]
    .as-console-wrapper { max-height: 100% !important; }

    Functional programming relies on higher order functions to build up applications and handle data with.

    Currying

    This is a special kind of a higher order function, not just any. Specifically a function that takes multiple parameters is converted to an equivalent series of functions that require one parameter each:

    const regularAdd = (a, b) => a + b;
    const curriedAdd = a => b => a + b;
    
    const regularPythagoreanCheck = (a, b, c) =>   a**2 + b**2 === c**2;
    const curriedPythagoreanCheck = a => b => c => a**2 + b**2 === c**2;
    
    
    //distance between two 2D points
    const regularDistance = (x1, y1, x2, y2) =>     Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
    const curriedDistance = x1 => y1 => x2 => y2 => Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
    

    And so on. This type of conversion is useful when working mainly in functions. But I will not go too deep in it – it is just worth keeping in mind that not any higher order function is curried. Just if it is a series of one parameter functions.

    Modern curry implementations are less rigorous than this, though. A modern way to curry will accept one or more parameters at each step until all expected parameters are satisfied. For example using curry from Ramda which converts a function to a curried variant:

    const regularDistance = (x1, y1, x2, y2) => Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
    const curriedDistance = R.curry(regularDistance);
    
    console.log(regularDistance(1, 1, 4, 5)); // original
    
    console.log(curriedDistance(1)(1)(4)(5)); // four 1 parameter functions
    console.log(curriedDistance(1, 1)(4, 5)); // two 2 parameter functions
    console.log(curriedDistance(1)(1)(4, 5)); // two 1 parameter functions + one 2 parameter
    console.log(curriedDistance(1, 1, 4, 5)); // 4 parameter function
    //etc
    <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.29.1/ramda.min.js"></script>
    Login or Signup to reply.
  2. abstraction distraction

    VLAZ has done a great job explaining the mechanics of the code and the background theory. I wanted to remark on the ergonomics of the original code and why it’s not a great example for beginners –

    • identity – what is this function? Aside from being renamed to orElse, it’s unused.
    • fortyTwo – some constant value, ok, but why is it inside a thunk?
    • safeOperation – unfamiliar pattern is just if..else in disguise
    • onlyIf – sometimes returns NaN, why?
    • orElse – a copy of identity?

    I would like to demonstrate the same program but with different lines drawn in the sand –

    const always = x =>
      () => x
    
    const eq = x => y =>
      x == y
    
    const square = x =>
      x * x
    
    const when = (predicate, ifTrue, ifFalse) =>
      x => predicate(x) ? ifTrue(x) : ifFalse(x)
    
    const safeSquare = when(
      eq(null),
      always(42),
      square,
    )
    
    console.log(safeSquare(2))     // 4
    console.log(safeSquare(null))  // 42

    It may not be a perfect demonstration that leaves no question in the learner’s mind, but hopefully the knowledge you acquired from VLAZ can help you see these abstractions as more intuitive and generally useful –

    • always makes a function which returns a constant value
    • eq is == as a function, in curried form
    • when is the familiar if..else as a function
    const safeSquare = when(    // if                   if 
      eq(null),                 //   (condition)          equal to null?
      always(42),               //   { true... }          always 42
      square,                   //   else { false... }    (or) square 
    )
    

    identity crisis

    For those wondering about identity, it is the identity element in the function monoid, where compose is the binary operation

    What is a monoid?

    A set S equipped with a binary operation S × S → S, is a monoid if it satisfies the following two axioms:

    associativity
    For all a, b and c in S, the equation (a × b) × c = a × (b × c) holds.

    identity element
    There exists an element e in S such that for every element a in S, the equalities e × a = a and a × e = a hold.

    The function monoid –

    // identity element
    const identity = a =>
      a
    
    // binary operation
    const compose = (f, g) =>
      x => f(g(x))
    

    Upholds the monoid laws –

    // associativity
    compose(compose(f, g), h) == compose(f, compose(g, h))
    
    // identity
    compose(f, identity) == compose(identity, f)
    

    Maybe the text already covered all of this before presenting you with the code in your post. If not, we can see why including identity introduces more questions than it answers.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search