[erlang-questions] Nested Case Statements v.s. multiple functions
Joe Armstrong
erlang@REDACTED
Mon Sep 25 17:57:37 CEST 2017
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