[erlang-questions] Nested Case Statements v.s. multiple functions
Joe Armstrong
erlang@REDACTED
Mon Sep 25 18:05:29 CEST 2017
This thread has had some pretty long replies.
To summarize:
- code the happy path (ie no nested cases, just what you expect to happen)
- add exit(Why) code at points in your code where you realise
something has gone wrong
This is what I call "let it crash"
Then - if you need it - processes the error *somewhere else* ie NOT
at the place where it occurred.
Erlang is a concurrent language - we have many processes running it's not
a biggie if a few of them crash. In a sequential language with one thread
it's a big deal if your one process crashes - so sequential programs spend
all their effort checking things and trying not to crash. We do the opposite :-)
/Joe
On Mon, Sep 25, 2017 at 5:57 PM, Joe Armstrong <erlang@REDACTED> wrote:
> Good question.
>
> This is example of abnormal termination.
>
> The Erlang 'way' is to write the happy path and, not write twisty little
> passages full of error correcting code.
>
> I'll assume send_update has a single argument 'a' or 'b' - and is as follows:
>
> send_update(Arg1) ->
> {Val1, Val2} = do_this(Arg1),
> {Val3, Val4} = do_that(Arg1),
> Val2+Val3.
>
> do_this(a) -> {1,2};
> do_this(b) -> {3,4}.
>
> do_that(a) ->{10,20};
> do_that(b) ->{15,12}.
>
> This is version 1 - no decent errors. Here's what we see in the shell:
>
> 1> 2> demo:send_update(a).
> 12
> 3> demo:send_update(c).
> ** exception error: no function clause matching demo:do_this(c)
> (demo.erl, line 9)
> in function demo:send_update/1 (demo.erl, line 5)
>
> The error message is not very good - but is enough to get you started in finding
> the error. We can get some information like this:
>
> 4> (catch demo:send_update(c)).
> {'EXIT',{function_clause,[{demo,do_this,
> [c],
> [{file,"demo.erl"},{line,9}]},
> {demo,send_update,1,[{file,"demo.erl"},{line,5}]},
> ...
>
> So you can see a stack trace.
>
> But we want better than this. So now I'll make a few small changes.
>
>
> send_update_v1(Arg1) ->
> {Val1, Val2} = do_this_v1(Arg1),
> {Val3, Val4} = do_that_v1(Arg1),
> Val2+Val3.
>
> do_this_v1(a) -> {1,2};
> do_this_v1(b) -> {3,4};
> do_this_v1(X) -> exit({do_this_v1,bad_arg,X}).
>
> do_that_v1(a) ->{10,20};
> do_that_v1(b) ->{15,12};
> do_that_v1(X) -> exit({do_that_v1,bad_arg,X}).
>
>
> We can now call this as follows:
>
> 1> demo:send_update_v1(a).
> 12
>
> 8> demo:send_update_v1(z).
> ** exception exit: {do_this_v1,bad_arg,z}
> in function demo:do_this_v1/1 (demo.erl, line 25)
> in call from demo:send_update_v1/1 (demo.erl, line 19)
>
> 9> (catch demo:send_update_v1(z)).
> {'EXIT',{do_this_v1,bad_arg,z}}
>
> Now let's get creative:
>
> send_update_v2(Arg1) ->
> try send_update_v2_happy(Arg1) of
> X -> X
> catch
> throw:{Type,Val} ->
> lager_error(Type, Val)
> end.
>
> send_update_v2_happy(Arg1) ->
> {Val1, Val2} = do_this_v2(Arg1),
> {Val3, Val4} = do_that_v2(Arg1),
> Val2+Val3.
>
> do_this_v2(a) -> {1,2};
> do_this_v2(b) -> {3,4};
> do_this_v2(X) -> throw({"EMERGENCY",{v1,bad_arg,X}}).
>
> do_that_v2(a) ->{10,20};
> do_that_v2(b) ->{15,12};
> do_that_v2(X) -> throw({"ERROR",{v2,bad_arg,X}}).
>
> lager_error(Type, Val) ->
> io:format("Error:~p ~p~n",[Type,Val]).
>
> Here we retain the happy path code - but add a wrapper that catches
> the error and reports it.
>
> 1> demo:send_update_v2(a).
> 12
> 2> demo:send_update_v2(b).
> 19
> 3> demo:send_update_v2(c).
> Error:"EMERGENCY" {v1,bad_arg,c}
>
> This code is beginning to look nice - but we're not their yet ...
>
> Lets' stop and think. We've refactored the code into three
> parts:
>
> + the happy path
> + the subroutines that report errors if their arguments are wrong
> + a wrapper that decides what to do with the errors
>
> But there's another problem - in a sense we've provisioned the program
> to detect a specific class of errors, and we call lager to report these if
> they happen.
>
> What happens if a totally unexpected error creeps in one where we do not
> throw an error??
>
> send_update_v3(Arg1) ->
> try send_update_v3_happy(Arg1) of
> X -> X
> catch
> throw:{Type,Val} ->
> lager_error(Type, Val);
> error:Why ->
> io:format("Unprovisioned error :: ~p~n",[Why])
> end.
>
> send_update_v3_happy(Arg1) ->
> {Val1, Val2} = do_this_v3(Arg1),
> {Val3, Val4} = do_that_v3(Arg1),
> Val2+Val3.
>
> do_this_v3(a) -> {1,2};
> do_this_v3(b) -> {X,Y,Z} = do_that_v3(b),
> {X+Y,Z};
> do_this_v3(X) -> throw({"EMERGENCY",{v1,bad_arg,X}}).
>
> do_that_v3(a) ->{10,20};
> do_that_v3(b) ->{15,12};
> do_that_v3(X) -> throw({"ERROR",{v3,bad_arg,X}}).
>
> Let's run this code:
>
> 1> demo:send_update_v3(b).
> Unprovisioned error :: {badmatch,{15,12}}
> ok
> 2> demo:send_update_v3(a).
> 12
> 3> demo:send_update_v3(c).
> Error:"EMERGENCY" {v1,bad_arg,c}
>
> Now our program has two type of errors
>
> - errors we write code to handle (they go the error logger) and
> - totally unexpected errors
>
> BUT in the wrapper that calls the happy case we can distinguish the two.
>
> Again this is good practice.
>
> BUT we can do even better :-)
>
> Errors occur in processes - if we spawn_link a process then the
> exception that is caught in the 'try catch end' is propagated to
> the link set of the process.
>
> This means we can handle the error in a *remote* process.
>
> As you can see, you can start with a very simple program
> (the happy case, no error handling) and refine it by adding
> exit(Why) or throw(Why) statements to the code. How
> far you go down this path depends upon the program and the
> target audience.
>
> You can read more about error handling here:
>
> http://learnyousomeerlang.com/errors-and-exceptions
>
> The complete program is here:
>
> -module(demo).
> -compile(export_all).
>
> send_update(Arg1) ->
> {Val1, Val2} = do_this(Arg1),
> {Val3, Val4} = do_that(Arg1),
> Val2+Val3.
>
> do_this(a) -> {1,2};
> do_this(b) -> {3,4}.
>
> do_that(a) ->{10,20};
> do_that(b) ->{15,12}.
>
>
> %% vsn with exit
>
> send_update_v1(Arg1) ->
> {Val1, Val2} = do_this_v1(Arg1),
> {Val3, Val4} = do_that_v1(Arg1),
> Val2+Val3.
>
> do_this_v1(a) -> {1,2};
> do_this_v1(b) -> {3,4};
> do_this_v1(X) -> exit({do_this_v1,bad_arg,X}).
>
> do_that_v1(a) ->{10,20};
> do_that_v1(b) ->{15,12};
> do_that_v1(X) -> exit({do_that_v1,bad_arg,X}).
>
> %% version 2
>
> send_update_v2(Arg1) ->
> try send_update_v2_happy(Arg1) of
> X -> X
> catch
> throw:{Type,Val} ->
> lager_error(Type, Val)
> end.
>
> send_update_v2_happy(Arg1) ->
> {Val1, Val2} = do_this_v2(Arg1),
> {Val3, Val4} = do_that_v2(Arg1),
> Val2+Val3.
>
> do_this_v2(a) -> {1,2};
> do_this_v2(b) -> {3,4};
> do_this_v2(X) -> throw({"EMERGENCY",{v1,bad_arg,X}}).
>
> do_that_v2(a) ->{10,20};
> do_that_v2(b) ->{15,12};
> do_that_v2(X) -> throw({"ERROR",{v2,bad_arg,X}}).
>
> lager_error(Type, Val) ->
> io:format("Error:~p ~p~n",[Type,Val]).
>
> %% version 3
>
> send_update_v3(Arg1) ->
> try send_update_v3_happy(Arg1) of
> X -> X
> catch
> throw:{Type,Val} ->
> lager_error(Type, Val);
> error:Why ->
> io:format("Unprovisioned error :: ~p~n",[Why])
> end.
>
> send_update_v3_happy(Arg1) ->
> {Val1, Val2} = do_this_v3(Arg1),
> {Val3, Val4} = do_that_v3(Arg1),
> Val2+Val3.
>
> do_this_v3(a) -> {1,2};
> do_this_v3(b) -> {X,Y,Z} = do_that_v3(b),
> {X+Y,Z};
> do_this_v3(X) -> throw({"EMERGENCY",{v1,bad_arg,X}}).
>
> do_that_v3(a) ->{10,20};
> do_that_v3(b) ->{15,12};
> do_that_v3(X) -> throw({"ERROR",{v3,bad_arg,X}}).
>
> Cheers
>
> /Joe
>
> On Mon, Sep 25, 2017 at 3:25 PM, code wiget <codewiget95@REDACTED> wrote:
>> Hello everyone,
>>
>> As I get further into Erlang, I am starting to realize that some of my functions have been getting pretty ugly with nested case statements. For example, I had a nested case statement that looked something like this:
>>
>> Send_update(Arg1) ->
>> case do this(Arg1) of
>> {ok, [Val1, Val2]} ->
>> case do_that(Val1, Val2) of
>> {ok, [Val3, Val4]} ->
>> case do_this2(…) of
>> ….
>>
>> It continued into this for another few functions, you get the picture - its ugly, and it is hard to read.
>>
>> So I went and I converted it to a top level function that would then call lower level functions like so:
>>
>>
>>
>> send_update(Arg1) ->
>> case ... of
>> {ok, [Val1, Val2]} ->
>> send_update(check_indices, {Arg1, Val1, Val2});
>> Else ->
>> lager:error("ERROR: ..")
>> end.
>> send_update(check_indices, {Arg1, Arg2, Arg3}) ->
>> case check_indices(Arg2, Arg3)of
>> true ->
>> send_update(get_values, {Arg1, Arg3});
>> false ->
>> lager:error("EMERGENCY: ….")
>> end;
>> send_update(get_values, {Arg1, Arg2}) ->
>> ...
>> case ... of
>> {ok, [Val1, Val2, VAl3]} ->
>> send_update(send_value, {Arg1, Val1, Val2, Val3});
>> Error ->
>> lager:error("ERROR: …")
>> end;
>> send_update(send_value, {Arg1, Arg2, Arg3, Arg4}) ->
>> …
>> Do_something(Args),
>> ok.
>>
>>
>> Now that I look at it though, both don’t look right. They don’t look like something I would write in any other language where I would just have if’s and else’s.
>>
>> Is this the proper way to write Erlang? I know everyone has their own style, but I assume there is some accepted form of writing functional programs with deep nests.
>>
>> Thank you for your advice!
>> _______________________________________________
>> erlang-questions mailing list
>> erlang-questions@REDACTED
>> http://erlang.org/mailman/listinfo/erlang-questions
More information about the erlang-questions
mailing list