DEV Community

Charles Loder
Charles Loder

Posted on

Piping in JS, or what Elm taught me about partial application

There has been some recent talk about the pipe operator coming to JS. I'm excited about this proposal but only now that I've struggled a bit learning functional patterns in Elm.

What is a pipe operator?

A pipe operator "pipes" the output of one function into another.

So instead of writing

const result = c(b(a(x)));
Enter fullscreen mode Exit fullscreen mode

Or, as I prefer, writing:

const one = a(x);
const two = b(one);
const result = c(two);
Enter fullscreen mode Exit fullscreen mode

We could write:

const result = a(x) |> b |> c;
Enter fullscreen mode Exit fullscreen mode

JavaScript has something similar with chaining methods like .map(), .filter(), and .reduce().

For that reason, I'll be using .map() as a stand in for exploring piping in JS and what I learned from Elm.

Mapping in JS and Elm

Let's start with a basic .map() example:

const square = (n) => n ** 2;
console.log([1, 2, 3].map(square));
// [1, 4, 9]
Enter fullscreen mode Exit fullscreen mode

What this does is apply the square(n) function to every item in the array, and returns a new array with those squared values.

This is similar to the way things are done in Elm:

List.map square [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

There is another way to write our code above in JS using an anonymous arrow function:

console.log([1, 2, 3].map(n => square(n)));
Enter fullscreen mode Exit fullscreen mode

At first, these two may seem similar, but they're slightly different.

The .map() syntax is like this:

Array.map(<function>)
Enter fullscreen mode Exit fullscreen mode

In the first way, we're saying apply the square(n) function to every item in the array.

The second way, we're saying apply this anonymous <function> which returns the result of the square(n) function to every item in the array.

The first syntax is common in functional languages; the second is not. We'll explore why in the next section.

Partial application

Before getting right into partial application, let's create another function, this time for multiplying:

const multiply = (a, b) => a * b;
Enter fullscreen mode Exit fullscreen mode

Unlike out square(n) function, this function takes two parameters.

Let's try to multiply our array by 10. Using the first syntax, it would look like this:

console.log([1, 2, 3].map(multiply(10)));
// TypeError: NaN is not a function
Enter fullscreen mode Exit fullscreen mode

That's frustrating! Because multiply() takes two arguments, we can't use that first syntax.

We can. however, use the second style syntax:

console.log([1, 2, 3].map(n => multiply(10, n)));
// [ 10, 20, 30 ]
Enter fullscreen mode Exit fullscreen mode

And, we can even combine these two arithmetic functions together using both syntaxes:

console.log([1, 2, 3].map(square).map(n => multiply(10, n)));
// [ 10, 40, 90 ]
Enter fullscreen mode Exit fullscreen mode

But if we wanted/needed to use that first syntax (like in Elm). Then we have to use Partial Application.

Let's refactor our multiply() function to employ partial application:

const multiplyPartial = (a) => (b) => a * b;
Enter fullscreen mode Exit fullscreen mode

If you're a simple JavaScript developer like myself, that probably hurt your brain and caused you to shudder a little.

Instead of two parameters, multiplyPartial is like two functions. The first function returns another function which returns the product of the two inputs.

With partial application, you can write a function like this

const multiplyPartial10 = multiplyPartial(10);
Enter fullscreen mode Exit fullscreen mode

The multiplyPartial10 function can now take the b argument, which returns the product of the two:

multiplyPartial10(4)
// 40
Enter fullscreen mode Exit fullscreen mode

Returning to that error we got, using partial application we can do:

console.log([1, 2, 3].map(multiplyPartial(10)));
// [10, 20, 30]

// or even
console.log([1, 2, 3].map(multiplyPartial10));
// [10, 20, 30]
Enter fullscreen mode Exit fullscreen mode

Again, the function multiplyPartial(10) returns a function, and that function is applied to each element of the array.

Mixing Types

In JavaScript, a function where the parameters are two different types is perfectly ok:

const mixedTypesOne = (a, b) => a.toUpperCase() + " " + (b * 10);
const mixedTypesTwo = (a, b) => b.toUpperCase() + " " + (a * 10);
Enter fullscreen mode Exit fullscreen mode

Both of them give you:

console.log([1, 2, 3].map(n => mixedTypesOne("This number multiplied by 10 is", n)));
console.log([1, 2, 3].map(n => mixedTypesTwo(n, "This number multiplied by 10 is")));
// [
//     'THIS NUMBER MULTIPLIED BY 10 IS 10',
//     'THIS NUMBER MULTIPLIED BY 10 IS 20',
//     'THIS NUMBER MULTIPLIED BY 10 IS 30'
// ]
Enter fullscreen mode Exit fullscreen mode

Regardless of which type comes first in the mixedTypes function, using the arrow syntax in map() we can pass in the correct argument.

Now let's refactor them using partial application:

const mixedTypesPartialOne = (a) => (b) => a.toUpperCase() + " " + (b * 10);
const mixedTypesPartialTwo = (a) => (b) => b.toUpperCase() + " " + (a * 10);
Enter fullscreen mode Exit fullscreen mode

And running the first gives:

console.log([1, 2, 3].map(mixedTypesPartialOne("This number multiplied by 10 is")));
// [
//     'THIS NUMBER MULTIPLIED BY 10 IS 10',
//     'THIS NUMBER MULTIPLIED BY 10 IS 20',
//     'THIS NUMBER MULTIPLIED BY 10 IS 30'
// ]
Enter fullscreen mode Exit fullscreen mode

But the second:

console.log([1, 2, 3].map(mixedTypesPartialTwo("This number multiplied by 10 is")));
// TypeError: b.toUpperCase is not a function
Enter fullscreen mode Exit fullscreen mode

In mixedTypesPartialTwo, the the argument passed in as b is a number, not a string.

So what?

As the above example demonstrated, piping and partial application don't always play well with some common JavaScript practices — namely, functions with two parameters.

In Elm, functions only take one argument,1 and partial application does the rest.

I'm excited for the pipe operator, but it does mean having to think a little differently about how to write code. I struggled with this concept a bit, so hopefully this can help others.


  1. So conceptually, every function accepts one argument. 

Oldest comments (0)