DEV Community

Cover image for JavaScript Abuse: Pipelining Functions
Jon Randy 🎖️
Jon Randy 🎖️

Posted on

JavaScript Abuse: Pipelining Functions

DISCLAIMER: Do NOT use this is production code. It's just a fun idea (full of potential danger) that could lead to something more useful...

Okay, so we're going to make the following JS code work, even though it doesn't look like it should:

const addOne = x => x + 1
const half = x => x / 2
const result = addOne[half][Math.floor](4)
console.log(result) // 2
Enter fullscreen mode Exit fullscreen mode

How? Through the magic of JS features that you:

  1. Have always been told (weirdly) to avoid: type coercion
  2. Know about, but can't think of a use for: Symbols

Regular viewers who've seen previous episodes such as 'Metho', 'Turboprop', and 'Ranger' will probably have a fairly good idea of how we're going to achieve this...

The Goal

For the pipelining to work, essentially all we need is to make the following true:

f[g] = x => g(f(x))

  • For ANY two functions f and g:
    • The function f must have a method named with g
    • That method should return another function that is g composed with f

If we can achieve this, we can essentially make a pipeline of as many functions as we like.

The Solution

Below is one way to achieve what we want. Please ask in the comments if there's anything you don't understand:

Function.prototype[Symbol.toPrimitive] = function (hint) {
  // retain as much 'normal' coercion behaviour as we can
  if (hint === "number") return NaN
  if (hint === "default") return this.toString()

  // store a reference to the function being coerced - for later use
  const fn = this

  // create a unique symbol to use as the method key
  const token = Symbol()

  // create a unique method on the Function prototype
  Object.defineProperty(Function.prototype, token, {
    configurable: true,
    // use a getter so we have access to the target function (this)
    // when creating the method to return
    get() {
      // remove the method when used - single use only
      delete Function.prototype[token]
      // the method itself - our composed function x => g(f(x))
      return x => fn(this(x))
    },
  })

  // self cleanup for stray coercions that could occur outside
  // of an intended pipeline
  queueMicrotask(() => {
    delete Function.prototype[token]
  })

  // return the symbol we 'named' the method with - so it is
  // immediately invoked
  return token
}
Enter fullscreen mode Exit fullscreen mode

References (MDN):

Why This is Dangerous?

This is obviously dangerous and/or flaky for a number of reasons:

  • We're adding properties to language-level prototypes. Generally a pretty bad idea, but we're adding properties keyed with Symbols so not risking collisions. However, there's still no guarantee this will not cause issues
  • Anything relying on coercion of functions to string primitives could well be trashed as we've modified the standard behaviour
  • It's not optimised - many identical functions will be recreated again and again
  • It will almost certainly have issues pipelining functions that use this - although I'm not even sure what functionality one would expect in that case

How Could it be Made Safer/Better?

The main way it could be made safer would probably be to mark/subclass pipeline-able functions in some way - so that we could leave standard function behaviour intact. The problem with this is that we'd probably lose the nice syntax, or at the very least end up with something less elegant.

A way to improve it might be to give the intermediate functions created meaningful names - might be nice in stack traces.

Why do this?

Why not? 😀



Not by AI

Top comments (0)