Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

state_t + error_t example? #4

Open
RoadRunnr opened this issue Oct 8, 2020 · 14 comments
Open

state_t + error_t example? #4

RoadRunnr opened this issue Oct 8, 2020 · 14 comments

Comments

@RoadRunnr
Copy link

Could you provide an example how to combine a state_t and error_t monad, please?

What I'm looking for is that my function can return either {ok, {Result, NewState}}, {ok NewState} or {error, Error}.

@slepher
Copy link
Owner

slepher commented Oct 9, 2020

StateErrorM = state_t:new(error_m).
Result = 
do([StateErrorM ||
             A <- return(1)
             monad_state:put(A)
             blabla
]).

state_t:run(Result, S) is either {ok, {A, State}} or {error, Reason}

@RoadRunnr
Copy link
Author

thanks for hint, but that does not work as I would expect (although I have to admit that I'm having some difficulty understanding how this is supposed to work).

What I have tried is:

test() ->
    Dimensions = 10,
    SeedCount = 10,
    WaterVolume = 10,
    Time = 10,

    StateErrorM = state_t:new(error_m),
    SM = monad_state:modify(_),
    SMR = monad_state:state(_),
    M = do([StateErrorM ||
	       monad_state:put(init(Dimensions)),
	       SM(plant_seeds(SeedCount, _)),
	       DidFlood <- SMR(pour_on_water(WaterVolume, _)),
	       SM(apply_sunlight(Time, _)),
	       DidFlood2 <- SMR(pour_on_water(WaterVolume, _)),
	       Crop <- SMR(harvest(_)),
	       return(Crop)
	   ]),

    error_m:run(state_t:exec(M, undefined)).

init(X) ->
    X.

plant_seeds(X, State) ->
    error_m:return(X + State).

pour_on_water(X, State) ->
    error_m:return({false, X + State}).

apply_sunlight(X, State) ->
    error_m:return(X + State).

harvest(State) ->
    error_m:return({State, State}).

But this fails in pour_on_water with:

** exception error: an error occurred when evaluating an arithmetic expression
     in function  mylib:pour_on_water/2 (/usr/src/erlang/mylib/src/mylib.erl, line 36)
     in call from state_t:'-state/2-fun-0-'/3 (/usr/src/erlang/mylib/_build/default/lib/erlando/src/instance/monad_transformer/state_t.erl, line 158)
     in call from state_t:'->>=/3-fun-1-'/4 (/usr/src/erlang/mylib/_build/default/lib/erlando/src/instance/monad_transformer/state_t.erl, line 128)
     in call from monad:'->>=/3-fun-0-'/3 (/usr/src/erlang/mylib/_build/default/lib/erlando/src/typeclass/monad.erl, line 55)
     in call from state_t:exec/3 (/usr/src/erlang/mylib/_build/default/lib/erlando/src/instance/monad_transformer/state_t.erl, line 183)
     in call from mylib:test/0 (/usr/src/erlang/mylib/src/mylib.erl, line 27)

@slepher
Copy link
Owner

slepher commented Oct 10, 2020

Just use

plant_seeds(X, State) ->
    X + State

type of monad_state:modify is (s -> s) -> StateT s Either e a -> StateT s Either e ()
other functions is works under the same rule
so while you use {ok, State} + State
it throws exception

@slepher
Copy link
Owner

slepher commented Oct 10, 2020

monad transformer is a concept from Haskell
learning haskell will help you understand this.

@RoadRunnr
Copy link
Author

monad transformer is a concept from Haskell
learning haskell will help you understand this.

That doesn't make send, learning Haskell does not help anyone to understand how to use your library with Erlang.

I understand that there is a difference between the ordering of the Error and State monade, but that still doesn't help me to get my example running.

The sample might not be useful in itself, but it demonstrates what I want to achieve. A monad that applies the state modification no matter whether the function returned ok or error and aborts afterwards if the return was error. The return of that monad should be {ok, State} or {error, State} (and maybe a result).

It would be extremely helpful if you could help with a working example of how this can be achieved with your library.

Reading through all the unit tests has not yielded anything close to that.

@RoadRunnr
Copy link
Author

Or could it be that what I want to achieve is not doable with this library? In my quest for a sample I found code in one of your repositories that would have benefited from such a construct, but is using a hand write state modification https://github.com/slepher/cluster_booter/blob/master/src/cluster_booter.erl#L44-L52

@slepher
Copy link
Owner

slepher commented Oct 26, 2020

i just modify your codes

test() ->
    Dimensions = 10,
    SeedCount = 10,
    WaterVolume = 10,
    Time = 10,

    StateErrorM = state_t:new(error_m),
    SM = monad_state:modify(_),
    SMR = monad_state:state(_),
    M = do([StateErrorM ||
	       monad_state:put(init(Dimensions)),
	       SM(plant_seeds(SeedCount, _)),
	       DidFlood <- SMR(pour_on_water(WaterVolume, _)),
	       SM(apply_sunlight(Time, _)),
	       DidFlood2 <- SMR(pour_on_water(WaterVolume, _)),
	       Crop <- SMR(harvest(_)),
	       return(Crop)
	   ]),

    error_m:run(state_t:exec(M, undefined)).

init(X) ->
    X.

plant_seeds(X, State) ->
    X + State.

pour_on_water(X, State) ->
    {false, X + State}.

apply_sunlight(X, State) ->
    X + State.

harvest(State) ->
    {State, State}.

why?
type of monad_state:modify is (s -> s) -> StateT s Either e a -> StateT s Either e ()
other functions is works under the same rule
so while you use {ok, State} + State
it throws exception.

@slepher
Copy link
Owner

slepher commented Oct 26, 2020

by the way
replace A + B with

plus(A, B) ->
    try
        A + B
    catch
        Type:Exception:StackTrace ->
            erlang:raise(Type, {plus_error, A, B, Exception}, StackTrace)
    end.

will help you to locate the error.

@RoadRunnr
Copy link
Author

RoadRunnr commented Oct 26, 2020

First, thanks for the answer. But I think you are not getting what I'm trying to achieve.

Your modified example does not do what I want. I want to be able to return {error, State} from a function and have the monad abort at that point and return State as the final state (or return {ok, State} and have it continue). That is the whole point of using error and state in the monad.

You version would work with a simple state monad, no need for the error monad.

I think in Haskell terms, that is called applying a monad state transformation to an error monad or the other way around (adding error handling to a state monad) ...
(essentially this https://stackoverflow.com/questions/4063592/how-can-i-write-a-state-monad-that-does-error-handling-as-well but in Erlang)

@slepher
Copy link
Owner

slepher commented Oct 26, 2020

replace

StateErrorM = state_t:new(error_m),

with

StateErrorM = error_t:new(state_m)

and at last,
use

state_m:run(error_t:run(M), undefined).

state_m:run will retrun {Result, State}
state_m:exec will only return State (Result or Error is ignored)

if you get some error
you will get

{{left, Reason}, State}

if not, you will get

{{right, Value}, State}

@RoadRunnr
Copy link
Author

doesn't work, something is still missing, the error transformer is not stripping the ok and {error, Error} part. I also tried various throws (erlang:throw, monad_throw, error_t:fail) without success.

Full example:

test() ->
    Dimensions = 10,
    SeedCount = 10,
    WaterVolume = 10,
    Time = 10,

    StateErrorM = error_t:new(state_m),

    SM = monad_state:modify(_),
    SMR = monad_state:state(_),

    M = do([StateErrorM ||
	       monad_state:put(init(Dimensions)),
	       SM(plant_seeds(SeedCount, _)),
	       DidFlood <- SMR(pour_on_water(WaterVolume, _)),
	       SM(apply_sunlight(Time, _)),
	       DidFlood2 <- SMR(pour_on_water(WaterVolume, _)),
	       Crop <- SMR(harvest(_)),
	       return(Crop)
	   ]),

    state_m:run(error_t:run_error_t(M), undefined).


init(X) ->
    X.

plant_seeds(X, State) ->
    io:format("Plant State: ~p~n", [State]),
    {ok, X + State}.

pour_on_water(X, State) ->
    io:format("Pour On State: ~p~n", [State]),
    {ok, false, X + State}.

apply_sunlight(X, State) ->
    io:format("SunLight State: ~p~n", [State]),
    {ok, X + State}.

harvest(State) ->
    io:format("Harvest State: ~p~n", [State]),
    {{error, no_harvest}, State}.

@slepher
Copy link
Owner

slepher commented Oct 26, 2020

just use

monad_fail:fail(no_harvest)
test() ->
    Dimensions = 10,
    SeedCount = 10,
    WaterVolume = 10,
    Time = 10,

    StateErrorM = error_t:new(state_m),

    SM = monad_state:modify(_),
    SMR = monad_state:state(_),

    M = do([StateErrorM ||
	       monad_state:put(init(Dimensions)),
	       SM(plant_seeds(SeedCount, _)),
	       _DidFlood <- SMR(pour_on_water(WaterVolume, _)),
	       SM(apply_sunlight(Time, _)),
	       DidFlood2 <- SMR(pour_on_water(WaterVolume, _)),
               if
                   not DidFlood2 ->
                       monad_fail:fail(no_harvest);
                   true ->
                       do([StateErrorM || 
                              Crop <- SMR(harvest(_)),
                              return(Crop)
                          ])
               end
	   ]),
    state_m:run(error_t:run_error_t(M), undefined).

init(X) ->
    X.

plant_seeds(X, State) ->
   X + State.

pour_on_water(X, State) ->
    {false, X + State}.

apply_sunlight(X, State) ->
    X + State.

harvest(State) ->
    {State, State}.

why this code is wrong?

plant_seeds(X, State) ->
    {ok, X + State}.

type of monad_state:modify is (s-> s) -> StateT s Either e a -> StateT s Either e ()
other functions is works under the same rule
so while you use {ok, State} + State
it throws exception.

juse use

plant_seeds(X, State) ->
   X + State.

@RoadRunnr
Copy link
Author

first, many thanks for your patience.

It might be my lack of grasp of monads or we are still talking about different things.

Given this non-monadic versions:

test() ->
    State0 = init(),
    case do_step1(State0) of
	{ok, State1} ->
	    case do_step2(State1) of
		{ok, {Value, State2}} ->
		    case do_step3(Value, State2) of
			{ok, State3} ->
			    case do_step4(Value, State3) of
				{ok, _} = Result ->
				    Result;
				{{error, _}, _State4} = Other4 ->
				    Other4
			    end;
			{{error, _}, _State3} = Other3 ->
			    Other3
		    end;
		{{error, _}, _State2} = Other2 ->
		    Other2
	    end;
	{{error, _}, _State1} = Other1 ->
	    Other1
    end.

I want a monadic version that roughly looks like this:

monad_version() ->
    SM = monad_state:modify(_),
    SMR = monad_state:state(_),
    do([monad ||
	   monat_state:put(init()),
	   SM(do_step1(_)),
	   Value <- SMR(do_step2(_)),
	   SM(do_step3(Value, _)),
	   SM(do_step4(Value, _))
       ]).


init() -> 1.
do_step1(State) -> {ok, State + State}.
do_step2(State) -> {ok, {State, State}}.
do_step3(Value, State) -> {ok, Value + State}.
do_step4(Value, State) -> {ok, Value + State}.

Any of the do_step function could also return {{error, Error}, NewState}, which would skip the remaining functions.

There might be bits missing, but the end result should be roughly that simple. No begin/end clauses in the monad, not if's or anything.

Is that a valid monadic rewrite of the non-monadic version? And if it is, how do I achieve that with you library?

@slepher
Copy link
Owner

slepher commented Oct 27, 2020

write a adapter function

mondify_with_error(StepFun) ->
    do([monad || 
           State <- monad_state:get(),
           case StepFun(State) of
               {ok, Value} ->
                   monad:return(Value);
               {new_state, State1} ->
                   monad_state:put(State1);
               {new_state, Value, State1} ->
                   do([monad ||
                          monad_state:put(State1),
                          monad:return(Value)
                      ]);
               {error, Reason} ->
                   monad_fail:fail(Reason);
               {error, Reason, State1} ->
                   do([monad ||
                          monad_state:put(State1),
                          monad_fail:fail(Reason)
                      ])
           end
       ]).

and then

test() ->
    M = do([monad ||
               monad_state:put(init()),
               mondify_with_error(do_step1(_)),
               Value <- mondify_with_error(do_step2(_)),
               mondify_with_error(do_step3(Value, _)),
               mondify_with_error(do_step4(Value, _))
           ]),
    state_m:run(error_t:run_error_t(M), undefined).

init() -> 1.
do_step1(State) -> {new_state, State + State}.
do_step2(State) -> {new_state, 20, State}.
do_step3(Value, State) -> {new_state, Value + State}.
do_step4(Value, State) -> {new_state, Value + State}.

it's no way to do static type&typeclass check in erlang.
if you use the wrong type, code will fail in strange way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants