This lesson is for PRO members.

Unlock this lesson NOW!
Already subscribed? sign in

Create a Pipe Function to Enable Function Composition

7:13 React lesson by

In this lesson, we’ll refactor a series of function calls into a functional pipeline, making the code more declarative and removing the need for intermediate variables. Rather than reaching for a utility library, we’ll start by building our own pipe function to gain a clear understanding of how left-to-right function composition is accomplished in JavaScript.

Get the Code Now
click to level up

egghead.io comment guidelines

Avatar
egghead.io

In this lesson, we’ll refactor a series of function calls into a functional pipeline, making the code more declarative and removing the need for intermediate variables. Rather than reaching for a utility library, we’ll start by building our own pipe function to gain a clear understanding of how left-to-right function composition is accomplished in JavaScript.

Avatar
gitnicos

Hi Andrew, Though piping is intuitive, delayed function execution (partial) is a bit complicating the overall picture. Looks like the whole reason we are using partial function here is due to "limitation" of passing one argument (or result of pure function) in the pipe. Hence in refactoring the code for updateTodo we declare (wrap) it as a partial function and call it twice with submission of the second argument. Now, findById would beg to be declared (wrapped) as a partial function too, since it too requires two arguments. Which is not a case here. Am I missing something?

In reply to egghead.io
Avatar
Andrew Van Slaars

The first function in a pipeline can have any arity, the limitation is for the subsequent functions, because they will receive a return value from the previous function and a function can only ever return a single value. This is where the use of rest ... and spread ... in the definition of pipe come in. The end result of pipe is a function that will take whatever arguments are passed to it, call the first function with all of those arguments and than pass the result into the next function in the pipeline.

I hope this clears things up.

In reply to gitnicos
Avatar
gitnicos

Got it. Thanks, Andrew!

In reply to Andrew Van Slaars
Avatar
Manuel Penaloza

poah this one is hard to follow :), especially that (...args) fn in the middle destroys all my theories about how that could work. is there a way to visualize this reduce iteration so see each iterator outcomes? that'd be awesome!

Avatar
Andrew Van Slaars

I have a written version of this that might help clear it up:

https://vanslaars.io/post/create-pipe-function/

Hope this helps!

In reply to Manuel Penaloza
Avatar
Manuel Penaloza

this totally helped out, awesome blog post including exactly the information to go the very last mile understanding this :). thx!

In reply to Andrew Van Slaars
Avatar
Andrew Van Slaars

Awesome! Glad it helped :)

In reply to Manuel Penaloza
Avatar
Patrick

Andrew,

Hi this is Patrick again. Quick question about the pipe function (and its mostly academic, not really about the functionality). Anyway, when pipe is called by getUpdatedTodos, something curious happens. If you console.log the functions coming in via the ...fns operator, you will see, as you would expect, an array of 3 functions --> findById, toggleTodo and the bound updateTodo. No problem. But whenever I ask to see the function's name property, I get an error for bound updateTodo. That is, if I do a try/catch block like so:

try {
console.log(...fns.name)
} catch(e) {
console.log(e.message);
}

I get findById, toggleTodo and an error (can't convert undefined or null to object).

My first thought was this was because of the bound nature of the updateTodo. But it does seem to have a name just like the other two. Granted, it has 'bound' in front of it, but there doesn't seem to any reason why I shouldn't still be able to access its name. I can access it from the partial function if I separate the return function into a variable first:

export const partial = function(fn, ...args) {
console.info("Remember...fn.bind() is not calling anything - it is merely binding. The fact that it has arguments is just binding the arguments...");
let fnBind = fn.bind(null, ...args);
console.log(fnBind.name+" --> fnBind.name"); // bound updateTodo
return fnBind
};

Any idea why this is the case? Again, purely academic, but I am curious...thanks again!

p.s. one thought is that it has something to do with babel...if you console.log out the functions themselves, you will notice they are written like this: function findById(id, list), function toggleTodo(todo), and
function (). The last function is of course the bound one. Might it be that rather than look in the function object itself, they are just parsing the text - if any - in front of the word function? In other words, if it is blank, it is presumed to not have a name (even though we know it is -- just bound). Anyway...

Avatar
Andrew Van Slaars

Patrick,

That is an interesting question. I'm not sure why you're seeing that behavior, but I'm also not entirely sure where you are trying to access the function name in the code. If you want to put a complete sample in codepen or plunker that recreates the issue I'd be happy to take a look.

In reply to Patrick

Looking at this handleToggle method we can see that findById is called and result of that is passed into toggleTodo, and then the result of that is passed in as an argument to updateTodo. The constants todo and toggled are basically only there, so we can pass them along to the next line and then they're never used again.

App.js

handleToggle = (id) => {
    const todo = findById(id, this.state.todos)
    const toggled = toggleTodo(todo)
    const updatedTodos = updateTodo(this.state.todos, toggled)
    this.setState({todos: updatedTodos})
}

It would be nice if we could get rid of these intermediate values, since they're only useful until the next line of code runs. We could do this by nesting our calls. What I mean by that is I could take this call to findById with my id and the todos list, cut that and paste it in place of its intermediate value in toggleTodo.

handleToggle = (id) => {
    const toggled = toggleTodo(findById(id, this.state.todos))
    const updatedTodos = updateTodo(this.state.todos, toggled)
    this.setState({todos: updatedTodos})
}

Then I could take that entire call to toggleTodo, cut that and I could paste it in place of toggled.

handleToggle = (id) => {
    const updatedTodos = updateTodo(this.state.todos, toggleTodo(findById(id, this.state.todos)))
    this.setState({todos: updatedTodos})
}

This works and it essentially allows us to get rid of these other two values. This is really messy and as soon as you add a fourth function or a fifth function this is going to get really hard to read.

To accommodate this behavior, but make it much more readable we're going to define a Pipe utility. pipe is going to let us take the results of one function and pass them in to the next function.

Then take the results of that function pass them into the next. Just like that messy nested code we just saw, but we'll keep the mess in our utility function keeping our production code much easier to read. I'm going to start by opening the utils.test.js file.

We'll define some test to make sure that our pipe utility is working as expected. I've pasted some code in. I have two new functions that I'm using just for test purposes inc and dbl to just each take a number in and either increment or double that number.

utils.test.js

const inc = (num) => num + 1
const dbl = (num) => num * 2

Then I have two test functions. The first one make sure that I can pass the results of inc to dbl. Then the other one does the inverse just to make sure that our pipe is going in the order we expect. We're going to define pipe in our utils.js file.

test('pipe passes the results of inc to dbl', () => {
  const pipeline = pipe(inc, dbl) // => dbl(inc(2)) OR g(f(...args))
  const result = pipeline(2)
  expect(result).toBe(6)
})

test('pipe passes the results of dbl to inc', () => {
  const pipeline = pipe(dbl, inc) // => inc(dbl(2))
  const result = pipeline(2)
  expect(result).toBe(5)
})

I'm going to update my import to also import pipe.

import {partial, pipe} from './utils'

Then I'm going to come over to the terminal and I'm going to run the test with npm test. Of course these are going to fail because we haven't defined pipe yet. I'm going to open utils.js and I'm going to export const pipe. We'll set that to equal a function and now let's define it.

utils.js

export const pipe = () => {}

Let's take another look at our test to see how we want to define pipe. If we look at our test in both cases we're calling pipe, we're passing it two functions, and we're getting back another function that we're calling later with an argument.

utils.test.js

const pipeline = pipe(inc, dbl)
const result = pipeline(2)`

Let's start defining our pipe function. In pipe the first two arguments are going to be a function f and a function which we'll just call g. That needs to return another function. That second function is going to take our arguments whatever that is.

In this case I'm going to just use the rest operator to take whatever arguments are passed in and wrap them up in an array called args. Now we need to get a result.

utils.js

export const pipe = (f, g) => {...args} => ?

A result is going to take our first function and it's going to call that first function with whatever arguments we have.

I'll use the spread operator to break that back out into a list of args. Then it needs to pass that result into our second function which in this case we're calling g. We'll do that by wrapping the results of f with the arguments in a call to g.

export const pipe = (f, g) => {...args} => g(f(...args))

If I save this our test will rerun and now our tests are passing. I'm just going to paste in a new test, and this test is just going to verify that pipe works with more than two functions. Here I have a pipeline that's going to add a number, increase, double, then increase again.

utils.test.js

test('pipe works with more than 2 functions', () => {
  const pipeline = pipe(add, inc, dbl, inc) // => inc(dbl(inc(add(1,2))))
  const result = pipeline(1,2)
  expect(result).toBe(9)
})

The end result should look something like what would happen if we nested these functions like this inc(dbl(inc(add(1,2)))). We're going to get back a function, call with the arguments one and two, get a result, and we expect that value toBe(9).

Now, if I save this our test will fail, but the interesting thing to note here is that we expected 9 and the result was 4. We did get a result back. The problem is that pipe took the first two functions and ignored the second two, so let's fix that. The existing pipe function isn't exactly what we need, but it's useful. We're going to keep it.

What I'm going to do is I'm going to take the export off of this and I'm just going to rename this to be _pipe so that we can use it inside of utils.js but we're not going to export it. Then we're going to replace that with a new pipe function, so export const pipe, and then we'll define that.

utils.js

const _pipe = (f, g) => {...args} => g(f(...args))

export const pipe =

pipe is going to take in a list of functions and instead of two specific functions now it's going to be a variable length. I'm going to use the rest operator to take in functions and wrap them up in an array we'll call fns.

export const pipe = (...fns)

Now, we have an array of functions and we need to return a single function. The way you take an array of anything and return a singular item is using .reduce. I'm going to call fns.reduce and then I'm going to pass in our internal _pipe function.

export const pipe = (...fns) => fns.reduce(_pipe)

By calling .reduce without an initial value, it's going to take the first two items in our functions array, pass them to _pipe and pipe those together and return a function. Then it can take the next one along with the returned function from the first call _pipe, pass those into pipe and put them together.

It will go on and on until it runs out of functions returning a single function at the end that's just waiting to receive arguments at which point it will execute and pipe the values all the way through those functions.

With that defined I'm going to save my file and we can confirm that the test passed. I'll open up the browser and jump over to App.js. At the top of the file I'm going to import {pipe} as well as partial from './lib/utils'.

App.js

import {pipe, partial} from './lib/utils'

Now I'm going to update the handleToggle method to use our pipe and partial functions.

I'm going to start by defining getUpdatedTodos which is going to be a function that we're going to get back from a call to pipe. We basically want to use pipe to recreate this structure here.

const todo = findById(id, this.state.todos)
const toggled = toggleTodo(todo)
const updatedTodos = updateTodo(this.state.todos, toggled)

We're going to start by piping findById. We're going to take the results of that pipe that into toggleTodo.

Then the results of that can get piped into updateTodo. There's one small problem here updateTodo actually takes two arguments. The value coming out of toggleTodo is the second argument. What we need to do to make this work is partially apply updateTodo making sure that it already has its first argument which in this case is this.state.todos.

handleToggle = (id) => {
    const getUpdatedTodos = pipe(findById, toggleTodo, partial(updateTodo, this.state.todos))
    const todo = findById(id, this.state.todos)
    const toggled = toggleTodo(todo)
    const updatedTodos = updateTodo(this.state.todos, toggled)
    this.setState({todos: updatedTodos})
}

With that in place we can get rid of these two lines. We can update this line to just use getUpdatedTodos and it will accept the id and this.state.todos.

handleToggle = (id) => {
    const getUpdatedTodos = pipe(findById, toggleTodo, partial(updateTodo, this.state.todos))
    const updatedTodos = getUpdatedTodos(id, this.state.todo)
    this.setState({todos: updatedTodos})
}

Our browser will reload and we can just verify that everything still works as expected allowing us to toggle our todo.

HEY, QUICK QUESTION!
Joel's Head
Why are we asking?