Futures implemented in Erlang
Erlang Elixir
Latest commit 5d9a5c6 Jul 27, 2013 @gleber Update README
Failed to load latest commit information.



Futures implemented in Erlang. It's an implementation using processes to represent a future. These futures can be used to: 1. Store a value later on 2. Compute a value using a fun 3. Chain/wrap futures to archive feature composition

Futures are actually garbage collected processes which are based on Tony Rogvall's resource project.

Notes on limitations:

  1. requires SMP support
  2. garbage collection works only locally (i.e. future which is not referenced on the node where it was created will be garbage collected)


Implement futures/promises framework which allows to chain futures to implement reusable mechanisms like timeouts, authentication, sharding, etc.

Inspired by http://monkey.org/~marius/talks/twittersystems/

TODO: write more about futures, terminology and possible uses


  • make set/2 and exec/2 transparent in regard to wrappers ??
  • add wrappers that pass params to other futures for sharding and authentication
  • add complex composition to -- wait for specific, but gather other values -- retry next future if first is slow (i.e. redundant fetching data from replicated database if one of shards is slow)


Currently the code supports few basic compositions:

  1. combine
  2. select
  3. timeout
  4. retry
  5. safe
  6. catcher


Simple example with delayed setting of value:

1> F = future:new(fun() -> timer:sleep(10000), 10 end).
2> F:get(). %% it hangs for 10 seconds

Exceptions are propagated with stacktrace preserved:

4> F = future:new(fun() -> a = b end).
5> F:get().
** exception error: no match of right hand side value b
     in function  erl_eval:expr/3
     in call from future:get/1 (src/future.erl, line 94)

Values can be bound to future after it is created:

7> F = future:new().
8> spawn(fun() -> timer:sleep(10000), F:set(42) end).
9> F:get().

Futures can be cloned to rerun them if need be:

65> F = future:new(fun() -> timer:sleep(5000), io:format("Run!~n"), crypto:rand_uniform(0, 100) end).
66> F:get().
67> F2 = F:clone().
68> F2:get().

Deeply wrapped futures can be cloned as well, see future_tests:clone_side_effect_fun_test/0. Combined futures can be cloned as well, see future_tests:clone_combined_test/0.

Multiple futures' values can be collected. If one future fails everything will fail:

5> F1 = future:new(fun() -> timer:sleep(3000), 10 end).
6> F2 = future:new(fun() -> timer:sleep(3000), 5 end).
7> lists:sum(future:collect([F1, F2])).

One can map over futures, which allows to run multiple concurrent computations in parallel:

1> F1 = future:new(fun() -> timer:sleep(3000), 10 end).
2> F2 = future:new(fun() -> timer:sleep(3000), 5 end).
3> F3 = future:map(fun(X) -> X:get() * 2 end, [F1, F2]).
4> F3:get().

Futures can be used to capture process termination reason:

1> Pid = spawn(fun() -> timer:sleep(10000), erlang:exit(shutdown) end).
2> F = future:wait_for(Pid).                    
3> F:get().

A fun can be executed when future is bound:

1> Self = self().
2> F = future:new(fun() -> timer:sleep(1000), 42 end).
3> F:on_done(fun(Result) -> Self ! Result end).
4> flush().
Shell got {ok,42}

future:on_success/2 and future:on_failure/2 can be used to execute a fun when future bounds to a value or to an exception respectively.



Timeout future wrapper can be used to limit time of execution of a future:

8> F = future:timeout(future:new(fun() -> timer:sleep(1000), io:format("Done!") end), 500).
9> F:get().
** exception throw: timeout
     in function  future:'-timeout/2-fun-0-'/2 (src/future.erl, line 270)
     in call from future:'-wrap/2-fun-0-'/2 (src/future.erl, line 220)
     in call from future:'-do_exec/3-fun-0-'/3 (src/future.erl, line 42)
     in call from future:handle/1 (src/future.erl, line 188)

but if timeout time is larger than 1 second it will normally perform expected computation:

13> F = future:timeout(future:new(fun() -> timer:sleep(1000), io:format("Done!~n"), done_returned end), 5000), F:get().


A wrapper which implements retrying in non-invasive way. It can be used to limit number of retries of establishing connection to external possibly-faulty resource. Example:

10> F = future:new(fun() -> {ok, S} = gen_tcp:connect("faulty-host.com", 80, []), S end).
11> F2 = future:retry(F).
12> F2:get().


1> F = future:new(fun() -> {ok, S} = gen_tcp:connect("non-existing-host.com", 23, []), S end).
2> F2 = future:retry(F).
3> F2:get().
** exception error: {retry_limit_reached,3,{error,{badmatch,{error,nxdomain}}}}
     in function  erl_eval:expr/3
     in call from future:reraise/1 (src/future.erl, line 232)
     in call from future:'-wrap/2-fun-0-'/2 (src/future.erl, line 263)
     in call from future:'-do_exec/3-fun-0-'/3 (src/future.erl, line 43)
     in call from future:reraise/1 (src/future.erl, line 232)


Safe wrapper wraps future execution and catches errors and exits:

18> F = future:safe(future:new(fun() -> error(error_in_computation) end)), F:get().

but it will pass through throws, since they are a code flow control tools.


Catcher wrapper is a stronger variant of Safe wrapper, which intercept all exceptions, including errors, exits and throws:

21> F = future:catcher(future:new(fun() -> throw(premature_end) end)), F:get().