Skip to content

Instantly share code, notes, and snippets.

@jakearchibald
Last active August 13, 2023 06:46
Show Gist options
  • Save jakearchibald/4202cae11022defd7c13b37005704e36 to your computer and use it in GitHub Desktop.
Save jakearchibald/4202cae11022defd7c13b37005704e36 to your computer and use it in GitHub Desktop.
Call operator vs pipeline

Using a call operator

function addToEach(add) {
  return this.map(val => val + add);
}

function sum() {
  return this.reduce((a, b) => a + b);
}

const result = [1, 2, 3]::addToEach(10)::sum();

Using pipeline

const addToEach = add => arr => arr.map(val => val + add);
const sum = arr => arr.reduce((a, b) => a + b);

const result = [1, 2, 3] |> addToEach(10) |> sum;

Analysing the complexity of the call operator

// Complexity: what is `this`?
// Answer: Like all other instances of `this` in JS, it's the context object, or the global,
// unless it's been explicitly set by call/apply/bind.
function addToEach(add) {
  return this.map(val => val + add);
}

function sum() {
  return this.reduce((a, b) => a + b);
}

// Complexity: With ::, what is relation between the left and right hand side?
// Answer: It's sugar for func.call, so foo::bar(10) desugars to bar.call(foo, 10).
// This means bar is called with argument 10, and within it `this` is set to foo.
const result = [1, 2, 3]::addToEach(10)::sum();

The complexities here already exist in JS, so you may already know them. If not, learning them will come in handy with other JavaScript patterns.

Analysing the complexity of pipeline

// Complexity: What's with the two instances of =>?
// Answer: It's a function that returns a function. That'll become clear(ish) later.
// Complexity: Why is the subject (arr) after the thing that'll happen to it (add)?
// Answer: It just has to be backwards to work (despite pipeline aiming to solve this
// same problem with function nesting).
const addToEach = add => arr => arr.map(val => val + add);
// Complexity: Why doesn't this have two =>?
// Answer: Because it doesn't have args, it doesn't need a function within a function.
const sum = arr => arr.reduce((a, b) => a + b);

// Complexity: With |>, what is relation between the left and right hand side?
// Answer: The right-hand side is called with the left-hand side as its single argument.
// Complexity: Why is addToEach called as a function, whereas sum is just passed as a value?
// Answer: The left-hand side is called with the right, so addToEach(10) is a function that
// returns a function, whereas sum doesn't return a function so you just use its value.
const result = [1, 2, 3] |> addToEach(10) |> sum;

Although the character count is lower, the complexity is higher & pipeline-specific.

Other benefits of the call operator

You can use other instance methods directly:

const { map, sort } = Array.prototype;

const headings = document.querySelectorAll('a')
  ::map(el => el.textContent)
  ::sort();

This is because instance methods already use this.

Pipeline is really easy to implement yourself

Since it's a functional pattern, you can implement it as a function:

const pipe = (val, ...funcs) => funcs.reduce((val, func) => func(val), val);

const result = pipe([1, 2, 3], addToEach(10), sum);
@spion
Copy link

spion commented Aug 3, 2018

lodash/fp did not exist before, but it does now. So could lodash/this - its not that difficult (in fact it can be automatically generated, too). The ecosystem would quickly adjust - its not a fundamental, difficult incompatibility, just a mechanically different calling convention.

@spion
Copy link

spion commented Aug 4, 2018

I am completely confused about how x |> await |> handleResponse even works. What does x |> await even do? Does it unpack a promise? What is the return type of that expression, standalone? Can it even exist stand-alone, or is it a syntax error not to add |> anotherFunction after it?

edit: oh I get it, its actually a special case of passing an operator instead of a function, but it looks like (p:Promise<T>) => T. Clever.

People did have misgivings about the other clever meaning of ::bindOperator. Wonder how they will feel about the above gymnastics.

@jichang
Copy link

jichang commented Aug 5, 2018

Nowadays, If you write code with ES6 syntax, I bet everyone has write nested arrow functions, so does the complexity of pipeline operator really comes from that ? Also, changing order of parameters feels easier with pipeline operator than call operator

@theJian
Copy link

theJian commented Aug 5, 2018

I do prefer the pipeline operator. It's more readable for me and for people who were familiar with FP. Is it possible that feeling pipeline more complex is because of unfamiliarity?

@mAAdhaTTah
Copy link

mAAdhaTTah commented Aug 6, 2018

@spion You're missing my point; I'm not suggesting it cannot exist. I'm pointing out that because it does not currently exist, and will not broadly exist until bind gets ratified (or advances), pipeline has an advantage coming into an existing ecosystem already compatible with it. At best, there are a handful of libraries of that currently exist that work w/ this (trine being the only one I know of); basically everything that currently exists on npm right now will work w/ pipeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment