Stratus3D

A blog on software engineering by Trevor Brown

Promises in Erlang

I recently took the University of Kent’s Concurrent Programming in Erlang course hoping that I would learn some new things about the Erlang VM. I have been programming in Erlang for 3 years so I was a little disappointed when the course only covered the more basic aspects of OTP and didn’t cover anything new to me. Overall the material in the course was a very well presented and it turned out to be a great way to refresh my knowledge of the Erlang concurrency primitives. There was a promise implementation that was briefly presented by Joe Armstrong that caught my attention, and that’s what I want to talk about here.

The code sample Joe presented stood out to me because it’s not the way things are normally done in Erlang. Promises are common in JavaScript, but with asynchronous message passing in Erlang there isn’t really a need for promises. If we have task that we want to run asynchronously in the background we can just spawn and link another process to do some work, and then wait for a message (or exit signal) in a receive block when we need the results. There are also things like Elixir’s Task module that can handle this and hide all the message passing. I was struck by how simple it is to implement simple promises in Erlang:

promise(Pid, Request) ->
    Tag = erlang:make_ref(),
    Pid ! {self(), Tag, Request},
    Tag.

yield(Tag) ->
    receive
        {Tag, Response} ->
            Response
    end.

Using these functions it’s easy to run a task in another process:

% Call the promise function with our function
Tag = promise(ExistingPid, fun() ->
        io:format("expensive task...", [])
    end).

% Receive the results when we need them
Result = yield(Tag).

All we need is the process ID of an existing worker function and we can pass the Pid and the function into the promise function. The promise function calls the erlang:make_ref/0 function returns a unique reference that we use in our response message. This ensures we don’t get things confused if a lot of promise messages are sent to the same process at the same time. Then the promise/2 function sends the function we provided to the worker process in a message, along with the reference. All we have to do is wait for a message back from the worker process in the yield/1 function. The yield/1 function blocks while waiting for a message back from the promise with the tag that is provided. The code running in the worker process isn’t show here, but it’s not hard to spawn a process that receives a function in a message, execute the function, and send a response message back to the caller Pid:

spawn_worker() ->
    spawn(fun worker_loop/0).

worker_loop() ->
    receive
      {Pid, Tag, RequestFun} ->
         % Do the work
         Result = RequestFun(),
         % Send the response back
         Pid ! {Tag, Result},
         worker_loop()
    end.

This all that is needed for the worker process. We just call spawn_worker/0 and that function spawns a process that executes worker_loop/0, which is a recursive function that will loop forever handling request messages by executing the function received and then sending the results back in a message. While this will work it isn’t ideal. In JavaScript we just pass the function into Promise object and get a promise back. We shouldn’t have to use an existing process to execute the function we passed in. The promise function should spawn a new process for us automatically. It’s easy to modify our promise function to do this:

promise(ExecutorFun) ->
    Tag = erlang:make_ref(),
    Self = self(),
    WorkerFun = fun() ->
        % Do the work

        Result = ExecutorFun(),

        % Send a message with the result back to the callers process
        Self ! {Tag, Result}
    end,
    spawn(WorkerFun),

    % Return the reference so we can use it in the yield function
    Tag.

All we have to do is spawn a new process inside the promise/1 function that executes the function we pass in and then sends the results back to the calling process in a message. Using the promise function is now even easier:

% Call the promise function with our function
Tag = promise(fun() ->
        io:format("expensive task...", [])
    end).

% Receive the results when we need them
Result = yield(Tag).

This is much better, but there is still room for improvement here. In JavaScript we have a catch function that we can add to our promise chains to handle exceptions that happen in the promise or previous then handlers. In Erlang there isn’t really a need for catch and then functions because we can pattern match on the result returned from the yield/1 function directly in the calling process. Currently if something goes wrong will our function is being executed by the promise the process will crash. We need to handle exceptions that may occur during the execution of the function so this doesn’t happen. We can do that by wrapping the function call in a try catch block and then sending a different message back to the calling process process based on the outcome:

promise(ExecutorFun) ->
    Tag = erlang:make_ref(),
    Self = self(),
    WorkerFun = fun() ->
        % Do the work

        Response = try ExecutorFun() of
            Result ->
                % If everything goes as planned just return an `ok` tuple with the response
                {ok, Result}
            catch
                Error:Reason ->
                    % Otherwise return an error tuple with the exception type and reason
                    {error, Error, Reason}
        end,

        % Send a message with the result back to the callers process
        Self ! {Tag, Response}
    end,
    spawn(WorkerFun),

    % Return the reference so we can use it in the yield function
    Tag.

This is all that is needed. This promise function takes a function to execute just like the Promise object in JavaScript, spawns a process to run the function, and then sends a message back to the calling process after the function has returned or crashed. We’ve done all this in less than 30 lines of code. Using the new function that handles exceptions is easy. Here we create a promise that immediately crashes, and then we pattern match on the value returned from yield/1 in a case statement:

% Crash the promise process
Tag = promise(fun() -> exit(crash) end),
% Do some other things...
% Then get the results from the promise when we need it
case yield(Tag) of
    {ok, Result} ->
         io:format("Success!", []);
    {error, Error, Reason} ->
         io:format("Failed with ~p due to ~p", [Error, Reason])
end

We now have one block of code that is executed when things go as expected and another that is run when an exception occurs. As you can see there is no need to chain callback functions onto the promise because yield/1 blocks when we need to get the results and the current process will wait until the promise message has been received. There isn’t an equivalent in JavaScript for this. The closest we can get in JavaScript is chaining then and catch callbacks on a promise like this:

new Promise(function() {
           throw 'crash';
        }).then(function() {
            console.log("Success!");
        }).catch(function(reason) {
            console.log("Failed with " + reason);
        });

This is very different because the callbacks are executed asynchronously, whereas in the Erlang code above our yield function is synchronous. If we wanted asynchronous callbacks to be executed in the Erlang promise variation we would simply call them from inside the function we pass into the promise. Something like this:

ThenCallback = fun() -> io:format("Success!", []) end,

Tag = promise(fun() ->
        do_some_work(), % Do everything we need to
        ThenCallback() % Then execute our callback function
    end),

Conclusion

We’ve used spawn, ! (send), and receive to construct a promise function and a yield function in less than 30 lines of code. Our promise function handles executing the executor function in a another process, catching errors that may occur, and sending the appropriate message back to the calling process. The yield function handles receiving the response message and could also enforce a timeout if the promise took too long. This wouldn’t be possible without Erlang’s concurrency primitives. We don’t ever use promises in Erlang because of other things like gen_server and Elixir’s Task, but hopefully this blog post has shown some of the inherit flexible in Erlang’s concurrency primitives.

References

  • https://hexdocs.pm/elixir/Task.html
  • https://www.futurelearn.com/courses/concurrent-programming-erlang
  • https://www.futurelearn.com/courses/concurrent-programming-erlang/1/steps/173396