Skip to content

Instantly share code, notes, and snippets.

@codehag
Forked from tabatkins/real-world-pipeline.md
Last active September 30, 2023 03:40
Show Gist options
  • Save codehag/2702b0513c684fda0054b029ace8eaac to your computer and use it in GitHub Desktop.
Save codehag/2702b0513c684fda0054b029ace8eaac to your computer and use it in GitHub Desktop.

Core Proposal real-world examples

Living Document. J. S. Choi, 2018-12.

WHATWG Fetch Standard

The WHATWG Fetch Standard contains several examples of using the DOM fetch function, resolving its promises into values, then processing the values in various ways. These examples may become more easily readable with smart pipelines.

Pipelines Status quo
'/music/pk/altes-kamuffel'
|> await fetch(#)
|> await #.blob()
|> playBlob;
fetch('/music/pk/altes-kamuffel')
  .then(res => res.blob())
  .then(playBlob);
fetch('/music/pk/altes-kamuffel')
|> await
|> x=>x.blob()
|> await
|> playBlob
Cleanup Using await:
let response = await fetch('/music/pk/altes-kamuffel');
const blob = await response.blob();
playBlob(blob);

Yulia Note: I don't think any of these are necessarily better than the rhs solution.

(same as above)
playBlob(
  await (
    await fetch('/music/pk/altes-kamuffel')
  ).blob()
);
'https://example.com/'
|> await fetch(#, { method: 'HEAD' })
|> #.headers.get('content-type')
|> console.log;
fetch('https://example.com/',
  { method: 'HEAD' }
).then(response =>
  console.log(
    response.headers.get('content-type'))
);
'https://example.com'
|> x=>fetch(x, {method: 'HEAD' })
|> await
|> x=>x.headers.get('content-type')
|> console.log

Cleanup:

let response = await fetch('https://example.com', {method: 'HEAD'});
let headers = response.headers.get('content-type');
console.log(headers)

Yulia Note: This is the best solution in my opinion.

(same as above)
console.log(
  (await
    fetch('https://example.com/',
      { method: 'HEAD' }
    )
  ).headers.get('content-type')
);
(same as above)
{
  const url = 'https://example.com/';
  const response =
    await fetch(url, { method: 'HEAD' });
  const contentType =
    response.headers.get('content-type');
  console.log(contentType);
}
'https://pk.example/berlin-calling'
|> await fetch(#, { mode: 'cors' })
|> do {
  if (#.headers.get('content-type')
    && #.headers.get('content-type')
      .toLowerCase()
      .indexOf('application/json') >= 0
   )
     return #;
   else
     throw new TypeError();
}
|> await #.json()
|> processJSON;

This example uses do expressions, which come from another ES proposal, and which work well with smart pipelines--in this case to embed ifelse statements.

fetch('https://pk.example/berlin-calling',
  { mode: 'cors' }
).then(response => {
  if (response.headers.get('content-type')
    && response.headers.get('content-type')
      .toLowerCase()
      .indexOf('application/json') >= 0
  )
    return response.json();
  else
    throw new TypeError();
}).then(processJSON);
'https://pk.example/berlin-calling'
|> x=>fetch(x, {mode: 'cors'})
|> await
|> response=>{
  if (response.headers.get('content-type')
    && response.headers.get('content-type')
      .toLowerCase()
      .indexOf('application/json') >= 0
  )
    return response;
  else
    throw new TypeError();
  }
|> x=>x.json()
|> await
|> processJSON
Cleanup:
let response = await fetch('https://pk.example/berlin-calling', {mode: 'cors'});
if (!(response
     .headers
     .get('content-type')
     ?.toLowerCase()
     .indexOf('application/json') >= 0)
) { 
  throw new TypeError();
}

const responseJson = await response.json();
const processedJson = processJson(responseJson);

Yulia note: This is the best solution to this one in my opinion. I would not use pipeline here, except at the very end to pass a single value. The error handling is easier to follow using existingn syntax, and the remaining pieces would use minimal pipelining.

jQuery

As the single most-used JavaScript library in the world, jQuery has provided an alternative human-ergonomic API to the DOM since 2006. jQuery is under the stewardship of the JS Foundation, a member organization of TC39 through which jQuery’s developers are represented in TC39. jQuery’s API requires complex data processing that becomes more readable with smart pipelines.

Pipelines Status quo
return data
|> buildFragment([#], context, scripts)
|> #.childNodes
|> jQuery.merge([], #);

The path that a reader’s eyes must trace while reading this pipeline moves straight down, with some movement toward the right then back: from data to buildFragment (and its arguments), then .childNodes, then jQuery.merge. Here, no one-off-variable assignment is necessary.

parsed = buildFragment(
  [ data ], context, scripts
);
return jQuery.merge(
  [], parsed.childNodes
);

From jquery/src/core/parseHTML.js. In this code, the eyes first must look for data – then upwards to parsed = buildFragment (and then back for buildFragment’s other arguments) – then down, searching for the location of the parsed variable in the next statement – then right when noticing its .childNodes postfix – then back upward to return jQuery.merge.

return data
|> x=>buildFragment([x], context, scripts)
|> x=>x.childNodes
|> x=>jQuery.merge([], x)
Cleanup:
const childNodes = buildFragment([data], context, scripts).childNodes;
return jQuery.merge([], childNodes);

Yulia Note: Not better, not worse than the original. I could see Topic as being useful here.

(key |> toType) === 'object';
key |> toType |> # === 'object';

|> has a looser precedence than most operators, including ===. (Only assignment operators, arrow function =>, yield operators, and the comma operator are any looser.)

toType(key) === 'object';

From jquery/src/core/access.js.

(key |> toType) === 'object';
Cleanup: (same as right)

Yulia Note: I honestly find thee original better in this case.

context = context
|> # instanceof jQuery
    ? #[0] : #;
context =
  context instanceof jQuery
    ? context[0] : context;

From jquery/src/core/access.js.

context = context
|> x=> x instanceof jQuery
  ? x[0] : x
Cleanup: Same as right.

Yulia note: I find the status quo easier to read, with fewer memory-drain variables used. I don't see a use case for pipeline here.

context
|> # && #.nodeType
  ? #.ownerDocument || #
  : document
|> jQuery.parseHTML(match[1], #, true)
|> jQuery.merge(this, #);
jQuery.merge(
  this, jQuery.parseHTML(
    match[1],
    context && context.nodeType
      ? context.ownerDocument
        || context
      : document,
    true
  )
);

From jquery/src/core/init.js.

context
|> x=>x && x.nodeType
  ? x.ownerDocument || x
  : document
|> x=>jQuery.parseHTML(match[1], x, true)
|> x=>jQuery.merge(this, x);
Cleanup:
function getDocument(context, document) { 
  if (context?.nodeType) {
    return context.ownerDocument || context
  } 
  return document;
}

// ...
const doc = getDocument(context, document);
const parsedHtml = jQuery.parseHTML(match[1], doc, true);
return jQuery.merge(this, parsedHtml);

Yulia Note: this is hard to judge, I personally prefer the cleaned up version because it makes clear what the first step really does. But this may be a personal preference, as I generally avoid ternaries (find them hard to read).

match
|> context[#]
|> (this[match] |> isFunction)
  ? this[match](#);
  : this.attr(match, #);

Note how, in this version, the parallelism between the two clauses is very clear: they both share the form match |> context[#] |> something(match, #).

Yulia note: this is no clearer for me what is happening here. We have a function invocation, or an attribute. That is actually hidden in this code.

if (isFunction(this[match])) {
  this[match](context[match]);
} else
  this.attr(match, context[match]);
}

From jquery/src/core/init.js. Here, the parallelism between the clauses is somewhat less clear: the common expression context[match] is at the end of both clauses, at a different offset from the margin.

match
|> x=>context[x]
|> x=>(this[match] |> isFunction)
  ? this[match](x);
  : this.attr(match, x);
Cleanup:
const contextMatch = context[match];
const maybeMatchedFunction = this[match];
if (isFunction(maybeMatchedFunction)) {
  maybeMatchedFunction(contextMatch);
} else
  this.attr(match, contextMatch);
}

Yulia note: here, unlike above, it is clear that one of these is a function invocation, and the other one is setting a method. These two branches are too different to be treated in the same way. I would find the pipeline examples to be error prone, because you cannot see how these two things are fundamentally different.

elem = match[2]
|> document.getElementById;
elem = document.getElementById(match[2]);

From jquery/src/core/init.js.

elem = match[2]
|> document.getElementById;

Cleanup version: Don't see an issue with rhs.

// Handle HTML strings
if ()
  
// Handle $(expr, $(...))
else if (!context || context.jquery)
  return context
  |> # || root
  |> #.find(selector);
// Handle $(expr, context)
else
  return context
  |> this.constructor
  |> #.find(selector);

The parallelism between the final two clauses becomes clearer here too. They both are of the form return context |> something |> #.find(selector).

// Handle HTML strings
if ()
  
// Handle $(expr, $(...))
else if (!context || context.jquery)
  return (context || root).find(selector);
// Handle $(expr, context)
else
  return this.constructor(context)
    .find(selector);

From jquery/src/core/init.js. The parallelism is much less clear here.

// Handle HTML strings
if ()
  
// Handle $(expr, $(...))
else if (!context || context.jquery)
  return context
  |> x=>x || root
  |> x=>x.find(selector);
// Handle $(expr, context)
else
  return context
  |> this.constructor
  |> x=>x.find(selector);
Cleanup
// Handle HTML strings
if ()
  
// Handle $(expr, $(...))
if (!context) {
  return root.find(selector);
if (context.jquery) {
  return context.find(selector);
}
// Handle $(expr, context)
return this.constructor(context).find(selector);

Yulia note: It is a lot easier to read this one again... I would flag the code in the above for all cases, in a review , for the mixed boolean of (!context || context.jquery). these two have different actions that need to be taken, but are bundled into one.

Underscore.js

Underscore.js is another utility library very widely used since 2009, providing numerous functions that manipulate arrays, objects, and other functions. It too has a codebase that transforms values through many expressions – a codebase whose readability would therefore benefit from smart pipelines.

Pipelines Status quo
function (obj, pred, context) {
  return obj
  |> isArrayLike
  |> # ? _.findIndex : _.findKey
  |> #(obj, pred, context)
  |> (# !== void 0 && # !== -1)
      ? obj[#] : undefined;
}
function (obj, pred, context) {
  var key;
  if (isArrayLike(obj)) {
    key = _.findIndex(obj, pred, context);
  } else {
    key = _.findKey(obj, pred, context);
  }
  if (key !== void 0 && key !== -1)
    return obj[key];
}
function (obj, pred, context) {
  return obj
  |> isArrayLike
  |> x=>x ? _.findIndex : _.findKey
  |> fn=>fn(obj, pred, context)
  |> x=>(x !== void 0 && x !== -1)
      ? obj[x] : undefined;
}
Cleanup:
function getFindMethod(obj) {
  if (isArrayLike(obj)) {
    return _.findIndex;
  } 
  return _.findKey;
}

function (obj, pred, context) {
  const find = getFindMethod(obj);
  const key = find(obj, pred, context);
  if (key !== void 0 && key !== -1)
    return obj[key];
  }
  return undefined; // added for explicitness, like the pipeline below
}

Yulia Note: Again, this is easy to read. of course, we can apply pipeline here

function (obj, pred, context) {
  getFindMethod(obj)
  |> #(obj, pred, context)
  |> (#!== void 0 && # !== -1) ?
       obj[#] : undefined
}

Yulia Note: This is shorter, but I forget what # means. is it the find method? no its the return of the find method. What is that? I have to go search. If we do this in the F# pipeline:

function (obj, pred, context) {
  getFindMethod(obj)
  |> find => find(obj, pred, context)
  |> key => (key!== void 0 && key !== -1) ?
       obj[key] : undefined
}

Yulia Note: Cleanup is better to understand what is happening. Pipeline, it forces you to use ternary operators, but the F# pipeline does allow you to see what is happening.

function (obj, pred, context) {
  return pred
  |> cb
  |> _.negate
  |> _.filter(obj, #, context);
}
function (obj, pred, context) {
  return _.filter(obj,
    _.negate(cb(pred)),
    context
  );
}
function (obj, pred, context) {
  return pred
  |> cb
  |> _.negate
  |> x=>_.filter(obj, x, context);
}

Cleanup

function (obj, pred, context) {
  const result = cb(pred);
  const negated = _.negate(result);
  return _.filter(obj, negated, context);
}

Yulia note: in this case I think the best one is Topic. But the cleaned up version is pretty easy to work with and read. Close call here.

function (
  srcFn, boundFn, ctxt, callingCtxt, args
) {
  if (!(callingCtxt instanceof boundFn))
    return srcFn.apply(ctxt, args);
  var self = srcFn.prototype |> baseCreate;
  return self
    |> srcFn.apply(#, args)
    |> _.isObject(#) ? # : self;
}
function (
  srcFn, boundFn,
  ctxt, callingCtxt, args
) {
  if (!(callingCtxt instanceof boundFn))
    return srcFn.apply(ctxt, args);
  var self = baseCreate(srcFn.prototype);
  var result = srcFn.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
}
function (
  srcFn, boundFn, ctxt, callingCtxt, args
) {
  if (!(callingCtxt instanceof boundFn))
    return srcFn.apply(ctxt, args);
  var self = srcFn.prototype |> baseCreate;
  return self
    |> x=>srcFn.apply(x, args)
    |> x=>_.isObject(x) ? x : self;
}
Cleanup: same as rhs

Yuia Note: none of these is easier to read.

function (obj) {
  return obj
  |>  # == null
    ? 0
    : (#|> isArrayLike)
    ? #|> #.length
    : #|> _.keys |> #.length;
  };
}

Smart pipelines make parallelism between all three clauses becomes clearer:
0 if it is nullish,
#|> #.length if it is array-like, and
#|> something |> #.length otherwise.
(Technically, #|> #.length could simply be #.length, but it is written in this redundant form in order to emphasis its parallelism with the other branch.)

This particular example becomes even clearer when paired with Additional Feature BP.

function (obj) {
  if (obj == null) return 0;
  return isArrayLike(obj)
    ? obj.length
    : _.keys(obj).length;
}
function (obj) {
  return obj
  |> x=>x == null
    ? 0
    : (x|> isArrayLike)
    ? x|> y=>y.length
    : x|> _.keys |> y=>y.length;
  };
}
Cleanup
function (obj) {
  if (obj == null) {
    return 0;
  }
  if (isArrayLike(obj)) {
    return obj.length;
  }
  return _.keys(obj).length;
}

Yulia Note: rhs is not bad, just the ternary is an issue. The nested ternaries in the pipeline are an issue. It is very hard to read. Incredibly, the pipeline is minimal in its use here, it is how it forces the use of a ternary that is the problem. I prefer the status quo here.

Lodash

Lodash is a fork of Underscore.js that remains under rapid active development. Along with Underscore.js’ other utility functions, Lodash provides many other high-order functions that attempt to make functional programming more ergonomic. Like jQuery, Lodash is under the stewardship of the JS Foundation, a member organization of TC39, through which Lodash’s developers also have TC39 representation. And like jQuery and Underscore.js, Lodash’s API involves complex data processing that becomes more readable with smart pipelines.

Pipelines Status quo
function listCacheHas (key) {
  return this.__data__
  |> assocIndexOf(#, key)
  |> # > -1;
}
function listCacheHas (key) {
  return assocIndexOf(this.__data__, key)
    > -1;
}
function listCacheHas (key) {
  return this.__data__
  |> x=>assocIndexOf(x, key)
  |> x=>x > -1;
}
Cleanup:
function listCacheHas (key) {
  return assocIndexOf(this.__data__, key) > -1;
}

Yulia note: I don't understand why pipeline splits this into two steps. A direct evaluation is better here.

function mapCacheDelete (key) {
  const result = key
  |> getMapData(this, #)
  |> #['delete'](key)
  this.size -= result ? 1 : 0;
  return result;
}
function mapCacheDelete (key) {
  var result =
    getMapData(this, key)['delete'](key);
  this.size -= result ? 1 : 0;
  return result;
}
function mapCacheDelete (key) {
  const result = key
  |> x=>getMapData(this, x)
  |> x=>x['delete'](key);
  this.size -= result ? 1 : 0;
  return result;
}
Cleanup:
function mapCacheDelete (key) {
  var result = getMapData(this, key)['delete'](key);
  if (result) {
    this.size -= 1;
  }
  return result;
}

Yulia Note: The step which does -= 0 is unnecessary as it applies a noop. This is an example of why ternaries are a problem and why I would prefer not to encourage them, it looks like it is doing something useful but it isn't. If you saw this.size -= 0, you would immediately understand it. But, in the case of applying pipeline operator here, you would have to do this. As a result, meaningless operations would be applied only to keep to the code style, not to express the meaning of the program. That is a problem.

function castPath (value, object) {
  if (isArray(value)) {
    return value;
  }
  return isKey(value, object)
    ? [value]
    : value |> toString |> stringToPath;
}
function castPath (value, object) {
  if (isArray(value)) {
    return value;
  }
  return isKey(value, object)
    ? [value]
    : stringToPath(toString(value));
}
function castPath (value, object) {
  if (isArray(value)) {
    return value;
  }
  return isKey(value, object)
    ? [value]
    : value |> toString |> stringToPath;
}
Cleanup:
function castPath (value, object) {
  if (isArray(value)) {
    return value;
  }
  if (isKey(value, object)) {
    return [value];
  }
  const stringifiedValue = toString(value);
  return stringToPath(stringifiedValue);
}

Yulia Note: I see the value of pipeline here. Either pipeline would work.

The very biased YULIA TALLY

type is the better solution would pass review would not pass review
Smart 2 7 10
F# 1 6 11
Cleanup 14 17 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment