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)));
Or, as I prefer, writing:
const one = a(x);
const two = b(one);
const result = c(two);
We could write:
const result = a(x) |> b |> c;
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]
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]
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)));
At first, these two may seem similar, but they're slightly different.
The .map()
syntax is like this:
Array.map(<function>)
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;
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
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 ]
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 ]
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;
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);
The multiplyPartial10
function can now take the b
argument, which returns the product of the two:
multiplyPartial10(4)
// 40
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]
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);
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'
// ]
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);
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'
// ]
But the second:
console.log([1, 2, 3].map(mixedTypesPartialTwo("This number multiplied by 10 is")));
// TypeError: b.toUpperCase is not a function
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.
Top comments (0)