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.
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 |
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. |
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 |
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 |
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';
|
toType(key) === 'object'; |
(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; |
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 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 |
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 |
// 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 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: 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 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 | |
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. |
type | is the better solution | would pass review | would not pass review |
Smart | 2 | 7 | 10 |
F# | 1 | 6 | 11 |
Cleanup | 14 | 17 | 0 |