[erlang-questions] Human readable errors in lists module

Ulf Wiger ulf@REDACTED
Wed Feb 11 17:29:26 CET 2009


I think there exists a refined version of this philosophy, but
zipwith() is perhaps not the best example to illustrate it. I agree
that 'let it crash' is the first principle, but the owner of the
interface has a responsibility to try to make the API behave in a
manner that doesn't confound the user. I'd be upset if, e.g.,
mnesia:activity() exited with function_clause in mnesia_trans,
mnesia_dumper etc, as a result of mistakes in using the interface.
Even crashes must give the user some reasonable indication of who's at
fault.

BR,
Ulf W

2009/2/11, Kenneth Lundin <kenneth.lundin@REDACTED>:
> Hi,
>
> As responsible for the Erlang/OTP team at Ericsson I can just say that
> we agree with Joe in this matter.
> We have no intention to add human readable errors to the list module
> or other library modules in standard lib.
>
> As a matter of fact I think a crash with function clause is quite
> clear because it informs you about
> that a named function did not have any clause which matched the
> arguments you called it with. And the arguments
> are also available in the crash info.
> As the source code and the documentation for lists in this case is
> available it should be possible to understand why the arguments was
> not accepted.
>
> A beginner might not have so easy to understand whats wrong but after
> just a few occurences of errors like that it should be
> quite obvious.
>
> /Kenneth Erlang/OTP Ericsson
>
> On Tue, Feb 10, 2009 at 10:46 PM, Joe Armstrong <erlang@REDACTED> wrote:
>> On Tue, Feb 10, 2009 at 4:22 PM, Adam Lindberg
>> <adam@REDACTED> wrote:
>>> Hi,
>>>
>>> Is there any reason that for example lists:zipwith/3 returns a function
>>> clause instead of a human readable error when the lists are of different
>>> length?
>>
>> Yes
>>
>> Everything has a cost - if we applied this principle rigidly to all
>> functions in
>> the system then every function in the entire system would be extended with
>> additional code. This make the code more difficult to read, and makes the
>> program larger (think cache hits).
>>
>> Suppose we have a function that maps a to 1 and b to 2, I'd write it like
>> this:
>>
>> f(a) -> 1;
>> f(b) -> 2.
>>
>> should I then add an additional clause to warn for bad arguments?
>>
>> f(a) -> 1;
>> f(b) -> 2;
>> f(_) -> error('arg is not a or b').
>>
>> This adds nothing to the clarity of the code since the original expresses
>> exact
>> the intention of the program *and nothing else*
>>
>> Programs that are cluttered with additional error messages are difficult
>> to read
>> (which increases the chances of an error) an less efficient
>> (everything has a cost)
>>
>>> It might seem obvious at first but the reason I'm asking is because a
>>> colleague of >mine just spent a long time debugging code with used
>>> list:zipwith/3 and it threw >this error. What he did at first was to
>>> check that all arguments to lists:zipwith/3
>>> > was not zero (this is what the function clause error indicated).
>>
>> *everybody* spends a long time figuring out what when wrong the first time
>> they get an error of a particular type - then they learn - when you've
>> seen
>> these errors a few times you'll find that you can find the error very
>> quickly
>>
>> The fact that erlang crashes at the first error and prints something
>> really
>> aids debugging ...
>>>
>>> lists:zipwith/3 could have been implemented as below (or something
>>> similar):
>>>
>>> zipwith(F, [X | Xs], [Y | Ys]) -> [F(X, Y) | zipwith(F, Xs, Ys)];
>>> zipwith(F, [], []) when is_function(F, 2) -> [].
>>> zipwith(F, [], Ys) -> error(lists_of_different_length). %% Just a
>>> proposal, insert
>>> zipwith(F, Xs, []) -> error(lists_of_different_length). %% preferred
>>> error mechanism here.
>>>
>>> The function clause, noting the arguments as [#Fun..., [], [5,6,7,...]],
>>> is kind of misleading since it happens inside the lists:zipwith/3
>>> function.
>>>
>>> I can see the purists' argument here "that it is really a function
>>> clause" but I also see the pragmatist argument "that it is much easier to
>>> debug."
>>
>> Nw let's look at the error message - here's an experiment
>>
>> 1> lists:zipwith(fun(X,Y) -> X + Y end, [1,2,3],[4,5]).
>> ** exception error: no function clause matching
>>                    lists:zipwith(#Fun<erl_eval.12.113037538>,[3],[])
>>     in function  lists:zipwith/3
>>
>> To the experienced eye the error is clear
>>
>> zipwith(Fun, [3], []) doesn't match any of the clauses defining zipwith
>>
>> Show me the code Luke ... (just run less on
>> /usr/local/lib/erlang/stdlib ... ish)
>>
>> zipwith(F, [X | Xs], [Y | Ys]) -> [F(X, Y) | zipwith(F, Xs, Ys)];
>> zipwith(F, [], []) when is_function(F, 2) -> [].
>>
>> This is two lines of code.
>>
>> So zipwith(Fun, [3], []) doesn't match one of these two lines of code ...
>>
>> this is actually *shorter* than the documentation (a lot shorter)
>>
>> what does the documentation say?
>>
>>  zipwith(Combine, List1, List2) -> List3
>>       Types  Combine = fun(X, Y) -> T
>>       List1 = [X]
>>       List2 = [Y]
>>       List3 = [T]
>>       X = Y = T = term()
>>      Combine the elements of two lists of equal length into one list.
>>
>> **************
>> What I have noticed teaching Erlang is that beginners make almost exactly
>> the same mistakes as experienced users - the difference is in the time
>> it takes them to debug an error.
>>
>> The first time is *always* slow - then you learn.
>>
>> (this is universally true - while I can fix erlang programs pretty quickly
>> I can stare at simple javascript errors for ages before twigging what
>> went wrong)
>>
>> But there's a more subtle problem.
>>
>> Let's try your suggestion. (I put your zipwith in a module test4)
>>
>> try4:zipwith(fun(X,Y) -> X + Y end, [1,2,3],[4,5]).
>> ** exception error: lists_of_different_length
>>     in function  try4:zipwith/3
>>     in call from try4:zipwith/3
>>
>> It works - great - we think ... but what about this?
>>
>> try4:zipwith(fun(X,Y) -> X + Y end, {1,2},[4,5]).
>> ** exception error: no function clause matching
>>                    try4:zipwith(#Fun<erl_eval.12.113037538>,{1,2},[4,5])
>>
>> Now what? Opps the guards were wrong - need to add a few
>> when is_list(..) guards. Or do we want an error message that says
>> error,arg1 should not be a tuple ....
>>
>> There are a very large number of ways we can supply incorrect arguments
>> and we can't program all of them.
>>
>> So what do we do - we only write patterns that match the desired cases
>> *and nothing else* - this is part of the erlang "let it crash" philosophy.
>>
>> In erlang we don't do defensive programming - (or rather we do do it using
>> patterns)
>>
>> Best
>>
>> /Joe Armstrong
>>
>>
>>> Cheers,
>>> Adam
>>> _______________________________________________
>>> erlang-questions mailing list
>>> erlang-questions@REDACTED
>>> http://www.erlang.org/mailman/listinfo/erlang-questions
>>>
>> _______________________________________________
>> erlang-questions mailing list
>> erlang-questions@REDACTED
>> http://www.erlang.org/mailman/listinfo/erlang-questions
>>
> _______________________________________________
> erlang-questions mailing list
> erlang-questions@REDACTED
> http://www.erlang.org/mailman/listinfo/erlang-questions
>



More information about the erlang-questions mailing list