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);
@satya164
Copy link

satya164 commented Aug 2, 2018

The bind operator proposal works great for prototype methods, but what about non-prototype methods which the pipeline operator solves? For example, a utility library.

Say I have a utility library which exports various methods. The following will work fine with the bind operator proposal:

import { map, filter } from 'utils'

items::map(x => x + 2)::filter(x => x % 2)

But what about the following?

import * as utils from 'utils'

items::utils.map(x => x + 2)::utils.filter(x => x % 2)

The code stops working depending on the way we import the utilities because this will now refer to module context. Granted this is a nuance of how this works, but I think it's safe to assume that it may not be immediately obvious and potentially confusing. There is no way to prevent such invalid usage either.

Agreed that developers should be familiar with this because it already exists, but it doesn't mean that this is very approachable. I spent a significant time understanding the nuances of this when learning, but I still run into mistakes time to time involving this even if I understand how it works.

I'm just a random developer, I'm not even an "FP folk". I find the pipeline operator easier to use. Sure to use them with existing prototype methods, it's a bit more work when declaring the utility functions (const map = (...args) => arr => Array.prototype.map.apply(arr, args)), but you define them only once and use them several times.

Also regarding complexity part, you mention that the complexity of this already exists in JavaScript, so we should learn it, however regarding pipeline's complexity, a complaint is the usage of closures. Closures also exist in JavaScript, so why prefer the complexity of this over the complexity of closures? One distinction to make is that this shifts the complexity towards the call site, whereas closures shift the complexity to the declaration site.

I've used functions here, because that's what you used in the call example. You could, of course, make addToEach return an arrow instead.

That's unfair. You need to use a non-arrow function with the bind operator, but it's fine to use an arrow function with pipeline operator. Examples should represent how people will use it in the real world, and I'd never use a normal function over an arrow function at least in the anonymous function you have there.

@mAAdhaTTah
Copy link

Also worth mentioning that the current pipeline proposals also include support for handling await within a pipeline whereas the :: operator does not (cannot?).

@spion
Copy link

spion commented Aug 2, 2018

We already have a solution for pipelining for promises. Its called then:

x.then(f).then(g).then(h)

@spion
Copy link

spion commented Aug 2, 2018

For everyone complaining about using this with lodash: if there can be lodash/fp, there can also be lodash/this

@Fishrock123
Copy link

We already have a solution for pipelining for promises. Its called then:

x.then(f).then(g).then(h)

This is how to throw your memory and cpu perf into the dumpster.

Us support companies see this crap in the wild. It's awful, performs awful, debugs awful, and people have a terrible time with it.

@spion
Copy link

spion commented Aug 2, 2018

It performs great with Bluebird and has decently nice long stack traces. That its still awful with native promises means there is more work to be done on that front, in node and V8. The same work would need to be done for the pipeline operator too.

I'm not sure what you're comparing with, but async/await still debugs awful in node. Its been a year, yet still no stack traces after the first await. I wish I didn't have to use Bluebird and chain then calls, but the alternative is still unusable.

@mAAdhaTTah
Copy link

@spion:

We already have a solution for pipelining for promises. Its called then:

Chaining promises & supporting async / await aren't the same thing:

array::map(x => x + 1)
  ::requestAsync()
  .then(handleResponse) // we can't use the bind operator on the response :(
  .then(value => value::filter(x => x > 1)) // or you're basically doing the same inline arrows as |>

vs

array |> map(x => x + 1)
  |> requestAsync // or |> await requestAsync(#) in smart
  |> await
  |> handleResponse // don't need to operate on the promise directly
  |> x => x.filter(x => x > 1)

For everyone complaining about using this with lodash: if there can be lodash/fp, there can also be lodash/this

The point isn't that we couldn't do it; the point is that it doesn't already exist and would need to be built, whereas not only does lodash/fp already exist, but there's a massive ecosystem of libraries that are already functions and already work with |>.

@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