Underscore: Array chain methods error on non-array values

Created on 20 Mar 2016  ·  6Comments  ·  Source: jashkenas/underscore

Array chain methods error on non-array values which is unlike other array category methods.

_().pop(); // error
_('').pop(); // error
_().first() // undefined

Most helpful comment

This brings up another issue, should we cast array-likes to array?

_('hello').first() // 'h'
_('hello').pop() // ?

All 6 comments

This brings up another issue, should we cast array-likes to array?

_('hello').first() // 'h'
_('hello').pop() // ?

This seems to still be an issue - we encountered it after bumping our version from 1.8.3 t0 1.9.0

Edit: It looks like this was fixed in 1.9.1, if so, does this issue need to remain open?

This behaviour is still present in 1.10.2.

A slightly more convincing contrast in my option, since both involve modifying a value in place:

Object.assign(undefined, {a: 1})  // error
_().extend({a: 1})  // undefined

Array.prototype.push.call(undefined, 1)  // error
_().push(1)  // error

I somewhat agree this is inconsistent.

It is worth considering in what kind of real-world situation this would be most likely encountered and what to expect of the wrapped object in such cases.

In the middle of a chain, the wrapped object is predictable. For example, in the chain below, the wrapped object that is passed to push will be either Array or undefined. For this reason, I would argue that the Array wrapper methods should implement a null check. This is easy.

_.chain(something).map(f).push(x)  // hoping to push x to an array

If the predicted wrapped object is more likely to be something other than an array, then it is arguably a programmer error. I don't mind throwing an exception in that case.

_.chain(something).map(f).join('').push(x)  // hoping to push x to a string??

At the start of a chain, the story may seem a bit different at first sight, but I will argue that it is the same and that we should leave it at a null check. At least it must be the case that the programmer has some reason to expect something to be a mutable array-like, since otherwise, there would be no reason to call push:

function giveMeAMutableArraylike(something) {
    _.chain(something).push(x)
}

The expectation that something is a mutable array-like could be unmet for a variety of reasons:

  1. giveMeAMutableArraylike ended up being called with null or undefined for whatever reason. This is comparable to the first chain-in-the-middle example and can be addressed with the same null check.
  2. The caller is trying to make giveMeAMutableArraylike do something that it obviously cannot do, i.e., breaking the contract. This is comparable to the second chain-in-the-middle example. Again, I think it is fine to throw an error in this case.
  3. something is a bare value instead of an array with a single element, i.e., v instead of [v]. This is a common situation in many JavaScript APIs. If the contract of giveMeAMutableArraylike allows this, it is obviously wrong to throw an error regardless of what v happens to be.

In the latter case, Underscore cannot know whether giveMeAMutableArraylike allows single bare elements or not, so it is up to the programmer of giveMeAMutableArraylike to implement its particular contract. There is nothing that a library like Underscore can do that will do the right thing for all possible contracts:

  | Current behaviour | Wrap in single-element array
---|---|---
Contract allows bare single element | _Programmer needs to intervene_ | Underscore does the right thing automatically
Contract does not allow bare single element | Underscore does the right thing automatically | _Programmer needs to intervene_

Also, how do you decide whether a value should be wrapped? Some contracts might want to wrap anything that isn't an Array, while others might want to pass arguments and plain objects as-is. Again, this is something that a library like Underscore cannot guess on behalf of the user.

More exotic situations are conceivable as well, for example a contract that allows immutable array-like objects such as strings and that will convert them to Array first. It is impossible to cover all the possible variations. By the principle of least change, there is just no convincing argument for supporting some contracts at the expense of contracts that were already supported. There is also a strong case for keeping the implementation as minimal as possible.

So I think the right solution is to insert just a null check and leave it at that. This is the only change to Underscore's behaviour that seems defensible in all conceivable situations and it is also consistent with the behaviour of _.extend. A null check could still be regarded a bugfix, while doing anything more than that would quickly turn it into an arbitrary breaking change.

@jashkenas could you shine your light on this? If you agree, I will prepare a pull request that implements the null check.

@jgonggrijp — Can you elaborate in a sentence or two with the behavior that you want to implement with the null check?

Is it that array methods will explicitly throw an exception when called on a nullish value? Or that they will noop when called on a nullish value?

@jashkenas Yes. The behavior I would implement is a no-op, following the style of the Underscore array functions:

https://github.com/jashkenas/underscore/blob/4cf715f593805ba8d7c5685cd06c82b3cd9b55ae/modules/index.js#L495

Except that the length check wouldn't be required, so I would just insert

if (obj == null) return chainresult(this, obj);

between these two lines:

https://github.com/jashkenas/underscore/blob/4cf715f593805ba8d7c5685cd06c82b3cd9b55ae/modules/index.js#L1654-L1655

Plus tests of course, and if those tests reveal a need, maybe also a null check that turns the method into a no-op before this line:

https://github.com/jashkenas/underscore/blob/4cf715f593805ba8d7c5685cd06c82b3cd9b55ae/modules/index.js#L1665

Sounds fine to me!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

haggholm picture haggholm  ·  8Comments

jashkenas picture jashkenas  ·  14Comments

ondrejhlavacek picture ondrejhlavacek  ·  10Comments

LeonFedotov picture LeonFedotov  ·  27Comments

umarfarooq125 picture umarfarooq125  ·  8Comments