A few times on Twitter, I've complained that Promises are a poor conceptual stand-in for Eventual Values.
A colleague of mine pointed out that I tweet this or something like it every few months. He's correct, I do.
The responses usually flow in saying something to the effect of "Well,
if you're not sure if a thing is a Promise or not, just use
Promise.resolve(thing)
and now you're guaranteed to get either the
Promise, or a Promise that resolves to the thing."
And it's true, that is "a" solution of sorts. It solves the "I don't know if this is a Promise or not" problem by ensuring that you definitely do have a Promise, and not a normal value.
My problem, though, is that I'd really like to write code that interacts with values, and not Promises, and leave the machinery to the computer to work out.
An Eventual Value is a value that is not yet resolved. However, by design, it is mostly indistinguishable from a "normal" value. Here's an example:
// promise code
function add5 (x) {
return x + 5
}
function add5Promise (x) {
return Promise.resolve(x).then(function (x) {
return add5(x)
})
}
function someNumberLater () {
return new Promise(function (resolve) {
databaseConnector.get('numeric value').then(function (x) {
resolve(x)
})
})
}
add5Promise(someNumberLater()).then(function (xplus5) {
console.log('the number plus 5 is %d', xplus5)
})
With Eventual Values, this would look like the following:
// eventual value code
function add5 (x) {
return x + 5
}
function someNumberLater () {
databaseConnector.get('numeric value'))
}
console.log('the number plus 5 is %d', add5(someNumberLater()))
If you imagine getting a few numbers, any of which might be as yet unresolved, it's even more annoying:
// promise code
function addThreeNumbers (x, y, z) {
return Promise.all([x, y, z]).then(function (numbers) {
return Promise.resolve(numbers[0] + numbers[1] + numbers[3])
})
}
// eventual code
function addThreeNumbers (x, y, z) {
return x + y + z
}
- Eventual Values can be interacted with like normal values.
- If an Eventual Value is part of a simple value operation, then that expression resolves to a new Eventual Value which resolves when all its Eventual Values are resolved.
That's it. You interact with Eventual Values as if they're normal synchronous values, and the machine takes care of waiting where it's necessary and appropriate.
Indeed, this is Zalgo's purest form. But let's clarify exactly what's wrong with the maybe-sync anti-pattern.
The problem with Zalgo APIs is that they're hard to reason about. Given code like this:
someApi(function () {
console.log('foo')
})
console.log('bar')
In a sync callback API, you know that this will always print out
foo\nbar
. In an async callback API, you know that this will always
print out bar\nfoo
. This predictability is important for human
brains.
This is why the built-in dezalgo of Promises A+ is so important.
new Promise(function (resolve) {
console.log('foo')
resolve()
})
console.log('bar')
This code must either (a) always print foo\nbar
, or (b) always print
bar\nfoo
. Since Promise resolution can be async, it must be
async all the time, or else this predictability constraint is
violated.
With Eventual Values, Zalgo is not a problem, because whether things happen synchronously or asynchronously, they always happen in the same predictable order.
function getFoo () {
return new Eventual(function (resolve) {
setTimeout(function () {
resolve('foo')
})
})
}
function getBar () {
return new Eventual(function (resolve) { resolve('foo') })
}
console.log(getFoo())
console.log(getBar())
In this case, it will always print foo\nbar
, even though foo
is
resolved later, and bar
is resolved immediately. Eventual Values
behave like synchronous values.
It'd be nice to have some magic isEventual
operator that could tell
you if a thing was already resolved or not.
Or even a wasEventual
operator to tell you whether this thing you
have was ever waited upon.
But mostly, while it's great to be able to introspect programs, it's even better not to have to need to introspect programs. We don't have memory address introspection in JavaScript, and no one seems to mind. Inspecting Eventual Values should be akin to peering into the dark machinery of the VM; something that's useful once in a while, and certainly interesting from an academic point of view, but not in the normal day to day activities of the typical JavaScript programmer.
Language design is hard. It may be that there's some really good
reason for at least having a special operator or something to say
"Yes, I would like any Eventual Values to be resolved before calling
this function." Maybe use functionE
instead of function
, I don't
know. I don't care. I just want to stop having to stick my nose in
the machinery. Ideally, we'd almost never have to care, because it
only matters at the very edges of a program, when data is sent to the
DOM, or written to a file or socket or terminal.
This cannot be implemented in userland, nor should they be. Eventual Values are a language feature. Promises are an API, and easy to implement in userland.
Promises will never grow into Eventuals, because they are fundamentally different things, even though they implement a similar pattern.
I've argued in the past, quite forcefully, that synchronous code should look synchronous, and async code should look async. But again, like Zalgo, let's not conflate "a good rule of thumb for API design" with "a language feature that would make our lives better".
If you dig one layer deeper, you find that the only reason asynchronous code needs to look asynchronous is that we continually rely upon our human brains to deal with the timing and synchronicity of our programs.
In cases where we are relying on a meat brain to analyze a program, it is extremely important for it to look like what it is. There will likely always be some API surfaces that are asynchronous, and even in a world with Eventual Values, there is a place for APIs that are creatively asynchronous, and they should look and behave like what they are.
Promises are neat. They're a terse and expressive way to chain together a series of potentially asynchronous actions in a way that's relatively easy for a meat brain to make sense of.
Also, it's quite nice how they invert control differently than callbacks. With a callback, the caller creates and passes a token to the API, with the contract that the API provider will use that token to indicate done-ness. With Promises, the API provider creates and passes a token to the caller, with the contract that the API provider will use that token to indicate done-ness.
It's a subtle difference, but one that many people find easier to reason about. That's fine.
However, if you have a function that takes a list of arguments, any of
which may potentially be promises, and then needs to only act once all
of those promises are resolved, it gets tedious quite fast. Promises
tend to expose quite a lot of boilerplate to the user in these types
of situations. And, since the only straightforward solution is to
either turn all things into Promises, or manually check each for
Promise-ness, one potentially ends up introducing a lot of
nextTick()
delays unnecessarily.
I don't dislike Promises. But I do long for Eventual Values, and Promises are not those.
Prolog. Pretty sure you’re looking for Prolog.