2

How do List Functions Fail in Erlang?

 3 years ago
source link: https://medium.com/erlang-battleground/how-do-list-functions-fail-in-erlang-e022eec6eecd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Introduction

Initially, I thought about writing this article as a story (like I usually do), but I suddenly had too much information on my hands. So, I chose to start with a table/cheatsheet instead.

The goal of the table is to show how the different high-order functions in the lists module react when they’re given bad input, particularly in the argument where they expect a function. This bad input can be either…

  • something that’s not a function (e.g. lists:map(not_fun, [1,2,3]) ); or…
  • a function with the wrong arity (e.g. lists:map(fun() -> wat end, [a]) ).

Empty Lists

Before we start and to reveal about half of the issue here, let me present you the first asymmetry:

1> lists:map(something_wrong, [1,2,3]). % A non-empty list
** exception error: bad function something_wrong
in function lists:map/2 (lists.erl, line 1243)
2> lists:map(something_wrong, []). % An empty list
** exception error: no function clause matching lists:map(something_wrong,[]) (lists.erl, line 1242)

And this happens in all the functions you’ll see below: If you call these functions with a bad first argument and an empty list, you’ll get a function_clause error. The same happens if you call them with something that’s not a list at all. If you call them with a non-empty list, well… keep reading ;)

At least, the lists module is consistently raising function_clauseerrors when the list argument is empty. That’s good.

Non-Empty Lists

What happens when the lists are not empty? To figure that out, I used this:

And these are the results I got…

Findings

So, as you can see, while most of the functions will raise a badfun error if you don’t use a function at all, and a badarity error if you use the wrong function arity, some of them will instead raise a function_clause error in both scenarios.

For context, this is how each of those errors looks as described by the shell…

10> lists:foldl(not_fun, 1, [a,b,c]).
** exception error: bad function not_fun
in function lists:foldl/3 (lists.erl, line 1267)
11> lists:foldl(fun() -> {wrong, arity} end, 1, [a,b,c]).
** exception error: interpreted function with arity 0 called with two arguments
in function lists:foldl/3 (lists.erl, line 1267)
12> lists:foldr(fun() -> {wrong, arity} end, 1, [a,b,c]).
** exception error: no function clause matching lists:foldr(#Fun<erl_eval.45.79398840>,1,[]) (lists.erl, line 1279)
in function lists:foldr/3 (lists.erl, line 1280)

At least from my perspective, the descriptions for the errors are clearer for lists:foldl/3 than they are for lists:foldr/3. And as I told you before, when the list is empty, all of the functions will look like lists:foldr/3(i.e., they'll raise function_clause in every bad scenario).

Is this a Problem?

Semantically, no. All of these functions are not defined for the scenarios I’m presenting so far. Therefore we shouldn’t rely on the errors that they produce when they’re called with improper values.

But, if you are new to Erlang and/or you’re trying some stuff on the console, it’s not unlikely that you call one of the functions above incorrectly. In that case, function_clause can be really confusing, while badfun or badarity ones can more easily guide you towards a solution for your problem.

Furthermore, if, for whatever reason, you need to write code that catches those errors and you don’t want to catch any error (only the specific ones you need), you’ll have to be careful always to handle function_clause in case your input is an empty list.

In other words, this is wrong

%% @doc similar to lists:all/2 but if the function has the wrong
%% arity it just returns false.
-spec my_all(fun((...) -> boolean()), [X]) -> boolean().
my_all(Pred, List) ->
try lists:all(Pred, List)
catch
error
:{badarity, _} -> false
end.

because…

45> a_module:my_all(fun(X) -> true end, [a,b,c]).
true
46> a_module:my_all(fun() -> true end, [a,b,c]).
false
47> a_module:my_all(fun() -> true end, []).
** exception error: no function clause matching lists:all(#Fun<erl_eval.45.79398840>,[]) (lists.erl, line 1216)
in function a_module:my_all/2

So, you have to write something like…

%% @doc similar to lists:all/2 but if the function has the wrong
%% arity it just returns false.
-spec my_all(fun((...) -> boolean()), [X]) -> boolean().
my_all(Pred, List) ->
try lists:all(Pred, List)
catch
error
:{badarity, _} -> false
error:function_clausewhen List == [] -> false
end.

What would I like to see?

I think the ideal scenario would be for all functions in the table above to have the same error behavior:

  • If they receive something that’s not a function where they expect a function, raise badfun, regardless of list length.
  • If they receive a function with the wrong arity, raise badarity, regardless of list length.
  • If the list or any of the other parameters is wrong (e.g., not a list), raise function_clause.

Basically, behave like lists:foldl/3 but don’t treat empty lists as a special case.

Is this possible?

Yeah! The changes in the list module are actually not that complex. Using lists:map/2 as an example, we “just” need to add these 2 clauses here:

map(F, [H|T]) ->
[F(H)|map(F, T)];
map(F, []) when is_function(F, 1) -> [];
map(F, []) when is_function(F) -> error({badarity, {F, []}});
map(_, []) -> error(badfun).

Is it worth it?

I don’t know… Maybe? Maybe it’s a good plan for a SpawnFest project? 🙃


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK