[erlang-questions] Erlang and Akka, The Sequel

Fred Hebert mononcqc@REDACTED
Mon Mar 30 15:39:21 CEST 2015


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.

Also there is *no replacement* to know when to use the appropriate 
pattern. I'm not sure of the intended wording here, if you meant 
"implement" as build the pattern from scratch, or "implement" as using 
the pattern. I went here with the latter interpretation, but do know 
that for futures, there is nothing else to do in Erlang than the example 
in my response to the initial post.

Let's use the Akka example (from 
http://doc.akka.io/docs/akka/snapshot/java/futures.html):

    Timeout timeout = new Timeout(Duration.create(5, "seconds"));
    Future<Object> future = Patterns.ask(actor, msg, timeout);
    String result = (String) Await.result(future, timeout.durtion())

Think of the following usage in Erlang:

   Timeout = timer:seconds(5000),
   Future = rpc:async_call(node(), Mod, call, [Actor, Msg, [Timeout]),
   {ok, Result} = rpc:nb_yield(Future, Timeout).

They are semantically equivalent. The only thing tricky about Erlang is 
to find it in the RPC module.

>1. The concept that an operation might fail (i.e., the "Try" type in 
>Akka).

It appears that `Try` is a type in Scala, not Akka. I went to 
http://helenaedelson.com/?p=939 and 
http://www.scala-lang.org/files/archive/nightly/docs/library/index.html#scala.util.Try 
to try to figure out what they are.

Those appear to be your standard 'either' type, and 'match' in the 
following example looks a bit like a 'maybe' monad, but I'll get on that 
later to avoid reordering the quotes of your email.

>2. Being able to easily compose operations that might fail in a way where
>the logic to detect and react to the failure can be expressed separately
>from the main logic path, aka the "happy path".
>

That does sound a lot like 'let it crash', but I'm guessing the point 
here is due to the cumbersome level of `{ok, Value}` vs. any other value 
to represent successes vs. errors (i.e. moving from an Either type to 
also passing them to a Maybe monad)

>Here's an example of the first (in Scala, taken from the Coursera course on
>Principles of Reactive Programming). It's simplistic, but I think it's
>applicable in more complicated use cases. Hopefully the Scala is pretty
>self-explanatory:
>
>1 val coins: Try[List[Coin]] = adventure.collectCoins()
>2
>3 val treasure: Try[Treasure] = coins match {
>4    case Success(cs) => adventure.buyTreasure(cs)
>5    case failure @ Failure(t) => failure
>6 }
>Basically what this is explicitly saying is that 
>adventure.collectCoins()
>may fail and the success or failure of that operation can be used in the
>next operation which is to buy treasure using those coins. At this point
>this is just expressing the semantics of the operation directly in the
>code, namely that the operation may fail (as is also the case in
>buyTreasure). This isn't a programming error, it's just expressing that
>perhaps the adventure character couldn't collect coins and/or wasn't able
>to buy treasure (maybe they didn't have enough coins). So Erlang's "Let It
>Crash" philosophy isn't applicable.  It can be argued that the block at
>lines 3-5 just a case expression, but this misses the point that the
>language directly expresses, through Try[], that both collectCoins() and
>buyTreasure can fail.

Right, so this could be:

    Coins = adventure:collect_coins(),
    Treasure = case Coins of
        {ok, Cs} -> adventure:buy_treasure(Cs);
        Error -> Error % or should it be thrown?
    end

But this does look like a regular `case` expression, and I'm guessing 
the example of composability would need more complexity to better show 
what you mean.

I am not ready to say that 'Let it crash' doesn't apply. You can very 
well let it crash there, but this is a question that depends on whether 
you decide to or not, and what you might need to report to the user.

>
>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.

The cumbersome alternative is to indeed bake in the flow control to the 
values, yielding things like:

    handle_coins() ->
        case collect() of
            {ok, Val} -> % keep going
            Error -> % error path
        end.

    collect() ->
        case adventure:collect_coins() of
            {ok, Coins} -> buy_treasure(Coins);
            Error -> Error
        end.

    buy_treasure() ->
        %% and so on

It's one we do actually see a lot in Erlang overall, especially if you 
end up wanting specific handling of alternative result at each level 
(logging a message, storing metrics, returning a failed value, undoing 
operations, backoff, whatever) which could be harder to do at one end or 
the other.

>
>As far as I know Erlang doesn't support "Try" and the associated use of it
>in "match". In Akka this support is provided by monads. erlando is an
>Erlang library intended to provide monad support, but monads aren't baked
>into the language. And "Try" just applies to synchronous operations.
>"Future" implements the same semantics to async operations. "Observable"
>implements the same semantics to asynchronous stream operations.
>

Right.

>I don't have enough real-world experience in whether or not these concepts
>are encountered in day-to-day "reactive" programming in Erlang or
>Scala/Akka, but they do seem useful. This leads me to my questions:
>
>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.

Mahesh Paolini-Subramanya, in one of his Erlang factories presentations 
(see 
http://www.erlang-factory.com/conference/Chicago2013/speakers/MaheshPaoliniSubramanya), 
defined the following concerns when handling failures in order to get 
fault tolerance:

1. Separation of Concerns (Each people can be treated individually)
2. Error Encapsulation (The infection doesn't spread)
3. Fault Detection (Make sure you know someone is infected)
4. Fault Identification (You have got this specific strain of flu!)

Separation of concerns os usually given by having each process entirely 
isolated in Erlang, and tends to compose well with Error Encapsulation.

What is interesting is that error encapsulation is invariably best done 
at the lowest level of abstraction possible (this is what isolates an 
error the most from the rest of the system!); conversely, fault 
detection and identification tend to get broken by this approach -- if 
you eat up errors, you never know what they properly are. This is the 
dreaded 'try ... catch' that catches everything and returns 'ok' as if 
everything was fine.

So to me the question is not necessarily whether it's worth making a 
first-class capability out of it, but whether adding it would help in 
any of these cases.

For example, one of the very interesting form we see in a production 
proxy here is the following form:

    relay(Client, Server) ->
        case read(Client) of
            done ->
                {ok, Client, Server};
            {ok, Data} ->
                send(Client, Server, Data);
            {error, Reason} ->
                {error, upstream, Reason}
        end.

    send(Client, Server, Data) ->
        case gen_tcp:send(Server#server.port, Data) of
            ok -> relay(Client, add_metrics(Server, Data));
            {error, Reason} -> {error, downstream, Reason}
        end.

Now the actual code is more complex than this (lots more funnier cases 
with buffering and error-detection)

This kind of mechanism is nice to have because it helps in all cases:

1. Obtained through individual processes
2. An error interrupts the flow ASAP and tells you *why*
3. The error condition bubbles up as a well known tuple (and we also
   take care of identifying good terminations)
4. We know specifically why a condition failed, and whether it was due
   to the client or the server, which turns out to be critical when
   relaying the information to customers.

So for me the question is rather whether option types / Either types / 
maybe monads retain the necessary flexibility. If the semantics of the 
maybe monad end up similar, to say:

    relay(Client, Server) ->
        case read(Client) of
            done ->
                {ok, Client, Server};
            {ok, Data} ->
                ok = gen_tcp:send(Server#server.port, Data),
                relay(Client, add_metrics(Server, Data))
        end.

Meaning that we just fail and lose contextual data when it happens, then 
I, as a programmer, may not get much in terms of expressiveness outside 
of not relying on exceptions. In this case, I'd say the benefit is minor 
at best and let-it-crash (with an optional try ... catch) is still good 
enough for pretty much all intents and purposes. I get a similar result 
(happy path, exceptions/branches may lose some contextual data, apparent 
composability, etc.)




More information about the erlang-questions mailing list