| Author |
Message |
< Erlang ~ why not a "return" statement in Erlang |
| bkl |
Posted: Wed Sep 10, 2008 11:40 pm |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
I hope I am not the only Erlang programmer who feels the need for a
"return" statement.
It is very common in programming to perform a series of condition
checking / processing steps. Without a "return" statement, a
programmer is forced to go for a N-level nested if statement:
Code: if <Cond#1> ->
blah, blah, Result#1;
true ->
blah, blah, blah,
if <Cond#2> ->
blah, blah, Result#2;
true ->
if <Cond#2> ->
blah, blah, Result#3;
true ->
blah, blah, .....
end
end
end
Instead, a much more readable (at least to me) way of implementing the
same program in a more step-by-step style:
Code: if <Cond#1> ->
blah, blah, return(Result#1);
true -> nop
end,
if <Cond#2> ->
blah, blah, return(Result#2);
true -> nop
end,
if <Cond#3> ->
blah, blah, return(Result#3);
true ->
nop end,
blah, blah, blah, ....
I cannot imaging anyone (or any text editor who can handle
programs with more than 5 levels of nestedness.
Maybe it's my own ignorance, but I can hardly think of a
reason why can't we have a "return" statement in Erlang. Is there any
Erlang guru out there who can shed some light on this matter?
Thanks in advance,
Bkl |
|
|
| Back to top |
|
| dsmith |
Posted: Thu Sep 11, 2008 1:19 am |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
Your coding style is very imperative. I can't think of a time I used more than one level of if expressions.
Try this on for size...
Code:
...
eval_condition(Data),
blah, blah, blah, ....
eval_condition(Data) when <Cond#1> ->
blah, blah, Result#1;
eval_condition(Data) when <Cond#2> ->
blah, blah, Result#2;
eval_condition(Data) when <Cond#3> ->
blah, blah, Result#3;
eval_condition(_) ->
nop.
|
|
|
| Back to top |
|
| bkl |
Posted: Thu Sep 11, 2008 5:43 am |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
Thanks for the reply. Correct me if I am wrong, but I am not sure if your solution will do the same...
In my example, if expression <Cond#1> is evalated to be false and <Cond#2> to be true, the execution will evaluate two lines of "blah, blah, blah", and return Result#2 as the program result.
However, in your example, you basically refactor the nested if statement blocks into functions. To achieve the same execution behavior as my original program, you still have to use nested case statement to first check the result of the 1st invokation of eval_condition(), then 2nd, then 3rd... In short, you will still end up with the same level of nestedness... |
|
|
| Back to top |
|
| dsmith |
Posted: Thu Sep 11, 2008 3:12 pm |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
From what I can tell, my solution is identical to your second solution, and your first solution is slightly different.
It might be helpful to have a more concrete example to work with so we can compare results. |
|
|
| Back to top |
|
| dsmith |
Posted: Thu Sep 11, 2008 4:01 pm |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
The other thing you should keep in mind is that if and case constructs are expressions in Erlang and not just statements. As such they evaluate to a term. So the code below would bind either the atom 'result1' or 'result2' to the variable Term.
Code:
Term =
if
<Guard#1> ->
result1;
<Guard#2> ->
result2
end,
io:format("~w~n", Term).
If you preempt an if expression with a return(result1) or return(result2) in the above example, the if expression would be left unevaluated and the rest of the expressions in the sequence would not be evaluated at all. It would be much the same as raising an exception. I don't think I'd agree that this is desirable for non-exceptional code. |
|
|
| Back to top |
|
| bkl |
Posted: Thu Sep 11, 2008 4:41 pm |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
Ok, here is a (almost real ) "loan approval" example. Only 3 steps are shown, while in a real program, there can be much more steps involved.
Currently in Erlang,
Code: loanApproval(Account) ->
case isFbiWanted(Account) of
true ->
FBI ! {wanted, Account),
%% execution stop and {wanted, Account} returned
{wanted, Account);
_ ->
Account2 = updateFbiCertificate(Account),
case badCredit(Account2) of
true ->
CreditDept ! (badcredit, Account2),
{badcredit, Account2};
_ ->
Booking = prepareBooking(Account2),
BookingDept ! {booking, Booking},
case taxWithhold(Account2) of
true ->
IRS ! {taxwithhold, Account2};
_ ->
:
Proposed with return statement added:
Code: loanApproval(Account) ->
case isFbiWanted(Account) of
true ->
FBI ! {wanted, Account),
return {wanted, Account);
_ -> nop end,
Account2 = updateFbiCertificate(Account),
case badCredit(Account2) of
true ->
CreditDept ! (badcredit, Account2),
return {badcredit, Account2};
_ -> nop end,
Booking = prepareBooking(Account2),
BookingDept ! {booking, Booking},
case taxWithhold(Account2) of
true ->
return IRS ! {taxwithhold, Account2};
_ -> nop end,
:
My understanding is that, theoretical purity aside, all programing languages has to address both the imperative and functional aspects of expressing computation. This is why most of the Erlang program still uses local variables to carry state transition (e.g. Account2 in my program above), and most of imperative languages (unless it is a assembly language where the entire memory address is your state ) support function invocation (even though you can easily abuse the functional behavior by passing global pointers in/out of a function).
Not sure if you regard Lisp as a "functional enough" language, but return statement is supported in Lisp.
I know there are several ways in Erlang to express the same long-list-of-step example above using:
1. create a list of functions and call map on them
2. break each step into a function; i.e. you trade nested function invokation with nested statement.
But my point is not whether it is express-able in Erlang. The argument is whether Erlang as a practical programming language will benefit (or harm) by supporting the return statement.
my 2+1 cents ... |
|
|
| Back to top |
|
| dsmith |
Posted: Thu Sep 11, 2008 5:41 pm |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
Yes, agreed. There are imperative and declarative aspects of all languages. I'm fairly new to functional languages, Erlang being my first and Haskell the only other one I have familiarity with.
Putting aside the question of purity, I have a long history with C/C++/Java, and long before I started Erlang code I knew I disliked deeply nested structures, but I also dislike having multiple returns in a function/method. Many times I've struggled with which if these two 'evils' was more appropriate for my Java method.
I find that using pattern matching and guards on Erlang function clauses provides a much more pleasing result. But this is just my opinion. |
|
|
| Back to top |
|
| dsmith |
Posted: Thu Sep 11, 2008 7:08 pm |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
BTW, I like your example. I work in the financial industry and can find almost the exact same logic implemented in C++ in our system.
We do things very much like this. We have some very large "validation" methods with very complex logic. Each edit is an if statement, with complex condition within.
I've mused about how I would re-write the system in Erlang and believe that using your list of funs approach would provide the best result in complex systems. |
|
|
| Back to top |
|
| bkl |
Posted: Fri Sep 12, 2008 7:31 am |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
So, you guys did send my loan approval information to the FBI after all ...
Seriously, I am not working in the financial industry. But, I just figure these kind of multi-step processing should be common in many applications.
I believe each of the approaches (return statement, nested if, map...) in our discussion has its place to express a particular algorithmetic requirement. I just wish there is a return statement in Erlang so that I am not forced to use other un-natural (at least to me) ways, where a simple return does the job... |
|
|
| Back to top |
|
| jbj |
Posted: Tue Sep 16, 2008 5:45 pm |
|
|
|
Joined: 16 Sep 2008
Posts: 4
|
A return statement is not needed in Erlang because it already has this functionality with a different name.
In Common Lisp return just returns from the current nil block (return is shorthand for (return-from nil). The reason it returns from functions is because when you define a function with defun the function body is put within a nil block.
In Erlang you change block to "catch" and return to "throw".
Now, as to your code, yes deeply nested functions are a code smell. But your proposed solution is a code smell as well (notice that half of your conditions are nop?).
There are a couple of ways to do what you want:
Code:
-export([loanApproval/1]).
loanApproval(Account) ->
loanApproval(Account, isFbiWanted(Account)).
loanApproval(Account, true) ->
FBI ! {wanted, Account};
loanApproval(Account, FbiWanted) ->
Account2 = updateFbiCertificate(Account),
loanApproval(Account2, FbiWanted,
badCredit(Account2)).
loanApproval(Account, _FbiWanted, true) ->
CreditDept ! {badcredit, Account};
loanApproval(Account, _FbiWanted, _Credit) ->
Booking = prepareBooking(Account),
BookingDept ! { booking, Booking},
case taxWithhold(Account) of
true ->
IRS ! {taxwithhold, Account};
_ ->
:
Some notes about the above: First, the result of a message send is the message so instead of
Code:
Proc ! Message,
Message;
We can just say
Second, the auxiliary function names (i.e. all the functions besides loanApproval/1) can be changed if you want. Here I kept e.g. the FbiWanted variable to give the function a different arity, plus I didn't know if the variable was needed any more. If it isn't you could make a function loanApproval2/2 and drop FbiWanted, etc.
And lastly, if you're coming from C/C++ then you may think having all those functions is inefficient. Here in high level land we call that "premature optimization". The fact is, if the code is running too slow then you'll need to run HIPE (Erlang JIT), and I would expect the JIT to inline all that code (if it's a hotspot). Also, notice that all those calls are tail calls (there are no statements after the call), so the intermediate returns might be elided, but I'm not certain on that.
Another approach to the code would be to recognize that being wanted by the FBI or having bad credit when asking for a loan are both exceptional circumstances. So another approach I've seen used would be:
Code:
assertNotFbiWanted(Account) ->
if
isFbiWanted(Account) ->
throw(wanted);
true ->
ok.
assertGoodCredit(Account) ->
if
badCredit(Account) ->
throw(badCredit);
true ->
ok.
loanApproval(Account) ->
try
loanApproval_aux(Account)
catch
wanted -> FBI ! {wanted, Account};
badCredit -> CreditDept ! {badCredit, Account}
end.
loanApproval_aux(Account) ->
assertNotFbiWanted(Account),
Account2 = updateFbiCertificate(Account),
assertGoodCredit(Account2),
Booking = prepareBooking(Account),
BookingDept ! { booking, Booking},
case taxWithhold(Account) of
true ->
IRS ! {taxwithhold, Account};
_ ->
:
These are just a couple of ways to break the code up. The big difference coming from a C/C++/Java/etc. background is that there you may commonly write a method of 500+ lines. I would consider that a problem in any high level language, and the language shows you visually that it's a problem by forcing you to keep adding more and more indents if you try to do this. |
|
|
| Back to top |
|
| bkl |
Posted: Wed Sep 17, 2008 3:48 am |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
jbj,
Thanks for replying. However, I am not sure if can I agree with your
try-catch as return solution...
IMHO, it is always not a good idea to use try-catch-throw other than
exception handling. With the throw solution (unless I put "catch" in
front of EVERY function call) I will need to know the "implementation"
(i.e. whether it throws or not) of the called function to distinguish
normal return values from the "throwed" ones.
Maybe the mis-understanding is due to the particular loan approval
example, where each step seems to branch to an "error" condition
(e.g. wanted by FBI!). But, the same problem can be easily illustrated
by other multi-step processing applications which each step may ends
with a normal "return" value.
PS. other than C/C++/Java/etc, I also used Lisp to implement the
system part of my thesis some years ago. And return is supported in
Lisp. So, once again, my question still remains: why not "return"
(unless you still think try-catch-throw is a good way to "return")... |
|
|
| Back to top |
|
| dsmith |
Posted: Wed Sep 17, 2008 4:49 am |
|
|
|
User
Joined: 08 Aug 2007
Posts: 41
Location: Toronto
|
I think the "when is it ok to throw" and "what constitute an exception" debates are too contentious and I'll leave that one alone. Typically, however, I wouldn't treat a business rule as an exception.
Otherwise I would agree with jbj and vote to omit the return. I like knowing that the result of the last expression in a function clause is always the evaluation of the function. Factoring condition expressions out into function clauses as jbj does in his first example looks good to me.
To your point that not all business edit should cause the process to fail. You are quite correct. We do money laundering checks in our systems; these accounts are flagged and the authorities notified, but the transactions are allowed to continue so as not to tip off the owner. I suspect the same would be true of your FBI edit. |
|
|
| Back to top |
|
| Mazen |
Posted: Wed Sep 17, 2008 8:22 am |
|
|
|
User
Joined: 20 Jul 2006
Posts: 164
Location: London
|
my two cents:
Erlang statements are nested statements combined with AND and OR.
E.g.
(A && B)
Or in Erlang terms:
A, B
Now assume you have an if statement
((If (A > B) Do_This) OR (If (A == B) Do_That))
In Erlang:
If A > B -> Do_This; A == B -> Do_That end
Let the if statment be F, and add a statement C
(A && B && F && C)
Or in erlang:
A, B, If A > B -> Do_This; A == B -> Do_That end, C.
Assuming that I didn't do any mistakes in my example, I can't see how a return statement should be used? It would be like saying "Don't evaluate all my statements" which is a valid way of reasoning but not by cutting of the statements in the middle.
In the above example it is not logical to avoid C based on an expression in F if C is a statement that is to be evaluated on the same level as F, Therefore return values are not needed. IMHO there is nothing wrong with this, there is something wrong in the reasoning of needing a return statement.
A better example would in this case be:
A, B, If A > B -> Do_This, C; A == B -> Do_That end.
Now C can safely be "avoided" and it is logical.
About try/catch; These should be used as a mechanism och describing that something is wrong, not that we have finished executing. If you use try/throw/catch as a mean of return then you are trying to mimic a Honda with your Ferrari.
Hehe, hope this makes sense... I'm famous for describing my thoughts in a way that doesn't make any sense  |
|
|
| Back to top |
|
| jbj |
Posted: Wed Sep 17, 2008 7:18 pm |
|
|
|
Joined: 16 Sep 2008
Posts: 4
|
bkl wrote: jbj,
Thanks for replying. However, I am not sure if can I agree with your
try-catch as return solution...
IMHO, it is always not a good idea to use try-catch-throw other than
exception handling. With the throw solution (unless I put "catch" in
front of EVERY function call) I will need to know the "implementation"
(i.e. whether it throws or not) of the called function to distinguish
normal return values from the "throwed" ones.
You're right. throw/try/catch is not equivalent to return. try/catch is. Sorry for the confusion, what I did was I answered your initial question (why doesn't Erlang have return like Lisp) at the beginning of my mail and then in the rest of the mail provided some alternative solutions to your problem. One of them used try/catch, which implies exceptional circumstances. But the equivalence I was talking about at the beginning was between Lisp return and Erlang's throw/catch.
So first, let me show the equivalence of these two constructs:
Code:
some_fun(Val) ->
Result = catch
case Val of
Val when is_list(Val) -> throw(0);
Val when Val < 10 -> throw(0);
Val -> Val * 100
end
end,
io:format("Got ~w~n", [Result]).
is equivalent to:
Code:
(defun some-fun (val)
(let ((result (block nil
(when (listp val) (return 0)
(when (< val 10) (return 0)
(* val 100))))
(format t "Got ~a~%" result)))
Now if I had not declared that nil block it is true that return would have returned from the function itself. As I said in my previous message, this is because (1) return is short for (return-from nil) and (2) defun atomatically wraps the function body in a nil block.
So if you want exactly the same behavior in Erlang you could make a macro that puts the code body inside a catch block (but of course this would screw up calls to error and exit....)
<snip>
bkl wrote:
PS. other than C/C++/Java/etc, I also used Lisp to implement the
system part of my thesis some years ago. And return is supported in
Lisp. So, once again, my question still remains: why not "return"
(unless you still think try-catch-throw is a good way to "return")...
Here it appears you didn't real my whole message. Fair enough I suppose, these days long messages are only skimmed.
I know Lisp has a return and I explained what it is. It is not what these other languages call return.
But in any event, it doesn't matter what Lisp has or doesn't have. Lisp isn't a functional language. It supports functional programming (as well as procedural, object oriented, context oriented and so on), but it isn't functional in the sense that Erlang or Haskell are.
The bottom line is: you don't know how to program in a language until you know how to express your programs the way the language wants to express them. If you learn to program in Erlang the way you program in C/C++/etc. then you've wasted your time. You'll have missed most of the benefits of programming in Erlang and the very few you do get you could have gotten quicker with libraries in your old language.
Most languages have something about them they excel at, so it's important to learn what this is and how to exploit it. In Erlang that "something" is (concurrency and) pattern matching (note: I'm not talking about regex here). When you set out to write something in Erlang it is best to figure out how to break the problem into patterns. The first code block in my previous reply had a quick cut at doing this for your problem. |
|
|
| Back to top |
|
| bkl |
Posted: Thu Sep 18, 2008 7:19 pm |
|
|
|
User
Joined: 17 Aug 2008
Posts: 19
|
jbj,
I see. So, you propose to use "throw" and "catch" WITHIN a function to
simulate the "nil block" in Lisp so that the throwed exception never
"escape" the confine of the catch block. I can see your point now, but
I still have reservations about using exception handling as a way to
handle a simple, well, "return"...
Incidentally, I just found that there is an entry about "return
statement" in Wikipedia
(http://en.wikipedia.org/wiki/Return_statement). Take a look at the
"Criticism" section. It is quite interesting.
I agree with your statement of
"you don't know how to program in a language until you know how to
express your programs the way the language wants to express them."
And I have been using all the good features you mentioned in Erlang,
and quite like them . But I cannot agree with the (seemingly
recursive) reason of "Erlang does not support return because you need
to use features other than return", and "it's good for you"; i.e.
1. "return" can be replaced by other mechanisms in Erlang
(e.g. catch-throw)
2. because code with "return" is bad practice.
My counter-arguments are:
For 1: "XYZ" can be replaced by other mechanisms in the Turing
Machine. Well, I kind of learned about this fact some time ago and
still struggle to find a good argument against it .
More seriously, for 2, I have seen some 500+ lines of functions with
nested if, loop (and returns , which made me want to "throw" up (no
pun intended . But on the other hand, I have seen programs with
20-some-line functions/predicates/patterns all over the place; by the
time you use up all your hand/mouse/keyboard-eye coordinate power
plus matching all the calling parameters with their caller's local
values among these short program pieces, your mind is already gone crazy.
My point is that, like many things in life, lots of the "rules" are
more of guidelines (nicely put by Cap. Barbossa in the "Pirates of the
Caribbean": "the code is more what you'd call 'guidelines' than actual
rules"). Situation dictates, and there are times when a sequential set
of steps with multiple 1-level if/case + return is way more readable
than "other mechanisms" in Erlang. And, of course, for other times,
you can easily abuse any of these mechanisms and screw up a seemingly
readable execution into a deeply-nested and/or web of messy calls...
I hope I didn't miss any of your arguments, but I think I need a more
direct or stronger argument against return...
P.S. I hope this is not another religious war in brewing. But, I
personally find it interesting when I heard statement like "Lisp is
not a functional language". I can live with "Lisp is not a functional
ENOUGH language" (see my reply to dsmith on Sep 11). If the
implication is that Erlang is one of THE functional languages, like
there is a clear-cut, black-and-white line in the sand of what's pure
functional and what's not. If Erlang is a pure functional language, then noe of the following
should ever happen:
1. Referential transparency:
predicableResultEveyTime(Fd) ->
BufSize = element(1, now()),
file:read(Fd, BufSize)
If you also throw in all the "functions" of random, file I/O,
time related functions, machine CPU statistics..., then the
"transparency" disappears. Humm, why on earth does Erlang have all of
these, but not return?
2. No (immutable) state
put(badvar, "I am a bad boy"),
put(badvar, get(badbar)++" but all girls are nice"),
:
Or,
gen_server:call(Pid,
{set, badvar,
gen_server:call(Pid,{get, badvar}++"I am a bad boy")})
:
My point is, like the primeval DNA from Von Neumann at work, (1) the
concept of "state" in practical programming is unavoidable, and (2) for
practical applications you have to introduce some
"non-theroticall-functional" constructs into the language. We have
come a very long way since the evolution from assembly -> C -> ...->
Java -> ... -> Erlang, Haskel... Along the way, we deviced things like compiler
hints, ingenious programming paradigms, even higher level mathematical
constructs (e.g. monad) to manage these uncivilized-but-necessary-evils
in a language design, but you just cannot totally remove them.
IMHO, lambda calclus is THE only functional programming language I
knew. It can be used to build applications like hand-calculator (as a
homework assignment some decades ago . But, with all the beauty and
theoretical correctness, I am still struggling to build a Web Server
or RDBMS using it...
my 1.5 cents |
|
|
| Back to top |
|
|
|
All times are GMT
Page 1 of 2
Goto page 1, 2 Next
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum You cannot attach files in this forum You cannot download files in this forum
|
|
|