[erlang-questions] Nested Case Statements v.s. multiple functions

Joe Armstrong <>
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 <> 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 <> 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
>> 
>> http://erlang.org/mailman/listinfo/erlang-questions


More information about the erlang-questions mailing list