rabbitmq/erlando

monad:sequence/2 evaluates 'Xs' from right to left?

Closed this issue · 6 comments

Because it's using lists:foldr/3, sequence/2 is evaluating Xs from right to left?

sequence(Monad, Xs) ->
    lists:foldr(fun (X, Acc) ->
                        do([Monad || E <- X,
                                     Es <- Acc,
                                     return([E|Es])])
                end, Monad:return([]), Xs).
test_sequence() ->
    List = lists:seq(1,5),
    ListM = [do([maybe_m || return(io:format("~p~n", [N])),
                            return(N)]) || N <- List],
    {just, List} = monad:sequence(maybe_m, ListM).

does indeed print out the numbers 1 to 5, in that order.

sequence is a pretty direct translation of the Haskell version at http://www.haskell.org/ghc/docs/latest/html/libraries/base/src/Control-Monad.html#sequence
That too, uses foldr. Whilst the list is indeed traversed from right to left, don't forget that you're actually the evaluation as part of the list construction, and that is done left to right. It is only the combination of the results that is done by sequence. So for example:

test_sequence() ->
    List = lists:seq(1,5),
    ListM = [do([maybe_m || return(io:format("~p~n", [N])),
                            return(N)]) || N <- List],
    io:format("HERE~n"),
    {just, List} = monad:sequence(maybe_m, ListM).

here, you get the numbers printed out first, then HERE. Does that make sense?

There is an interesting issue though with more complex monads where "values" are actually functions. For example, compare the difference between:

test_sequence() ->
    StateT = state_t:new(identity_m),
    List = lists:seq(1,5),
    ListM = [do([StateT || return(io:format("~p~n", [N])),
                           return(N)]) || N <- List],
    io:format("HERE: ~p~n", [ListM]),
    io:format("~p~n", [StateT:run(monad:sequence(StateT, ListM), undefined)]).

and

test_sequence() ->
    StateT = state_t:new(identity_m),
    List = lists:seq(1,5),
    ListM = [do([StateT || return(true),
                           return(io:format("~p~n", [N])),
                           return(N)]) || N <- List],
    io:format("HERE: ~p~n", [ListM]),
    io:format("~p~n", [StateT:run(monad:sequence(StateT, ListM), undefined)]).

In the do transform, the construction of the list will actually force the evaluation of the argument to the first return. So even though in both cases, ListM will be a list of functions, in the first example, you'll get the numbers printed out before the HERE, but not in the second case.

In the first case, as the io:format is performed as part of list construction, it's easy to see it happens left to right. In the second case, it occurs only as a result of the StateT:run call. Whilst sequence does iterate right-to-left through the list, don't forget the list is a list of functions, and thus sequence itself must return a function too - itself, it is not really doing any work at all. So if you then look at the nature of the function that sequence will return, you'll see that the "current" value is forced before the accumulator: ultimately you'll end up with a series of nested functions, one per list element, and the outer-most will correspond to the first element of the list (and was constructed last, due to the use of foldr). It is then only when you hit the StateT:run (or eval or exec) that the outer-most function will be invoked, and evaluation will the ripple down.

I'm just curious about the monad:sequence/2's order of evaluation for monads with side effects like below.
it looks like evaluating right to left.

And I also believe haskell's sequence evaluates left to right as written in the document ---
"...Evaluate each action in the sequence from left to right, and collect the results... ".

Yes, haskell's sequence is also using foldr, but it is not strict. Their foldr is just right associative
and it does not affect the order of evaluation.

% cat src/seffect_m.erl
-module(seffect_m).
-compile({parse_transform, do}).
-behaviour(monad).
-export(['>>='/2, return/1, fail/1,
         test_do/0, test_seq/0]).

'>>='(X, Fun) ->
    io:format(":::~p:::~n", [X]),
    Fun(X).

return(X) -> X.
fail(X)   -> throw({error, X}).

%% --- test ---
test_do() ->
    M0 = abc,
    M1 = def,
    do([seffect_m || V0 <- M0, V1 <- M1, return([V0, V1])]).

test_seq() ->
    M0 = abc,
    M1 = def,
    monad:sequence(seffect_m, [M0, M1]).

% erl -pz ./ebin
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.2  (abort with ^G)
1> seffect_m:test_do().
:::abc:::
:::def:::
[abc,def]
2> seffect_m:test_seq().
:::def:::
:::[]:::
:::abc:::
:::[def]:::
[abc,def]
3> 

I think this is fixed in the above commit - certainly my testing and thinking supports this. I'm quite pleased that the implementation I've come up with is both tail-call based and does not use ++. Please let me know if this works for you.

Mmm, I wonder if the same issue occurs in monad_plus:msum, and maybe also the "real" implementation of >>= in list_m.

I repeated the same small example in my comment above. Yes, it is now left to right. Thanks.

% erl -pz e bin
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.2  (abort with ^G)
1> seffect_m:test_seq().
:::abc:::
:::def:::
[abc,def]
2>