Tuesday 20 July 2021

Promises and Futures

In the last years asynchronous programming has become more and more important. Hopefully the old callbacks techniques have given way to objects that later at some point will hold a result, objects that we call Tasks in C# and Promises in Javascript. I had also heard about Java using Future objects, so I just thought of Promises and Futures (and partially also Tasks) as interchangeable funny names "invented" by the designers of those languages.

Well, I've found out that the Promises and Futures concepts were invented long ago in Computer Science (CS), and that they are related, but not the same (and that language designers could have been a bit more careful). If you read this wikipedia article, and check some discussions like for example this one, you'll end up with this idea:

  • In CS, a Future is a read-only container for a result that does not yet exist
  • In CS, a Promises is also a container for a result that does not yet exist, but it can be written (normally only once)

A Future is read-only in the sense that it lacks a public "setResult" kind of method, but obviously there has to be an internal way to set its value. This is normally done by a callback passed to the object constructor (this sounds familiar to all JavaScript people out there, right?)

Java seems to be quite CS friendly regarding this topic based on the naming chosen by its designers. It provides a Future interface (which main implementation is FutureTask) that allows read access to a result. It also provides the CompletableFuture class, that implements the Future interface, but allows write access (you set the result with .complete(result)). So a CompletableFuture is indeed a Promise in Computer Science terms. There is a very nice example here of creating a CompletableFuture but returning it just as a Future, so that the client has no write access to it.

In C# there are no Promise or Future objects, but Tasks, that are read-only. We could write that example above by means of Task and TaskCompletionSource. The difference is that as there is not an "ITask interface" (though we talk about awaitable objects), and TaskCompletionSource is not a Task (nor an awaitable either), but it "contains" and directly gives us access to a Task object, and the result of that object can be set "internally" by the "owner" of the TaskCompletionSource object (by calling TaskCompletionSource.SetResult).

JavaScript Promises correspond to a Future in CS parlor. They are read-only and the only way to set its result is through the resolve-reject callback functions passed to the constructor. It's interesting to note that in JavaScript we use the thenable concept (an object with a "then" method). You can use the await magic keyword with any thenable (not just with Promises), and obviously a Promise is a "thenable", but there is at least one case where Promises and "thenables" are treated differently, in the Promise.resolve method.

It's been centuries since the last time I wrote some real python code, but as it's the first language where I saw the async-await magic construct, it was interesting to see how it deals with asynchrony. We can read here this:

We say that an object is an awaitable object if it can be used in an await expression. Many asyncio APIs are designed to accept awaitables.
There are three main types of awaitable objects: coroutines, Tasks, and Futures.

So Python also uses the awaitable concept. If you read the documentation to know more about those 3 types of awaitables you'll find that even Python designers did not follow the CS expected rules, and a Future in Python is not read-only, as it has a set_result method, so it would correspond to a CS Promise. Indeed, a Future is needed to allow callback-based code to be used with async/await.. and is similar to a Java CompletableFuture (and partially similar to the Task-TaskCompletionSource pair in C#)

There's something I really like, that Python uses a specific object, Task, to run code concurrently, Tasks are used to schedule coroutines concurrently.. This is something that I've always found confusing in C#. In C# we use a same object, Task, for 2 related, but different things:

  • For what we are seeing in this post, a placeholder for a value that will be available in the future and for which we can wait asynchronously
  • For running one piece of code in the ThreadPool by means of Task.Run.

For the second case, frequently we also want to wait asynchronously for its returned value (but not always, maybe we just want to run a code with no need for its result or even knowing if it has completed), but anyway it would be interesting to differentiate between both cases and it would help to avoid some confusions (beginners tend to wrongly think that an async method just magically runs in a separate thread, you can check this related old post)

No comments:

Post a Comment