[erlang-questions] Erlang and Akka, The Sequel

ok@REDACTED ok@REDACTED
Mon Mar 30 06:00:18 CEST 2015


"Try" appears to be Haskell's "Maybe" monad,
given a somewhat confusing name, with "flatMap"
being the traditional "join :: Monad m => m (m x) -> m x".

The Maybe monad is very close to the List monad;
basically Nothing ~ [], Just x ~ [x].

And this leads immediately to how you can represent
Try[X] in Erlang:  as list(X), limited to at most 1 element.

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

    Coins = adventure:collect_coins(),
    Treasure = case Coins
                 of [Cs] -> adventure:buy_treasure(Cs)
                  ; Failure -> Failure
               end

The "Failure(t)" bit shows that Try[t] is probably closer
to Either String t.  Either isn't a monad.

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

    Coins = adventure:collect_coins(),
    Treasure = [adventure:buy_treasure(Cs) | Cs <- Coins],
    case Treasure
      of [Success_Value] -> ...
       ; [] -> ...
    end

Your "happy path" then becomes
    Final_Result = [Z |
        A <- first_step(),
        B <- second_step(A),
        ...,
        Z <- last_step(Y)]

> 1. Are these concepts generally useful or just interesting from an
> academic perspective?

Monads are pervasive in Haskell and tolerably comfortable
there due to the essentially typed nature of Haskell.
As soon as you have more than one monad to worry about (like
IO + State + Error + ...) things can get messy.  There is the
Monad Transformer Library to help out, recently revised.

Concepts may be generally useful and practical in one programming
language but not another.  Scala, for example, has this whole
'implicit' thing, so I suppose it can automatically convert a
T to a Try[T] and vice versa when it's appropriate.  Ada and ML,
in contrast, do not.  They are strongly typed, and get a lot of
leverage from their type system, but don't have typeclasses.  So
they use exceptions.

If you wanted to write code that was idiomatic maintainable
Erlang, you'd want to keep it pretty straightforward and obvious.
(As C.A.R.Hoare put it, "It is better that a program obviouslyhave no
errors than have no obvious errors."

To start with, you might want to make a clear distinction
between errors and normal situations.  For example, if you
were doing some calculations with natural numbers, you might
want a function
   least_proper_factor(N)
Passing something that is not an integer is an error.
Passing a negative number is an error.
If the calculation encounters a divide-by-zero, that's an error.
But there are lots of numbers that don't _have_ any proper
factors, 1 for example.  The other things are just plain not
supposed to happen, but passing 1 or a prime is something you
should think about handling.  (Maybe the answer would be that
that too counts as something that is just plain not supposed to
happen; I only say you should *think* about that.)

In an adventure game, there could be all sorts of reasons why
trying to buy treasure with coins could fail:
 - there might not be enough
 - you might have collected East Slakan coins but the
   treasure's owner might only accept West Slakan coins
   (What, you didn't hear about the war in Slaka?)
 - your coins might be fairy gold
 - the coins might actually be little robots that shoot the
   treasure up and fly away
 - the vendor might swallow the coins and deny ever seeing them
 - your nannybot might decide that treasure isn't Good For You
...
Considering all the things that could go wrong, expecting the
program to handle them *and trying to hide the handling* does
not seem like a good idea.

In all discussions like this, a *real* is really important.

> 2. Would it be useful to support these capabilities as first-class
> concepts
> in Erlang (similar to gen_servers)?

I don't know what you mean by "first-class concepts".
I certainly don't see gen_servers as first-class concepts
in Erlang.  They are a library, admittedly a very useful
one, but basically not part of the *language*.

> Or is this so trivial in Erlang that
> it's not worth making these first class capabilities?

Depending on the actual use case, it might be trivial,
or it might be undesirable, or who knows what.

> 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 Futures,
> 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

Quoting my Smalltalk library,
"Futures are not needed because joinable Processes *are* futures."
("Join" here referring to POSIX pthread_join(3).)

The equivalent of a Future in Erlang is just a process.
The only even slightly tricky bit is getting the result back.
Here's the basic code:

    Key = self(),
    Pid = spawn(fun () ->
        Result = (the calculation),
        Key!{promise_reply, Result}
    end),
    ...
    receive {Pid, {promise_reply, Result}} ->
        ...
    end

spawn(), !, and receive _are_ "first-class concepts".

If we define

    future(Fun) when is_function(Fun, 0) ->
        Key = self(),
        {future, spawn(fun() ->
            Result = Fun(),
            Key!{promise_reply, Result}
        end)}.

    resolve({future, Pid}) when is_pid(Pid) ->
        receive {Pid, {promise_reply, Result}} ->
            Result
        end.

then using a Future is just a matter of

    Future = future(fun () -> ... long computation ... end),
    ...
    Result = resolve(Future)

It's not clear what you mean by saying that this doesn't
support composition.  I've expanded out what rpc:async and
rpc:yield do to make the point that they don't do very much,
and that if you want them to do something else, it's
probably going to be pretty easy to do so using Erlang's
native building blocks.

(There's one thing that is a touch awkward about this.
 resolve/1 has to be called in the *same* process that
 called future/1.  That's fairly easy to work around.)

The thing is that nobody normally misses futures in Erlang
because the things that people would use futures for are
so easy to do directly with processes.

The idea of handling futures by registering callbacks,
as described in http://docs.scala-lang.org/overviews/core/futures.html,
gives me the horrors.  (Squeak Promises with their #whenResolved:
give me similar horrors.  I implemented them as a compatibility
feature for my Smalltalk and rapidly realised that I never wanted
to use them.)  The preferred way to provide a result from one
thread to another in Erlang is to *send a message*.






More information about the erlang-questions mailing list