Chaining asynchronous functions is a common problem across languages. JavaScript
has Promise
to deal with this, Elm has Task
, and the list goes on.
However, many real-world problems are more complex than straightforward linear
chaining. To solve this, many languages have introduced special syntactic sugar
over their chaining API.
Pseudocode
We want to measure how long an HTTP request takes. To do so, we check the time before and after the request completes and calculate the difference. In pseudocode we could say:
get the current time
THEN get the user data via HTTP
THEN get the current time
THEN calculate the difference between the start time
and end time
Elm
In Elm, both HTTP requests and getting the current time are async operations. We
can chain them with Task.andThen
. However, things are not as straightforward
as the pseudocode made it seem.
Time.now
|> Task.andThen (\before -> getUserDataViaHttp)
|> Task.andThen (\_ -> Time.now)
|> Task.andThen (\after ->
-- ???
-- no access to the before time
-- in this lambda
)
This is awkward because we need both before
and after
at the end of the
pipeline, but andThen
only has access to the result of the operation
immediately previous.
To get around this, we can start nesting the lambdas so the result of each step is always in scope for all the following steps. It’s really ugly but it gets the job done 😬.
timeUserRequest : Task Http.Error Int
timeUserRequest =
Time.now
|> Task.andThen (\before ->
getUserDataViaHttp
|> Task.andThen (\_ ->
Time.now
|> Task.andThen (\after ->
Task.succeed (after - before)
)
)
)
Haskell
Like Elm, Haskell has a function that can be used to chain async operations (the
operator =<<
). To get around the nested lambdas we saw in the Elm example,
Haskell introduces some syntactic sugar: do
notation.
do
before <- getCurrentTime
_ <- getUserDataViaHttp
after <- getCurrentTime
return (after - before)
This desugars into a bunch of chained =<<
calls in nested lambdas. The sugared
version is much nicer to read though!
Scala
Like Elm and Haskell, Scala also has a function that can chain async operations
(theirs is called flatMap
). Like Haskell, it has a syntactic sugar for those
nested lambdas. It’s called a for
comprehension and it’s visually
really similar to Haskell’s do
notation.
for {
before <- getCurrentTime
_ <- getUserDataViaHttp
after <- getCurrentTime
} yield (after - before)
JavaScript
Like all the other languages here, JavaScript has a method for chaining async
operations (Promise.prototype.then
). It also has some syntactic sugar
(async/await
) that makes non-linear chains cleaner. If you squint a bit,
you’ll see it’s very similar to both Haskell’s do
notation and Scala’s for
comprehensions.
Note that while getting the current time can be done syncronously in JavaScript, I’m using a function that returns a promise here so the example is similar to the ones we looked at in other languages.
const timeUserRequest = async () => {
const before = await getCurrentTimeAsync()
await getUserDataViaHttp()
const after = await getCurrentTimeAsync()
return (after - before)
};
Chaining elsewhere
While chaining with then
is promise-specific in JavaScript, the other
languages examined in this article use their chaining functions for many other
constructs - not just for async operations.
For Haskell and Scala, that means that their syntactic sugar notations can be
used for all chainable structures. If you work with these languages you will
encounter the do
/ for
sugar everywhere from IO operations to null-checking,
to JSON parsing.