Living Document. J. S. Choi, 2018-12.
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.
| With smart pipelines
| Status quo
|
'/music/pk/altes-kamuffel'
|> await fetch(#)
|> await #.blob()
|> playBlob;
|
fetch('/music/pk/altes-kamuffel')
.then(res => res.blob())
.then(playBlob);
|
|
[Equivalent to 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/'
|> await fetch(#, { method: 'HEAD' })
|> #.headers.get('content-type')
|> console.log;
|
console.log(
(await
fetch('https://example.com/',
{ method: 'HEAD' }
)
).headers.get('content-type')
);
|
|
[Equivalent to 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 #.json();
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 if–else 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);
|
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.
| With smart 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.
|
(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.
|
context = context
|> # instanceof jQuery
? #[0] : #;
|
context =
context instanceof jQuery
? context[0] : context;
From jquery/src/core/access.js.
|
context
|> # && #.nodeType
? #.ownerDocument || #
: document
|> jQuery.parseHTML(match[1], #, true)
|> jQuery.merge;
|
jQuery.merge(
this, jQuery.parseHTML(
match[1],
context && context.nodeType
? context.ownerDocument
|| context
: document,
true
)
);
From jquery/src/core/init.js.
|
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.
|
elem = match[2]
|> document.getElementById;
|
elem = document.getElementById(match[2]);
From jquery/src/core/init.js.
|
// Handle HTML strings
if (…)
…
// Handle $(expr, $(...))
else if (!# || #.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.
|
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.
| With smart 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 pred
|> cb
|> _.negate
|> _.filter(obj, #, context);
}
|
function (obj, pred, context) {
return _.filter(obj,
_.negate(cb(pred)),
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 (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;
}
|
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.
| With smart pipelines
| Status quo
|
function hashGet (key) {
return this.__data__
|> nativeCreate
? (#[key] === HASH_UNDEFINED
? undefined : #)
: hashOwnProperty.call(#, key)
? #[key]
: undefined;
}
|
function hashGet (key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED
? undefined : result;
}
return hasOwnProperty.call(data, key)
? data[key] : undefined;
}
|
function listCacheHas (key) {
return this.__data__
|> assocIndexOf(#, key)
|> # > -1;
}
|
function listCacheHas (key) {
return assocIndexOf(this.__data__, key)
> -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 castPath (value, object) {
return value |>
#|> isArray
? #
: (#|> isKey(#, object))
? [#]
: #|> toString |> stringToPath;
}
|
function castPath (value, object) {
if (isArray(value)) {
return value;
}
return isKey(value, object)
? [value]
: stringToPath(toString(value));
}
|
[Unix pipe]: https://en.wikipedia.org/wiki/Pipeline_(Unix
[untangled flow]: ./goals.md#untangled-flow
[Visual Basic’s select statement]: https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/select-case-statement
[WebKit console variables]: https://webkit.org/blog/829/web-inspector-updates/
[WHATWG Fetch + CP]: ./core-real-examples.md#whatwg-fetch-standard
[WHATWG Fetch Standard]: https://fetch.spec.whatwg.org/
[WHATWG Streams + CP + BP + PF + NP]: ./additional-feature-np.md#whatwg-streams-standard-core-proposal--additional-features-bppfmt
[WHATWG Streams + CP + BP + PF]: ./additional-feature-pf.md#whatwg-streams-standard-core-proposal--additional-feature-bppf
[WHATWG Streams Standard]: https://stream.spec.whatwg.org/
[WHATWG-stream piping]: https://streams.spec.whatwg.org/#pipe-chains
[Wikipedia: term rewriting]: https://en.wikipedia.org/wiki/Term_rewriting
[zero runtime cost]: ./goals.md#zero-runtime-cost