[erlang-questions] if vs. case (long)
Jay Nelson
jay@REDACTED
Thu Mar 13 06:27:51 CET 2008
There were a few points in the if vs. case discussion and the binding
of multiple variables in the case statements that I wanted to make,
but couldn't find all the relevant bits so I started a new thread.
I personally almost never use 'if'. I think it was an unfortunate
name choice because it is so ingrained in any imperative programmer's
toolset, but it has a different meaning in erlang. I would not miss
it if it were removed, however, I do see Sean's points about other
people's style.
I think someone else showed that the alternatives in a function
definition can be used in place of both 'if' and 'case' so neither is
really necessary, but I think case does make code more concise if it
is not abused with too much complexity.
My default instinct in erlang is to use case because I know it can
always be easily expanded to include more branches, and it guarantees
that all legal cases are enumerated so nothing is hidden in the order
of branches:
Var = computeStuff(),
case DebugOn of
true -> log(Var);
false -> ok
end,
continue(Var).
While this may seem like extra typing, it is clear that there are
only two legal cases. In general, I don't often find myself in this
situation. It has something to do with thinking in imperative terms,
versus thinking in functional terms. In general, I would probably
end up with the following log function:
%% Standard clauses for logging.
log(What, true) -> log(What);
log(_What, false) -> ok. %% or use _NotTrue instead of false if
other values are ignored.
And then would pass around a debug logging flag. Even though it
looks inefficient, the compiler should do a good job of optimizing
this, it allows tracing as others have said, and gives the option of
adding special cases of What that apply to all code locations:
%% Placed as first clause of function, catches special cases...
log({special_case, Value} = What, true) ->
report_special_case(Value),
log(What);
The added advantage is that the inline code has no branches because
the case is eliminated, so it is easier to read in a functional
manner. I tend to prefer to call out to a function which may have
several alternatives, but always returns a common value (or no value)
which hides the complexity of what needs to be done.
Var = computeStuff(),
log(Var, DebugOn),
continue(Var).
------------------------------------------------
Case is my default when differentiating on a single value. I resort
to if when there are different reasons for each branch, which depend
on different bindings:
if
StopLight =:= red -> stop();
StopLight =:= yellow and CopPresent =:= true -> stop();
PedestrianInCrosswalk =:= true -> stop();
true -> go()
end
Without an if statement, I would turn this into a function:
drive() ->
go_or_stop(StopLight, CopPresent, PedestrianInCrosswalk).
go_or_stop(red, _, _) -> stop();
go_or_stop(yellow, true, _) -> stop();
go_or_stop(_, _, true) -> stop();
go_or_stop(_, _, _) -> go().
The confusion on which argument represents which value can be
documented in one of two ways:
1) Use labels => (yellow = _LightColor, true = _CopPresent, _ =
_PedestrianInCrosswalk)
2) Pass the args as a record => #intersection_state{color = yellow,
cop = true, ped = false}
------------------------------------------------
There was another discussion about adding variables to case
statements. The starting clauses were:
{ValA, ValB} = case Var of
2 -> {5, computeB(2)};
4 -> {computeA(4), 5};
_Other -> {5,5}
end,
...
Over time, more cases and variables are added to get something like
the following:
{ValA, ValB, ValC, ValD, ValE} = case Var of
... 12 different branches ...
end,
...
I would argue that the code has long ago evolved past the original
construct, and should have been refactored. How it is refactored
depends on how related the variable bindings are.
If the variables are being set independently of one another, or some
of them are being set based on bindings other than the discriminator
(in this case, Var), they should be split from the rest. One
alternative is to use a separate function for those that are
independent of Var:
ValA = computeA(AltVar),
ValB = computeB(AltVar, AnotherVar),
{ValC, ValD} = case Var of
...
end,
ValE = computeE(Var, AltVar),
...
The separate functions for computing can each have a different number
of branches.
If the 5 variable bindings are related to each other, use
encapsulation techniques to show their relationship just as you would
in an OO language: create a record type that holds the values. Then
call a separate function that takes all the parameters needed to
construct the bound record instance:
Obj = make_new_object(AltVar, AnotherVar, Var),
...
make_new_object(AltVar, AnotherVar, Var) ->
{ValC, ValD} = compute_dependent_vars(Var),
ValA = computeA(AltVar),
ValB = computeB(AltVar, AnotherVar),
ValE = computeE(Var, AltVar),
#object{a=ValA, b=ValB, c=ValC, d=ValD, e=ValE}.
Related to this someone mentioned that things get complicated when a
throw is used inside one of the branches of the complex case
statement. I tend to avoid throw and rely on crashes instead. It is
best not to test for cases that have to be handled in a special way
if there is an easier mechanism to get around them. If you really
have to throw, I think you will find the helper function approach
(either of the last two cases above) to more easily accommodate a
traceable throw coming from one of the support functions.
----------------------------------------------------
I think Mats suggestion to put a Good vs. Bad coding style manual in
the docs would help alleviate a lot of complaints that are not
language issues, but poor usage approaches.
jay
More information about the erlang-questions
mailing list