Hacker News new | ask | show | jobs
by skybrian 4221 days ago
I don't think I've ever heard it clearly explained why you rarely need futures in Go. (The blog article doesn't explain it either.)

It seems to be because each goroutine has a single parent (which launched it), so there is typically a single reader for its return value.

1 comments

I'm not sure that's the case. Erlang has public-addressable processes: you can pass a process's handle around, and then anything with that handle can direct messages to that process—including messages containing their own process handles, allowing for responses; built on top of this, there's a global service registry, allowing anything to direct messages to a given process. Despite this, Erlang still doesn't need futures/promises.

Generally, futures/promises just solve the problem of how to simulate blocking within an event-loop-driven (i.e. cooperatively-scheduled) platform, e.g. Node. When actor-processes are cheap, your consumer can be an actor which just actually blocks on a result—and then continues execution in the same linear block of code when that result arrives.

To put it another way: in non-actor languages, you'd see a block of code like this...

    do_random_other_work();
    bfn_args = some_setup();
    blocking_fn(bfn_args).then(function(result) {
      console.log(result);
    });
    more_random_other_work();
      
...while in actor-modelled languages, it'd go more like this:

    do_random_other_work();
    spawn(function() {
      bfn_args = some_setup();
      result = blocking_fn(bfn_args);
      console.log(result);
    });
    more_random_other_work();
Note how you have a closure in both cases, but they contain different parts of the code. The actor-modelled closure contains the setup work of sending the message, as well as the work of dealing with the reply—which must be linear with respect to one-another, but which can all happen concurrently to the random other work.

As well, notice that the actor-modelled version of blocking_fn is actually a plain old blocking function—a synchronous-IO read, for example. The blocking_fn in the promise-modelled code, on the other hand, is a special function which must be defined using a promises library, and must do the work of ensuring its asynchronicity at definition-time, whether or not a given consumer wants to call the function in an async way relative to its own thread of execution.

In short, idiomatic actor-modelled code puts the consuming programmer in control of which tasks are linear and which are concurrent, which usually makes for succinct, reusable plainly-synchronous functions that Get Things Done, and higher-level glue code that represents all the policy of what concurrency happens where. Promises throw out all these advantages and mix policy with mechanism in a way that increases verbosity and decreases reusability.

I think that explains why languages like JavaScript will have many functions that return Promises and Go doesn't need that kind of API. Often in JavaScript, there will only be one reader for the result of a function call, but you'll use a Promise anyway since there are no threads.

But there are cases where you really want multiple readers. For example, on a cache miss, you want a single goroutine to do the work to fill the cache, but there may be multiple goroutines waiting on the result. In this case it might seem natural to model the cache internally as a map from keys to futures, even if it's not exposed?

Your actor-modelled example doesn't touch the idea behind promises, ie the background operations should return a result that is usable by the "main thread". In your example, that would be "result" being passed in some way to "more_random_other_work".

The way to do it would be closer to something like that:

    do_random_other_work();
    actor = spawn(function() {
      bfn_args = some_setup();
      result = blocking_fn(bfn_args);
      emit(result);
    });
    result = wait_for_result(actor)
    more_random_other_work(result);