Skip to content

Instantly share code, notes, and snippets.

@isaacs
Last active April 22, 2018 00:42
Show Gist options
  • Save isaacs/76df78e3f67dcb58e27d to your computer and use it in GitHub Desktop.
Save isaacs/76df78e3f67dcb58e27d to your computer and use it in GitHub Desktop.

Promises vs Eventual Values

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.

Eventual Values

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
}

Criteria

  • 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.

But Zalgo!

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.

Introspection, Operators, Etc.

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.

Implementation

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.

Looking Async

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.

Anti-Promises

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.

@kriskowal
Copy link

Prolog. Pretty sure you’re looking for Prolog.

@phpnode
Copy link

phpnode commented Feb 2, 2016

@isaacs you'll find promises a bit nicer if you don't double wrap them (I know this wasn't exactly the point of your post but this mistake is really common). In this example you're treating a promise like a callback, not only does it swallow errors, it provides no advantages over callbacks at all.

function someNumberLater () {
  return new Promise(function (resolve) {
    databaseConnector.get('numeric value').then(function (x) {
      resolve(x)
    })
  })
}

Instead, this can be written as:

function someNumberLater () {
  return databaseConnector.get('numeric value')
}

This does the same thing, but it's a lot shorter, more efficient and ensures that any errors will propagate to the caller.

@keithamus
Copy link

async/await will get you 99% of the way to your ideal:

// eventual value code
function add5 (x) {
  return x + 5
}

function someNumberLater () {
  return databaseConnector.get('numeric value'); // this returns a promise
}

async function main () {
  console.log('the number plus 5 is %d', add5(await someNumberLater()))
}

@justsml
Copy link

justsml commented Feb 4, 2016

Good points Isaac.

I find native promises slightly lacking. With their dodgy error handling, unexpected Promise.race([]) behavior and
irritating memory leaks (especially so with poorly mixed Callback and Promise patterns).

It would be nice if 'Eventual Value' code could be achieved with plain synchronous-style code.

I think things get ugly or fall apart when you try add 'real-world' concerns.
I'm not just throwing that (meaningless) term around. I'm really curious as to how your Eventual Code/Values pattern would handle these issues:

  1. Caching. Often leads to barely-reusable code
  2. Rate-limiting & timeouts. Often a confusing ad-hoc mess - also frequently not functioning as intended
  3. Concurrency limits. Callbacks and Promise.all([db.insert, x1000s ... ]) range from difficult to misery.
  4. Memory leaks. Many promise-ish libs make it seem necessary to write leaky code, with big closures/scope. Now featuring: too many knobs and buttons.
  5. Sequences of discrete tasks. Often sub-tasks have different timeout/concurrency reqs. For example, let's say 1 thread for filesystem read, 4 to extract & transform in RAM, and 8 threads for db inserting. Imagine trying to make it non-linear: lazily extract/process file so DB inserts happen as early as possible, even if the file hasn't processed yet.

All these problems are solved with Bluebird Promises - I'm not on their team, just a massive fan.

Not almost all, but ALL those issues are well tackled by bluebird.
Also the best feature is probably the simplest: promisifyAll which turns any traditional callback code into the Promised Land of beautiful, chainable code. (Sorry, pun intended)

  1. The 'Eventual' code pattern seems like it's just ignoring the fundamentals around crafting durable systems.
  2. Native Promises, async & Generators are no solution either. Compared to bluebird, they look like a bunch of anti-patterns

+1 to @phpnode - keep spreading the good patterns ...

@keithamus - your pattern is good, but it just doesn't dove-tail well when mixing with the massive amount of code already out there.

Here's an example showing what I'm going on about:

From https://gist.github.com/justsml/0afefe73da112df90dae

// Promises are great, but bluebird covers :allthethings
var Promise       = require('bluebird');
var appContext    = {ready: false, error: false};
const INVALID_CTX = {ready: false, error: true, message: 'App or User not initialized'};

// Bluebird-ify around `fetch` API
function getUser(username) {return Promise.resolve('users/' + username + '.json').then(fetch);}

function initApp(username) {
  // Use bluebird to do some real-world-ish code: 
  // First, get a user, cache them, check an inbox, then filter msgs already viewed
  return getUser(username)
    .timeout(2500)                    // easy to declare expected performance, seriously awesome
    .bind(appContext)                 // subsequent Promise methods can share access to appContext obj via `this`
    .tap(user => this.user = user)    // use `.tap` to cache the result without modifying the promise result/chain
    .then(checkInbox)                 // send the returned `user` to `checkInbox` - returns an array of msgs
    .tap(messages => this.messages = messages.length) // cache the # of msgs for UI
    .filter(msg => !msg.viewed)       // applied to current array excludes previously viewed msgs
    .tap(unreadMsgs => {
      // update UI without changing promise's return value
      if (unreadMsgs.length >= 1) {
        showToast(`${unreadMsgs.length} New Messages: ${unreadMsgs[0].subject}...`);
      } else {
        showToast('No new messages');
      }
    })
    .tap(() => this.ready = true) // at the end of the line, set `ready` flag
    .then(checkContext) // return the app's context for further chaining
    .catch(console.error.bind(console, `Err! not very fetching: ${url}`))
}
function checkContext() { 
  if ( !this.user || !this.messages ) {
    // uh oh, override context
    return INVALID_CTX;
  }
  // otherwise ok, return ctx
  return this;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment