####Why is .constructor not good enough? What does doing .constructor[@@species] add?
@@species is an extension point that allows abstract algorithms that may be inherited by (or applied to) various subclasses with differing characteristics to decouple the class of the objects generated by an application of the method from the actual class of the objects the method was applied to. This is a realivately rare situation, but one that does infact occur when developing relatively complex class hierarchies such as a collections hierarchy.
The first known usage of the species pattern that I’m aware of (and the origin of the name species) was in the Smalltalk-80 collection hierarchy. For example, the Smalltalk-80 collection hierarchy includes a class called SortedCollection. SortedCollection is a subclass of OrderedCollection (essentially a double ended queue, similar to a JS Array) except that whenever a new element is added to the collection, the value ordering of the collection is resorted. The familiar operations over collection elements (map
, filter
, concat
) all make sense for SortedCollecitons but in most cases it wouldn’t make sense to use a SortedCollection as the class of the resulting collection (think about map, for instance, or what you want when you concatenate a SortedCollectuon and an Array). So, in Smalltalk-80 the species of SortedCollection is set to OrderedCollection, over-riding the default which would be SortedCollection.
####Is the intent that @@species is only used for instance methods, and not static ones? Why? What is different between those two cases that makes it applicable there?
@@species is primarily intended for use with instance methods. Whether it is also applicable to static methods depends upon the nature of the static method. For the primary instance side use case (or example the Array map method)we have a generic algorithm that needs a value (the “constructor” to be used to create the result collection) which may differ depending upon the kind of object the algorithm is applied to and for which there is no API level way for calling code to supply a value. Delegating the choice to the this
object is a common OO pattern for dealing with such situations.
If map
was rewritten as a static method, it would not have the problem because the this
value passed to the static method would identify the constructor that should be used to create the result. We see something like this with the ES2015 from
and of
static methods of collection constructors. We also see this distinction between Promise then
where we need to infer what type of derived promise to create and the static resolve
method where the API explicitly passes the desired result constructor as the this
value.
The instance-side uses of @@species is just trying to address the most common ways this sort of extensibility use case occurs. There is nothing that says that all generic instance methods in all hierarchies of this sort will only need one such extension point. Nor is it necessarily the case that this type of extension point is never useful for static methods. There is certainly nothing to stop someone from designing a class with lots of distinct @@species style extension points and different ones for instance and static methods. But usually, that degree of extensibility is over engineering.
However, Promise.all
(and race
) are pretty good examples of where we have multiple things going on that may require different extension strategies within a single generic method. Promise.all
really has two object instantiation sites within its algorithms. One such site, creates the result promise that is returned as the value of the all
method. The this
value is probably the right thing to use as the constructor of that value, as that would be the obvious user intent if they invoked MyPromise.all(…)
instead of invoking AnotherPromise.all(…)
or just Promise.all(…)
. So, no need to use a @@species or a similar sort of extension point method for that case.
The other site in the all
algorithm occurs when it invokes resolve
on each argument value. This also creates a “thenable” value, but it isn’t at all obvious to me why the type of each thenable should necessarily be the same as this
value. It seems that we have 4 reasonable alternatives for how to invoke resolve
on an argument value x:
- Promise.resolve(x) //always use the built-in Promise constructor
- this.resolvet(x) //use the constructor that
all
was invoked upon - x.constructor.resolve(x) //use x’s constrictor’s
resolve
- x.constructor.species.result(x) //use the same mechanism as
x.then
To me, #1 is perhaps too constraining on subclasses; for #2 I just don’t see why the resolved argument promises would be expected to be of the same type as the result promise. Both #3 and #4 seem plausible, but given that we have already decided on using the @@species pattern for the then
method, consistency of creating derived promises favors #4 over #3.
(of course, when you look at it this way it raises the issue of why isn’t there just a instance resolve
method that the Promise.all algorithm can invoke? If it existed, it would probably use @@species, just like then
)
####If @@species is primarily intended for use of instance methods, why is it defined as a static method?
It’s just following the pattern as first implemented by Smalltalk-80. There are arguments to be made in favor of either placement. Placement of methods (“assignment of responsibilities”) is a key element of good OO design. One reasonable argument is that something like @@specifies that relates to instantiation policy most reasonably belongs with a constructor object. However, there is also an argument that @@specifies is just a instance method extension hook and as such is should be part of the instance-side subclass extension contract. Given the lack of an obvious preferable design, historic precedent (or homage) seems a fine rationale. (BTW, if you want to dig deeper into OO design thinking, I recommend reading Object Design: Roles, Responsibilities, and Collaborations
I'm not sure if I understand the logic here. The core seems to be sentences like
I don't really see why the decoupling couldn't also be useful there. What if
MyPromise
is just a sort of shell, and the constructor always returnsAnotherPromise
instances? Then, at least, you'd want @@species to returnAnotherPromise
(though instances might not find much use of @@species).I was curious about the motivation for @@species, so I dug up some details about the original Zepto bug that I couldn't find in the notes. I wrote a little summary if anyone's curious.