A Lazy Sequence

Duck-wrapping

Duck-wrapping (verb): If it doesn't quack like a duck, wrap it in a duck.

JavaScript has an problematic idiom that I have come to consider a code-smell: functions that wrap some values (based on their type) in other types. I consider this to be an issue because it impairs the ease at which you can reason about the code in question, and it can complicate some logic with extra boilerplate.

Here are some examples:

[1, 2].concat(3); //=> [1, 2, 3]
[1, 2].concat([3]); //=> [1, 2, 3]

$.map([1,2,3], function (_, a) { return a; }); //=> [0, 1, 2]
$.map([1,2,3], function (_, a) { return [a]; }); //=> [0, 1, 2]

Here, concat is duck-wrapping its argumenti with an Array if it is not already an Array instance.

$.map is a little more insidious: it is not actually the map function as functional programmers would recognise it. It is instead a duck-wrapping variant of mapcat/flatMap/concatMap!

The downside in both examples is how do you get an Array that you want as a value in the resulting Array: You explicitly wrap it in another Array, or find some other method to do the job. For example:

[1, 2].concat([[3]]); //=> [1,2,[3]]

$.map([1, 2, 3], function (_, a) { return [[a]]; }); //=> [[1], [2], [3]]

This is annoying at the level of Arrays, but gets more difficult with more complex types, and function interactions. The recent brouhaha around the Promise/A+ highlights one such example: It is difficult to return a Promise of a Promise as a value from onFulfilled because then duck-wraps the return value as described in the Promise Resolution Procedure

The solution to all these awkwardnesses is to simply provide two methods for the operations: Array might have concat and append for instance. jQuery could have map and flatMapii. These changes both clearer and actually easier to use than the duck-wrapping variants.

Footnotes

  1. The examples of concat above only work with one argument for simplicity, but it is actually takes arguments of things to concat to the object in order: Each argument is duck-wrapped as needed. E.g.: [1, 2].concat(3, [4]); //=> [1, 2, 3, 4].
  2. It just so happens that fantasy-land has formalised interfaces for both the concat case and for map and flatMap (applicable to both Array and Promise types).

14 April 2013

A clarification

Duck-wrapping is not exactly function or method overloading. Consider the type T<Any> ∪ Any. Any is any type in the program and T is some wrapper type (here parameterised over Any) and together they form a type union. The problem arises because T<Any> is part of the set of types Any. Therefore T<T<Any>> is also valid for either side of the union.

Contrast this with T<Number> ∪ Number. There is no ambiguity in this type. Likewise String ∪ Element is not ambiguous (and doesn't wrap either). These could both be implemented with method overloading but would not be duck-wrapping.