Closed fwienber closed 10 years ago
No.
A Promise is not "just" a thenable. A thenable is a thenable. There can be other kinds of thenables that are not Promises.
The presence of a then()
function is the sole defining characteristic of a thenable.
A Promise is a thenable with additional functionality and behavior, some of which is standardized in the Promises/A+ specification.
If the ActionScript core framework had defined a IThenable interface, and all potential participants implemented that interface, part of what you describe might be reasonable.
The definition of thenable originates from a philosophy of duck typing, not strong typing. If it has a then()
function, it's a thenable.
As for the IPromise suggestion:
There is no guarantee that any other future Promise implementation for ActionScript would implement Promise-AS3's IPromise interface even if it existed. In fact, it would be highly unlikely. So, that would mean writing adapters for any new Promise implementation as they emerge (unlikely as that is, given ActionScript's dim future).
Further, it assumes that all IPromise implementations would be equal, and trustworthy.
One of the major pain points discovered as more and more JavaScript implementations of Promises emerged was the incompatibility of seemingly minor differences in their behavior as it relates to timing (current tick vs next tick execution) and behavior (handling of return values and thrown errors to support recovery and propagation). These subtle differences have a huge impact on the outcome of any non-trivial promise chain.
(Anyone who has developed a Promise library implementation and attempted to pass the current suite of Promise/A+ tests knows just how subtle those differences can be.)
Thankfully, this is part of what the specification solves. The standards codified in the Promises/A+ specification define a method for interoperability that provides safe interaction between different Promise implementations. Promises implementations vary wildly, but they share one thing - the then()
method. So, the specification defines the then()
method as the sole integration point.
This allows a library like Promise-AS3 to adapt and integrate even with badly behaving Promises, so long as they have a then()
function that calls the specified handlers. With this approach, there is also no need to create adapters to make things conform to an arbitrary IPromise interface.
A Promise-AS3 Deferred is never going to return anything other than a trusted instance of a Promise-AS3 Promise class. The resolver logic will always adapt external Promises into trusted Promises. Under no circumstances do I want to have to directly consume some random Promise implementation that claims to be an IPromise, that behaves subtly differently and breaks the behavior deep in a nested Promise chain. Nor, as an end-user developer, would I ever want to attempt to debug such a thing.
My stance is that the suggestions you outline above are hostile to end-user developers of this framework. They require the developer to engage in unnecessary additional ceremony to interact with the library. They eliminate the fluent nature of the API and cofound the convenience of chaining. They make it much more foreign to developers who have learned popular implementations of Promises in JavaScript.
I understand why you made this suggestion and I appreciate you spending the time to write up your case for it. The classical interface and implementation approach is powerful and useful in many contexts. Taken an extreme, it results in APIs that are unnecessarily complex and developer hostile. The core concern of interoperability between different Promise implementations can and has been solved in a different (and safer) manner here.
It seems I didn't get across the main intention of the refactoring I propose.
The intention is not to create an interface any thenable must implement. I understand that promises use duck typing. Other promise implementations will not know of your interface at all, so they cannot implement it. Only your framework should provide an implementation of the interface. Client code should only use the interface (API), not implement it (SPI). Only extensions of the framework (custom adapters) may want to provide alternative implementations, but most likely, they use Deferred
to produce safe promises and there remains only one (internal) implementation. In any case, it is still an advantage to use an interface (see below).
The interface is intended to mean "a safe promise generated by this library". An interface specifies more than method signatures, it also specifies a contract (in the ASDoc), and your contract would be "Promises/A+ spec".
The main point is that client code must not instantiate the class Promise
. In ActionScript, the only way to keep a client from instantiating a public type is to make the type an interface. This does not make the API any more complicated, because in exchange, the implementing class would not be part of the API (but an internal class
).
I also still think it is a good idea to put all the static utility methods into a separate class (you cannot define them in an interface, anyway). You can continue making up many new utility methods (just look at Q.js!), but the core promise interface is not likely to be extended at the same speed, if at all. Thus, you separate the (stable) core API from the (growing) utility API.
If you like chaining and really think the additional methods will be used very often, my second proposal was to keep the convenience methods in the promise interface. If you think there is no need for an AbstractPromise
as a base class for different internal implementations (because there is only one?), just leave it out.
Thus, an alternative refactoring would be to make Promise
an interface, move its implementation to an internal
class PromiseImpl
, and move its static methods to a new class Promises
or even directly inside the package (like you define spread
). Using this approach, the name of the type Promise
stays the same, but the downside is this would be a breaking API change, because the static methods cannot be defined in the interface to forward to the new utility functions. This is why I originally proposed the new name IPromise
for the interface, so that Promise
can remain backwards-compatible ([Deprecated]
) and become an internal class later.
Does that make more sense?
According to the spec, a
Promise
is just a "thenable", i.e. an object with athen()
method with a certain signature. In ActionScript, this could best be expressed with an interface. Your implementation provides aPromise
class that aggregates (at least) three aspects:Promise
interface:then
,otherwise
,always
,done
when
,isThenable
,all
, ...For separation of concerns, a clear, concise public API, and to simplify building adapters to foreign promises, these aspect should be separated into three entities:
IPromise
that only specifies one method:then
PromiseImpl
that should not be used by client code directly, only throughDeferred
(maybe this is really theResolver
?)Promises
containing all static utility methods plus static convenience methods forotherwise
,always
anddone
, using only theIPromise
interface in its APIDeferred#promise
would change its type fromPromise
toIPromise
.To stay compatible, class
Promise
could be[Deprecated]
andIPromise
interface, delegating toPromiseImpl
Promises
Promises
If you definitely want to keep the additional convenience methods
otherwise
,always
,done
, instead of adding them as static convenience methods toPromises
, you would add them toIPromise
and provide anAbstractPromise
class that implements the convenience methods, but notthen
. (Technically, since ActionScript does not offer abstract classes or methods, it would have to implementthen
, but it could for example throw an "abstract method" error.) AllIPromise
implementations (PromiseImpl
, foreign promise adapters) would then extendAbstractPromise
and only have to implementthen
.