[erlang-questions] Human readable errors in lists module

Joe Armstrong erlang@REDACTED
Thu Feb 12 16:56:49 CET 2009


Absolutely,

"the system should act in a manner so as to cause the least possible
  astonishment to the user"

It's this in the programming rules?

/Joe Armstrong


On Wed, Feb 11, 2009 at 5:29 PM, Ulf Wiger <ulf@REDACTED> wrote:
> 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