[erlang-questions] Erlang and Akka, The Sequel

Youngkin, Rich richard.youngkin@REDACTED
Wed Apr 1 04:16:44 CEST 2015

Fred & ok@REDACTED (not sure what else to call you :>), thanks for
your thoughtful answers. They're helping to provide the insight I'm looking
for. More below...

On Mon, Mar 30, 2015 at 7:39 AM, Fred Hebert <mononcqc@REDACTED> wrote:

> On 03/29, Youngkin, Rich wrote:
>> Fred's response in [2] is interesting and informative, but it misses the
>> mark in that it's up to the developer to know/understand how and when to
>> implement the appropriate pattern. That's what capabilities such as
>> gen_server are intended to eliminate.
> I'm not sure I understand the criticism there. [2] was about using
> RPC/futures, and the answer shows how to use RPC/futures.

Maybe I'm splitting hairs here, but my point is that an RPC isn't a Future
(even though the Erlang documentation on rpc:async_call/4 states that it
implements "call streams with promises"). Futures can be implemented in
Erlang using RPCs, but, to me, they're not equivalent semantic concepts. My
intent wasn't to criticise your answer. Your answer does clearly show how
to implement futures using RPCs and I found it extremely helpful.

Garrett Smith gave a great presentation at last week's Erlang conference
about "The Timeless Way of Building Erlang Apps" (see [3] and
erlangpatterns.org). What I think could be useful is a "pattern"
description for futures that incorporates the implementation using RPCs. I
may just do this if someone doesn't beat me to it.

>> The example above is interesting from a semantic perspective, but it's
>> mixing the happy path with failure handling. This is where the next
>> concept/capability of Akka is interesting, namely the ability to compose
>> operations while separating the failure handling path from the "happy
>> path".  Here's the follow-on example from the same course:
> This is where a difference is made with these types/monads, yes!
>  1  val treasure: Try[Treasure] =
>> 2    adventure.collectCoins().flatMap(coins => {
>> 3       adventure.buyTreasure(coins)
>> 4    })
>> 5
>> 6  treasure match {
>> 7     case Success(successValue) =>
>> 9        do something like continue to the next challenge...
>> 10   case Failure(errorValue) =>
>> 11     do something like make the character repeat the previous
>> challenge...
>> So the "happy path" of collectCoins() and buyTreasure() isn't intermingled
>> with what to do if one or both of these operations fail. Specifically,
>> buyTreasure() won't throw an exception if collectCoins() fails. I don't
>> know of any way to express this in Erlang.
> Yes. So the regular `try .. catch` attempt would be:
>    try
>        {ok, Cs} = adventure:collect_coins(),
>        Res = lists:flatmap(fun(Coins) ->
>            {ok, Val} = adventure:buy_treasure(Coins),
>            Val
>        end, Cs),
>    of
>        SuccessValue ->
>            %% Do something like continue to next challenge
>    catch
>        Type:Reason ->
>            %% Do something like maybe repeat the previous challenge
>    end.
> Given what we care about here is whether we actually failed *anywhere* or
> *nowhere* (at least based on your failure value match), this is equivalent
> to what you have. Interestingly, because you could be expected to see each
> operation fail, you might also have a callee go for harder failures:
>    try
>        lists:flatmap(fun(Coins) -> adventure:buy_treasure(Coins) end,
>                      adventure:collect_coins())
>    of
>        SuccessValue -> % Keep going
>    catch
>        _:_ -> % alt path
>    end
> The distinction is there, and really, the challenge is picking which one
> to implement when you design the 'adventure' module. I'd argue for the
> former if you expect multiple operations to 'fail' (it is likely that a
> character cannot connect coins without it being a programmer error), so
> that the caller can choose how to handle alternative branches at every
> level.

I don't see a difference between the previous 2 Erlang implementations. Can
you elaborate?

>> 1. Are these concepts generally useful or just interesting from an
>> academic
>> perspective?
> They are truly useful in my opinion, but how needed they are may depend on
> what exception handling mechanism you have. Erlang does tend to have that
> pattern made explicit with sequences of case expression (as in my last
> example). Whether this is boilerplate that ought to be eliminated is likely
> a question of personal preferences.
>  2. Would it be useful to support these capabilities as first-class
>> concepts
> in Erlang (similar to gen_servers)? Or is this so trivial in Erlang that
>> it's not worth making these first class capabilities?
> This is a more interesting question, because Erlang does have a lot of
> ways to handle exceptions. I mean you can have actual exceptions, option
> types, tagged values, multiple return values, signals, continuations, mixes
> of them, and so on.
> ...

Yes, I do see why you wouldn't want to limit Erlang's capabilities in this
area, especially if it results in losing required contextual data regarding
the failure.

I also had a 3rd question regarding futures. Here it is from ok@
cs.otago.ac.nz's response:

>> 3. Is there any way to express these capabilities in Erlang (in addition
>> to
>> the rpc:async_call as described by Fred in [2], which only covers
>> and doesn't support composition)?

> You've now changed the subject from Try to Futures.
> I think you may have misunderstood
> [2] http://erlang.org/pipermail/erlang-questions/2012-November/070679.html

Yes, I did change the subject to Futures rather abruptly. Sorry about that.
It's possible that I misunderstood [2] above, but I don't think so. So to
press on this a little more, is it possible to compose futures in Erlang in
a manner similar to *Try* as shown above?  I think the answer is yes, I
think it's just a matter of chaining together rpc:async calls passing along
the returned "Key" to the remaining calls to rpc:async_call, e.g.,

27> Fn = fun(Key) -> rpc:nb_yield(Key, 45000), timer:sleep(10), 42 end.
28> Key1 = rpc:async_call(node(), timer, sleep, [30000]).
29> Key2 = rpc:async_call(node(), erlang, apply, [Fn,Key1]).
30> rpc:nb_yield(Key2, 60000).

Note, this example fails with a badrpc on the final line (30), but I'm
guessing this can be worked out.

To wrap up, Try, Future, Observable are useful concepts, perhaps worth
expressing in a pattern someplace such as erlangpatterns.org. I don't think
this has to take away from the ability to use "actual exceptions, option
types, tagged values...." if they are more appropriate to the problem at
hand and are needed retain needed contextual data about a failure.

*Try* is a semantic that may be best expressed in Erlang as something like
*-type try() :: any() | error*, and perhaps only in the context of a
module. This example is a bit rough around the edges, but it's hopefully
good enough to get the point across for now. Composing *Try* may be as
simple as a try/catch block depending on the level of granularity that is
needed and assuming that "let it crash" isn't appropriate.

*Future* is a pattern that can be implemented using rpc:async. I think this
is worth documenting as a pattern.

*Observable" is something I didn't cover very well except to say that it's
is future applied to a stream. This is a bit simplistic.  An *Observable*
is something that may produce events. To receive events, a subscription
must be created by an *Observer*. After a subscription is created the
*Observer* just listens for events. I can see how this can be easily
accomplished in Erlang by registering an *Observer* (e.g., register self())
with an *Observable*) and then processing events as they arrive in the
Observer's mailbox. In fact, this sounds a lot like gen_event.

Thanks again for the comments. Additional comments are welcome.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://erlang.org/pipermail/erlang-questions/attachments/20150331/e2e0768e/attachment.htm>

More information about the erlang-questions mailing list