DEV Community

Matthew Ricci
Matthew Ricci

Posted on

Higher order functions in React (deep dive)

I really enjoy teaching and explaining things, so I thought I'd give a crack at explaining this idiom in React. Specifically, I am referring to code snippets like the following:

const handleClickTab = (title: string): React.MouseEventHandler<HTMLButtonElement> => (e) => { //Triggered by clicking a tab
e.stopPropagation()
setWhichArenaSelected(title);
};

And then later in the same code:
<ArenaTab title={title} handleClickTab={handleClickTab(title)} selected={title === whichArenaSelected} key={${title}-${index}} />)}

What makes this non-straightforward to the untrained eye is the double =>. Yes that's right, we are making a function object/lambda that returns a function! This is because in Typescript or Javascript, functions are first-class values. This means that they can be used like normal expressions. So if we think about a compiler and its Context Free Grammar, where it defines what an Expression is, functions could basically fall under that category in languages where functions are first-class values.

To wrap our heads around this, I think it is first helpful to look at the differences between lambda functions and functions. Really, there hardly is any. However, when eking out those differences, I think it helps to define functions in terms of their strict, low-level definition. Right down to the ABI.

A function is a sub-procedure of code; a compiler basically records down its name, and what address to pass it to for a jump command. The ABI calling convention specifies which registers arguments go into and how to handle the stack (which args go there, stack alignment, etc.) A function (in its PUREST definition, not counting compiler sleight-of-hand) needs ALL of its arguments to be filled. Even if we have default values and it doesn't look like we're passing in an argument for each parameter, under the hood it's still being fulfilled, just not by you. Correct me if I am wrong in this, but my intuition tells me that when it comes to typescript optional arguments (e.g. func(x?, y?)), those arguments are still fulfilled, it's just that they are given the value of 'undefined' if you don't pass them in, which is still a value!

Lambda functions I think are actually best understood when we stop calling them lambda functions and think of them for what they are: expressions. An expression evaluates until it eventually hits an rvalue; the CFG explains to the compiler how it can break a complex expression down into its core atomic value. Recall that rvalues are those values, that the compiler need not record down in a symbol table. They can basically slot into the 'immediate' slot of immediate instructions in assembly. A compiler needs to record lvalues because they can be named anything, but you can't obfuscate the number 5 with your own name. 5 is 5.

So something like x = y*y + (z + (a/b)) is a complex expression that eventually breaks down to an rvalue, and lambda expressions are no different. The only difference is they just happen to accept arguments. Again, I think it's helpful to get away from functions for a minute. Where do expressions and the variables that contain them go? On the stack. Functions VERY STRICTLY adhere to their local scope and cannot 'see' outside of it. But when we think of lambdas as JUST ANOTHER EXPRESSION that lives on the stack, code like this makes sense:
let x = 5;
let lambda = () => x+x;
let y = lambda();
console.log(y);

'y' will evalute to 10. A function could NEVER do that. A lambda is just a spicy expression that says "hey, I'm an expression that evalutes to an rvalue like anyone else, but since I'm also just a thing on the stack, I can look at things on the stack in my own scope. I'm an expression that can just so happen to run code."

Now what if it takes arguments?
let x = 5;
let lambda = (x: number) => x+x;
let y = lambda(7);
console.log(y);

'y' is 14. Again, lambdas are just spicy expressions. "Hey, I'm an expression that can take from my local stack, or you can just GIVE me a value and I'll evaluate to something." Lambdas in typescript are not curried, so we cannot do partial application, so they will require all their arguments.

Returning back to our original code:

const handleClickTab = (title: string): React.MouseEventHandler<HTMLButtonElement> => (e) => { //Triggered by clicking a tab
e.stopPropagation()
setWhichArenaSelected(title);
};
<ArenaTab title={title} handleClickTab={handleClickTab(title)} selected={title === whichArenaSelected} key={
${title}-${index}} />)}

To explain what's going on here, let's keep it simple:
let lambda = (x: number, z: number) => (arg1: string) => {console.log(${arg1} is your string and ${x+z} is your sum.);};

Lambdas are spicy expressions that can sometimes take arguments, or just take stuff from their scope like any other lvalue. They return a value no matter what, which is what it will finally evaluate to. It should EVENTUALLY evaluate to an rvalue, but in this example, the first pass of the function would not return a full rvalue just yet, because it returns ANOTHER lambda, which is an lvalue!

I brought up partial application because while it is not present here, I would say something similar is going on. In a curried language like Haskell, your ENTIRE program is just one function call with one return statement and one argument. No matter how big it is. This is because take something like this in Haskell:
add(x, y) = x+y

Now if I call:
add(5, 3)

Under the hood, what's REALLY going on is that Haskell basically writes a COMPLETELY NEW function for you under the hood, like this:
add(y) = 5 + y

Sure it has the same name as before, but because it has different parameters, we can say this is an entirely new function. It is the function that adds 5 to its single argument. This 5 is hardcoded, an rvalue on the stack, it never changes. It may as well be called 'add5'. So our call to add(5,3) REALLY means "pass in 3 to the function that adds 5 to a number".

This 'add5' function is called partial application because we only partically supplied arguments. But that's okay! We 'create' a 'new' function that is the function that adds 5 to its SINGLE argument. If I passed in 8 instead of 5, I'd be creating the function that adds 8 to its single argument, hard-coded, every single time.

Of course, that's not ACTUALLY what's going on here, but it's similar as you will see.

So going back:
let lambda = (x: number, z: number) => (arg1: string) => {console.log(${arg1} is your string and ${x+z} is your sum.);};

So what I know is:

  1. I HAVE to supply both arguments because Typescript is not curried (by default, anyways).
  2. When I do pass in those arguments, I will get an lvalue back, not an rvalue, so I can assign that to yet another variable.


let x = 5;
let lambda = (x: number, z: number) => (arg1: string) => {console.log(
${arg1} is your string and ${x+z} is your sum.);};
let lambda2 = lambda(5, 3);

What have we just made? Similar to partial application, we have taken what were previously lvalues and branded them hardcoded right into the stack with rvalues. It doesn't show up, but lambda2 basically looks like this now:
let lambda2 = (arg1: string) => {console.log(${arg1} is your string and ${5+3} is your sum.);};

ONCE MORE, our original code:

const handleClickTab = (title: string): React.MouseEventHandler<HTMLButtonElement> => (e) => { //Triggered by clicking a tab
e.stopPropagation()
setWhichArenaSelected(title);
};

And then later in the same code:
<ArenaTab title={title} handleClickTab={handleClickTab(title)} selected={title === whichArenaSelected} key={${title}-${index}} />)}

I haven't shown, but that ArenaTab code lives in side of a map() call, so remember how partial application literally DEFINES a completely new, unrelated function to your original? Well as many 'title's as I have over the data structure I'm mapping, as many 'new' functions basically get defined FOR me, with 'title' hardcoded completely. These get passed to each ArenaTab. This reduces the amount of props I have to pass down to ArenaTab; all it has to do is call handleClickTab, no arguments required, because 'title' is "burned" right into the stack! Just needs 'e', event, which React does anyways. So in more concrete terms, if my title is, say, 'hello', I have:

const handleClickTab = (e) => { //Triggered by clicking a tab
e.stopPropagation()
setWhichArenaSelected("hello");
};

I hope this is clear! Please correct me if I am wrong about anything and feel free to ask questions.

Top comments (0)