Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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
(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
(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

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)
(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';
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
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);
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, #).

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);
elem = match[2]
|> document.getElementById;
elem = document.getElementById(match[2]);

From jquery/src/core/init.js.

elem = match[2]
|> document.getElementById;
// 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);

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;
}
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);
}
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;
}
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;
  };
}

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;
}
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;
}
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment