Exception error no function clause matching

Доброй ночи, Хабр! Мы продолжаем изучение Erlang для самых маленьких. В прошлой главе мы рассмотрели как объявлять функции и как объединять их в модули. В этой...

Время прочтения
6 мин

Просмотры 21K

imageДоброй ночи, Хабр! Мы продолжаем изучение Erlang для самых маленьких.

В прошлой главе мы рассмотрели как объявлять функции и как объединять их в модули. В этой главе мы рассмотрим синтаксис функций более подробно.

Исходники примеров к главе лежат здесь.

Сопоставление с образцом

Для начала давайте напишем функцию, которая будет приветствовать пользователя и текст приветствия будет зависеть от его пола. В виде псевдокода наша функция будет выглядеть следующим образом:

function greet(Gender,Name)
  if Gender == male then
    print("Hello, Mr. %s!", Name)
  else if Gender == female then
    print("Hello, Mrs. %s!", Name)
  else
    print("Hello, %s!", Name)
end

Если вместо классической конструкции if then else использовать сопоставление с образцом, можно сэкономить кучу шаблонного кода. Вот так эта функция будет выглядеть на Erlang если использовать сопоставление с образцом:

greet(male, Name) -> 
    io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
    io:format("Hello, Mrs. ~s!", [Name]).

Функция io:format() используется для форматированного вывода в терминал. Здесь мы использовали сопоставление с образцом в описании списка аргументов функции. Это позволило нам одновременно присвоить входные значения и выбрать ту часть функции, которая должна быть выполнена. Зачем сначала присваивать значения, а потом сравнивать их в теле функции, если можно сделать это одновременно и в «более декларативном» стиле?

В общем виде объявление такой функции выглядит следующим образом:

fnct_name(X) ->
  Extpression;
fnct_name(Y) ->
  Expression;
fnct_name(_) ->
  Expression.

Каждая ветвь функции объявляется как полноценная функция но заканчивается точкой с запятой(;), следом за ней объявляется следующая. После последней части ставится точка.

Обратите внимание на последний образец. Что будет, если мы вызовем нашу функцию greet() и укажем непредусмотренный пол? Мы получим исключение о том, что входные параметры не подходят ни под один из образцов:

1> chapter03:greet(someOther, "Haru").
** exception error: no function clause matching chapter03:greet(someOther, "Haru")

Поэтому важно включать в объявление образец, который подойдет под любое значение. При этом он должен быть последним. В противном случае образцы объявленные после него никогда не будут обработаны.
Давайте перепишем нашу функцию, так что бы она корректно обрабатывала неверные входные значения:

greet(male, Name) ->
    io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
    io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
    io:format("Hello, ~s!", [Name]).

Но сопоставление с образцом в объявлении функций приносит намного больше пользы, чем просто сокращение объема кода. Вспомним списки: список состоит из головы и остальной части. Давайте напишем две функции, которые будут возвращать первый и второй элементы полученного списка.

first([X|_]) -> 
    X.
second([_,X|_) -> 
    X.

Достаточно просто, не так ли? Есть еще один интересный прием, основанный на том факте, что переменные в Erlang можно присвоить только один раз.

same(X,X) ->
    true;
same(_,_) ->
    false.

Эта функция возвращает true, если ее аргументы одинаковы, иначе возвращает false. Как это работает? При вызове функции chapter03::same(one, two). переменной X присваивается значение one, затем производится попытка присвоить этой же переменной значение two. Из-за того, что значение уже было присвоено, попытка заканчивается неудачей и шаблон отбрасывается как неподходящий. Так как во втором случае мы не указываем явно каким переменным нужно присвоить значения, шаблон подходит и функция возвращает false. Если же передать в функцию одинаковые значения chapter03:same(3, 3)., то первый шаблон подойдет и функция вернет true.

Охранные выражения

У сопоставления с образцом есть один большой недостаток: оно недостаточно выразительно. С его помощью нельзя указать тип данных, диапазон и другие подобные обобщения. Каждый образец является конкретным случаем. Для решения этой проблемы в Erlang есть Охранные выражения (или сторожевые условия). Для начала давайте напишем функцию, которая будет принимать наш ИМТ (индекс массы тела) и оценивать наше состояние. ИМТ человека равен его весу разделенному на квадрат роста.

bmi_tell(Bmi) when Bmi =< 18.5 ->
    "You're underweight.";
bmi_tell(Bmi) when Bmi =< 25 ->
   "You're supposedly normal.";
bmi_tell(Bmi) when Bmi =< 30 ->
   "You're fat.";
bmi_tell(_) ->
    "You're very fat.".

Здесь при обращении к функции происходит проверка первого условия, находящегося после слова when (Bmi =< 18.5). Если это выражение возвращает true будет выполнена соответствующая ветвь кода. Иначе происходит проверка следующего условия и так до конца. И тут мы тоже добавили в конец условие, под которое подойдет любое значение.

В общем случае объявление функции с использованием охранных выражений выглядит следующим образом:

fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_1 ->
    Expression;
fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_2 ->
    Expression.

Так же мы не ограничены одним выражением в условии. Мы можем провести несколько проверок. Если для прохождения проверки должны быть выполнены оба условия (аналог andalso), между ними ставится запятая(,).

lucky_number(X) when 10 < X, X > 20 ->
    true;
lucky_number(_) ->
    false.

Если достаточно хотя бы одного (аналог orelse), они разделяются точкой с запятой(;).

lucky_atom(X) when X == atom1; X == anot2 ->
    true;
lucky_atom(_) ->
    false.

В охранных выражениях можно использовать функции. Вот функция, которая делит одно число на другое, но перед этим проверяет, что бы переданные аргументы были числами и что бы Y не был равен нулю:

safe_division(X, Y) when is_integer(X), is_integer(Y), Y /= 0 ->
    X / Y;
safe_division(_, _) ->
    false.

Ветвление

Оператор if

Помимо рассмотренных выше выражений в Erlang существует и обычный оператор ветвления if. Давайте перепишем нашу функцию bmi_tell() с использованием этого оператора.

if_bmi_tell(Bmi) ->
    if Bmi =< 18.5 -> "You're underweight.";
       Bmi =< 25   -> "You're supposedly normal.";
       Bmi =< 30   -> "You're fat.";
       true        -> "You're very fat."
    end.

В общем случае оператор ветвления if выглядит следующим образом:

if Rule_1 -> Expression;
   Rule_2 -> Expression;
   ...
   true -> Expression
end.

Хочу напомнить, что в отличии от Haskell, здесь отступы не играют никакой роли кроме декоративной и код отформатирован так только того, что бы его было легче воспринимать. Вы вольны форматировать его так, как вам будет удобно.
Здесь выражения rule_1 и rule_2 — это одно или несколько условий. В случае успешного выполнения условия, будет выполнен блок кода Expression (может содержать одну или несколько команд) идущий за ним. Таких логических ветвей может быть сколько угодно.

Обратите внимание на последний блок true -> "You're very fat.. Что это за условие? Это альтернатива оператору orelse. Блок кода идущий за ним будет выполнен если ни одно условие не пройдет проверку. Важно запомнить, что этот оператор обязателен. Ведь, как мы помним, в Erlang любое выражение должно возвращать результат. Если не описать этот блок, то модуль скомпилируется, но когда поток выполнения проверит все условия и не обнаружит блок true, будет сгенерированно исключение.

** exception error: no true branch found when evaluating an if expression
     in function  chapter03:if_bmi_tell/1 

Оператор case ... of

Помимо if в Erlang есть еще один оператор ветвления — case of. И этот оператор подобен целой функции вставленной в другую функцию. Он позволяет делать выбор не только на основании условия, но так же дает возможность использовать сопоставления с образцом и охранные выражения. В общем виде он выглядит так:

case Rule of
    Val_1 -> Expression;
    Val_2 -> Expression
    ...
    Val_n -> Expression

Здесь Rule — это переменная или выражение, результат которого будет проверяться. Дальше следуют уже знакомые нам блоки «условие — выражение» (Val_1 -> Expression;). В условии можно использовать сопоставления с образцом и охранные выражения, что делает эту конструкцию невероятно гибкой.

Давайте для примера напишем функцию, которая будет принимать кортеж состоящий из температуры и названия шкалы ее измерения и на основании этих данных оценивать ее.

assessment_of_temp(Temp) ->
    case Temp of
        {X, celsius} when 20 =< X, X =< 45 ->
            'favorable';
        {X, kelvin} when 293 =< X, X =< 318 ->
            'scientifically favorable';
        {X, fahrenheit} when 68 =< X, X =< 113 ->
            'favorable in the US';
        _ ->
            'not the best tempperature'
  end.

При выполнении этой функции наш кортеж будет сопоставлен с переменной Temp, затем будет найден паттерн, под который этот кортеж подойдет. Затем будет проведена проверка охранными выражениями и если она будет пройдена — функция вернет соответствующую строку.
Здесь мы так же как и раньше добавляем в конец выражение, которое будет перехватывать все неподошедшие варианты.

Как мы видим, выражение case of почти полностью можно переместить в область объявление функции. Так где же лучше размещать условия? Ответ прост: там где вам больше нравиться. Различия между этими двумя конструкциями минимальны. Поэтому используйте тот вариант, который вам легче читать.

Заключение

В этой главе мы рассмотрели то, как можно управлять потоком выполнения внутри функций. Узнали, какие для этого существуют конструкции и как их использовать. Так же мы познакомились с охранными выражениями.

В следующей главе мы более пристально рассмотрим систему типов в языке Erlang.

Спасибо, что дочитали до конца. Надеюсь, было интересно. Прошу прощения за столь большую задержку. Времени свободного совсем нет.

P.S. Об опечатках и ошибках прошу сообщать в личку, а не в комментарии. Спасибо за понимание.

Not so fast!

A green man with a huge head and tiny body on a bicycle

There’s no right place for a chapter like this one. By now, you’ve learned enough that you’re probably running into errors, but not yet enough to know how to handle them. In fact we won’t be able to see all the error-handling mechanisms within this chapter. That’s a bit because Erlang has two main paradigms: functional and concurrent. The functional subset is the one I’ve been explaining since the beginning of the book: referential transparency, recursion, higher order functions, etc. The concurrent subset is the one that makes Erlang famous: actors, thousands and thousands of concurrent processes, supervision trees, etc.

Because I judge the functional part essential to know before moving on to the concurrent part, I’ll only cover the functional subset of the language in this chapter. If we are to manage errors, we must first understand them.

Note: Although Erlang includes a few ways to handle errors in functional code, most of the time you’ll be told to just let it crash. I hinted at this in the Introduction. The mechanisms that let you program this way are in the concurrent part of the language.

A Compilation of Errors

There are many kinds of errors: compile-time errors, logical errors, run-time errors and generated errors. I’ll focus on compile-time errors in this section and go through the others in the next sections.

Compile-time errors are often syntactic mistakes: check your function names, the tokens in the language (brackets, parentheses, periods, commas), the arity of your functions, etc. Here’s a list of some of the common compile-time error messages and potential resolutions in case you encounter them:

module.beam: Module name ‘madule’ does not match file name ‘module’
The module name you’ve entered in the -module attribute doesn’t match the filename.
./module.erl:2: Warning: function some_function/0 is unused
You have not exported a function, or the place where it’s used has the wrong name or arity. It’s also possible that you’ve written a function that is no longer needed. Check your code!
./module.erl:2: function some_function/1 undefined
The function does not exist. You’ve written the wrong name or arity either in the -export attribute or when declaring the function. This error is also output when the given function could not be compiled, usually because of a syntax error like forgetting to end a function with a period.
./module.erl:5: syntax error before: ‘SomeCharacterOrWord’
This happens for a variety of reason, namely unclosed parentheses, tuples or wrong expression termination (like closing the last branch of a case with a comma). Other reasons might include the use of a reserved atom in your code or unicode characters getting weirdly converted between different encodings (I’ve seen it happen!)
./module.erl:5: syntax error before:
All right, that one is certainly not as descriptive! This usually comes up when your line termination is not correct. This is a specific case of the previous error, so just keep an eye out.
./module.erl:5: Warning: this expression will fail with a ‘badarith’ exception
Erlang is all about dynamic typing, but remember that the types are strong. In this case, the compiler is smart enough to find that one of your arithmetic expressions will fail (say, llama + 5). It won’t find type errors much more complex than that, though.
./module.erl:5: Warning: variable ‘Var’ is unused
You declared a variable and never use it afterwards. This might be a bug with your code, so double-check what you have written. Otherwise, you might want to switch the variable name to _ or just prefix it with an underscore (something like _Var) if you feel the name helps make the code readable.
./module.erl:5: Warning: a term is constructed, but never used
In one of your functions, you’re doing something such as building a list, declaring a tuple or an anonymous function without ever binding it to a variable or returning it. This warning tells you you’re doing something useless or that you have made some mistake.
./module.erl:5: head mismatch
It’s possible your function has more than one head, and each of them has a different arity. Don’t forget that different arity means different functions, and you can’t interleave function declarations that way. This error is also raised when you insert a function definition between the head clauses of another function.
./module.erl:5: Warning: this clause cannot match because a previous clause at line 4 always matches
A function defined in the module has a specific clause defined after a catch-all one. As such, the compiler can warn you that you’ll never even need to go to the other branch.
./module.erl:9: variable ‘A’ unsafe in ‘case’ (line 5)
You’re using a variable declared within one of the branches of a case ... of outside of it. This is considered unsafe. If you want to use such variables, you’d be better of doing MyVar = case ... of

This should cover most errors you get at compile-time at this point. There aren’t too many and most of the time the hardest part is finding which error caused a huge cascade of errors listed against other functions.
It is better to resolve compiler errors in the order they were reported to avoid being misled by errors which may not actually be errors at all. Other kinds of errors sometimes appear and if you’ve got one I haven’t included, send me an email and I’ll add it along with an explanation as soon as possible.

No, YOUR logic is wrong!

An exam with the grade 'F'

Logical errors are the hardest kind of errors to find and debug. They’re most likely errors coming from the programmer: branches of conditional statements such as ‘if’s and ‘case’s that don’t consider all the cases, mixing up a multiplication for a division, etc. They do not make your programs crash but just end up giving you unseen bad data or having your program work in an unintended manner.

You’re most likely on your own when it comes to this, but Erlang has many facilities to help you there, including test frameworks, TypEr and Dialyzer (as described in the types chapter), a debugger and tracing module, etc. Testing your code is likely your best defense. Sadly, there are enough of these kinds of errors in every programmer’s career to write a few dozen books about so I’ll avoid spending too much time here. It’s easier to focus on those that make your programs crash, because it happens right there and won’t bubble up 50 levels from now. Note that this is pretty much the origin of the ‘let it crash’ ideal I mentioned a few times already.

Run-time Errors

Run-time errors are pretty destructive in the sense that they crash your code. While Erlang has ways to deal with them, recognizing these errors is always helpful. As such, I’ve made a little list of common run-time errors with an explanation and example code that could generate them.

function_clause
1> lists:sort([3,2,1]). 
[1,2,3]
2> lists:sort(fffffff). 
** exception error: no function clause matching lists:sort(fffffff)
        
All the guard clauses of a function failed, or none of the function clauses’ patterns matched.
case_clause
3> case "Unexpected Value" of 
3>    expected_value -> ok;
3>    other_expected_value -> 'also ok'
3> end.
** exception error: no case clause matching "Unexpected Value"
        
Looks like someone has forgotten a specific pattern in their case, sent in the wrong kind of data, or needed a catch-all clause!
if_clause
4> if 2 > 4 -> ok;
4>    0 > 1 -> ok
4> end.
** exception error: no true branch found when evaluating an if expression
        
This is pretty similar to case_clause errors: it can not find a branch that evaluates to true. Ensuring you consider all cases or add the catch-all true clause might be what you need.
badmatch
5> [X,Y] = {4,5}.
** exception error: no match of right hand side value {4,5}
        
Badmatch errors happen whenever pattern matching fails. This most likely means you’re trying to do impossible pattern matches (such as above), trying to bind a variable for the second time, or just anything that isn’t equal on both sides of the = operator (which is pretty much what makes rebinding a variable fail!). Note that this error sometimes happens because the programmer believes that a variable of the form _MyVar is the same as _. Variables with an underscore are normal variables, except the compiler won’t complain if they’re not used. It is not possible to bind them more than once.
badarg
6> erlang:binary_to_list("heh, already a list").
** exception error: bad argument
     in function  binary_to_list/1
        called as binary_to_list("heh, already a list")
        
This one is really similar to function_clause as it’s about calling functions with incorrect arguments. The main difference here is that this error is usually triggered by the programmer after validating the arguments from within the function, outside of the guard clauses. I’ll show how to throw such errors later in this chapter.
undef
7> lists:random([1,2,3]).
** exception error: undefined function lists:random/1
        
This happens when you call a function that doesn’t exist. Make sure the function is exported from the module with the right arity (if you’re calling it from outside the module) and double check that you did type the name of the function and the name of the module correctly. Another reason to get the message is when the module is not in Erlang’s search path. By default, Erlang’s search path is set to be in the current directory. You can add paths by using code:add_patha/1 or code:add_pathz/1. If this still doesn’t work, make sure you compiled the module to begin with!
badarith
8> 5 + llama.
** exception error: bad argument in an arithmetic expression
     in operator  +/2
        called as 5 + llama
        
This happens when you try to do arithmetic that doesn’t exist, like divisions by zero or between atoms and numbers.
badfun
9> hhfuns:add(one,two).
** exception error: bad function one
in function  hhfuns:add/2
        
The most frequent reason why this error occurs is when you use variables as functions, but the variable’s value is not a function. In the example above, I’m using the hhfuns function from the previous chapter and using two atoms as functions. This doesn’t work and badfun is thrown.
badarity
10> F = fun(_) -> ok end.
#Fun<erl_eval.6.13229925>
11> F(a,b).
** exception error: interpreted function with arity 1 called with two arguments
        
The badarity error is a specific case of badfun: it happens when you use higher order functions, but you pass them more (or fewer) arguments than they can handle.
system_limit
There are many reasons why a system_limit error can be thrown: too many processes (we’ll get there), atoms that are too long, too many arguments in a function, number of atoms too large, too many nodes connected, etc. To get a full list in details, read the Erlang Efficiency Guide on system limits. Note that some of these errors are serious enough to crash the whole VM.

Raising Exceptions

A stop sign

In trying to monitor the execution of code and protect against logical errors, it’s often a good idea to provoke run-time crashes so problems will be spotted early.

There are three kinds of exceptions in Erlang: errors, throws and exits. They all have different uses (kind of):

Errors

Calling erlang:error(Reason) will end the execution in the current process and include a stack trace of the last functions called with their arguments when you catch it. These are the kind of exceptions that provoke the run-time errors above.

Errors are the means for a function to stop its execution when you can’t expect the calling code to handle what just happened. If you get an if_clause error, what can you do? Change the code and recompile, that’s what you can do (other than just displaying a pretty error message). An example of when not to use errors could be our tree module from the recursion chapter. That module might not always be able to find a specific key in a tree when doing a lookup. In this case, it makes sense to expect the user to deal with unknown results: they could use a default value, check to insert a new one, delete the tree, etc. This is when it’s appropriate to return a tuple of the form {ok, Value} or an atom like undefined rather than raising errors.

Now, errors aren’t limited to the examples above. You can define your own kind of errors too:

1> erlang:error(badarith).
** exception error: bad argument in an arithmetic expression
2> erlang:error(custom_error).
** exception error: custom_error

Here, custom_error is not recognized by the Erlang shell and it has no custom translation such as «bad argument in …», but it’s usable in the same way and can be handled by the programmer in an identical manner (we’ll see how to do that soon).

Exits

There are two kinds of exits: ‘internal’ exits and ‘external’ exits. Internal exits are triggered by calling the function exit/1 and make the current process stop its execution. External exits are called with exit/2 and have to do with multiple processes in the concurrent aspect of Erlang; as such, we’ll mainly focus on internal exits and will visit the external kind later on.

Internal exits are pretty similar to errors. In fact, historically speaking, they were the same and only exit/1 existed. They’ve got roughly the same use cases. So how to choose one? Well the choice is not obvious. To understand when to use one or the other, there’s no choice but to start looking at the concepts of actors and processes from far away.

In the introduction, I’ve compared processes as people communicating by mail. There’s not a lot to add to the analogy, so I’ll go to diagrams and bubbles.

A process 'A' represented by a circle, sending a message (represented by an arrow) to a process 'B' (another circle)

Processes here can send each other messages. A process can also listen for messages, wait for them. You can also choose what messages to listen to, discard some, ignore others, give up listening after a certain time etc.

A process 'A' sending 'hello' to a process 'B', which in turns messages C with 'A says hello!'

These basic concepts let the implementors of Erlang use a special kind of message to communicate exceptions between processes. They act a bit like a process’ last breath; they’re sent right before a process dies and the code it contains stops executing. Other processes that were listening for that specific kind of message can then know about the event and do whatever they please with it. This includes logging, restarting the process that died, etc.

A dead process (a bursting bubble) sending 'I'm dead' to a process 'B'

With this concept explained, the difference in using erlang:error/1 and exit/1 is easier to understand. While both can be used in an extremely similar manner, the real difference is in the intent. You can then decide whether what you’ve got is ‘simply’ an error or a condition worthy of killing the current process. This point is made stronger by the fact that erlang:error/1 returns a stack trace and exit/1 doesn’t. If you were to have a pretty large stack trace or lots of arguments to the current function, copying the exit message to every listening process would mean copying the data. In some cases, this could become unpractical.

Throws

A throw is a class of exceptions used for cases that the programmer can be expected to handle. In comparison with exits and errors, they don’t really carry any ‘crash that process!’ intent behind them, but rather control flow. As you use throws while expecting the programmer to handle them, it’s usually a good idea to document their use within a module using them.

The syntax to throw an exception is:

1> throw(permission_denied).
** exception throw: permission_denied

Where you can replace permission_denied by anything you want (including 'everything is fine', but that is not helpful and you will lose friends).

Throws can also be used for non-local returns when in deep recursion. An example of that is the ssl module which uses throw/1 as a way to push {error, Reason} tuples back to a top-level function. This function then simply returns that tuple to the user. This lets the implementer only write for the successful cases and have one function deal with the exceptions on top of it all.

Another example could be the array module, where there is a lookup function that can return a user-supplied default value if it can’t find the element needed. When the element can’t be found, the value default is thrown as an exception, and the top-level function handles that and substitutes it with the user-supplied default value. This keeps the programmer of the module from needing to pass the default value as a parameter of every function of the lookup algorithm, again focusing only on the successful cases.

A fish that was caught

As a rule of thumb, try to limit the use of your throws for non-local returns to a single module in order to make it easier to debug your code. It will also let you change the innards of your module without requiring changes in its interface.

Dealing with Exceptions

I’ve already mentioned quite a few times that throws, errors and exits can be handled. The way to do this is by using a try ... catch expression.

A try ... catch is a way to evaluate an expression while letting you handle the successful case as well as the errors encountered. The general syntax for such an expression is:

try Expression of
    SuccessfulPattern1 [Guards] ->
        Expression1;
    SuccessfulPattern2 [Guards] ->
        Expression2
catch
    TypeOfError:ExceptionPattern1 ->
        Expression3;
    TypeOfError:ExceptionPattern2 ->
        Expression4
end.

The Expression in between try and of is said to be protected. This means that any kind of exception happening within that call will be caught. The patterns and expressions in between the try ... of and catch behave in exactly the same manner as a case ... of. Finally, the catch part: here, you can replace TypeOfError by either error, throw or exit, for each respective type we’ve seen in this chapter. If no type is provided, a throw is assumed. So let’s put this in practice.

First of all, let’s start a module named exceptions. We’re going for simple here:

-module(exceptions).
-compile(export_all).

throws(F) ->
    try F() of
        _ -> ok
    catch
        Throw -> {throw, caught, Throw}
    end.

We can compile it and try it with different kinds of exceptions:

1> c(exceptions).
{ok,exceptions}
2> exceptions:throws(fun() -> throw(thrown) end).
{throw,caught,thrown}
3> exceptions:throws(fun() -> erlang:error(pang) end).
** exception error: pang

As you can see, this try ... catch is only receiving throws. As stated earlier, this is because when no type is mentioned, a throw is assumed. Then we have functions with catch clauses of each type:

errors(F) ->
    try F() of
        _ -> ok
    catch
        error:Error -> {error, caught, Error}
    end.

exits(F) ->
    try F() of
        _ -> ok
    catch
        exit:Exit -> {exit, caught, Exit}
    end.

And to try them:

4> c(exceptions).
{ok,exceptions}
5> exceptions:errors(fun() -> erlang:error("Die!") end).
{error,caught,"Die!"}
6> exceptions:exits(fun() -> exit(goodbye) end).
{exit,caught,goodbye}

The next example on the menu shows how to combine all the types of exceptions in a single try ... catch. We’ll first declare a function to generate all the exceptions we need:

sword(1) -> throw(slice);
sword(2) -> erlang:error(cut_arm);
sword(3) -> exit(cut_leg);
sword(4) -> throw(punch);
sword(5) -> exit(cross_bridge).

black_knight(Attack) when is_function(Attack, 0) ->
    try Attack() of
        _ -> "None shall pass."
    catch
        throw:slice -> "It is but a scratch.";
        error:cut_arm -> "I've had worse.";
        exit:cut_leg -> "Come on you pansy!";
        _:_ -> "Just a flesh wound."
    end.

Here is_function/2 is a BIF which makes sure the variable Attack is a function of arity 0. Then we add this one for good measure:

talk() -> "blah blah".

And now for something completely different:

7> c(exceptions).
{ok,exceptions}
8> exceptions:talk().
"blah blah"
9> exceptions:black_knight(fun exceptions:talk/0).
"None shall pass."
10> exceptions:black_knight(fun() -> exceptions:sword(1) end).
"It is but a scratch."
11> exceptions:black_knight(fun() -> exceptions:sword(2) end).
"I've had worse."
12> exceptions:black_knight(fun() -> exceptions:sword(3) end).
"Come on you pansy!"
13> exceptions:black_knight(fun() -> exceptions:sword(4) end).
"Just a flesh wound."
14> exceptions:black_knight(fun() -> exceptions:sword(5) end).
"Just a flesh wound."

Monty Python's black knight

The expression on line 9 demonstrates normal behavior for the black knight, when function execution happens normally. Each line that follows that one demonstrates pattern matching on exceptions according to their class (throw, error, exit) and the reason associated with them (slice, cut_arm, cut_leg).

One thing shown here on expressions 13 and 14 is a catch-all clause for exceptions. The _:_ pattern is what you need to use to make sure to catch any exception of any type. In practice, you should be careful when using the catch-all patterns: try to protect your code from what you can handle, but not any more than that. Erlang has other facilities in place to take care of the rest.

There’s also an additional clause that can be added after a try ... catch that will always be executed. This is equivalent to the ‘finally’ block in many other languages:

try Expr of
    Pattern -> Expr1
catch
    Type:Exception -> Expr2
after % this always gets executed
    Expr3
end

No matter if there are errors or not, the expressions inside the after part are guaranteed to run. However, you can not get any return value out of the after construct. Therefore, after is mostly used to run code with side effects. The canonical use of this is when you want to make sure a file you were reading gets closed whether exceptions are raised or not.

We now know how to handle the 3 classes of exceptions in Erlang with catch blocks. However, I’ve hidden information from you: it’s actually possible to have more than one expression between the try and the of!

whoa() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    of
        tequila -> "hey this worked!"
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

By calling exceptions:whoa(), we’ll get the obvious {caught, throw, up}, because of throw(up). So yeah, it’s possible to have more than one expression between try and of

What I just highlighted in exceptions:whoa/0 and that you might have not noticed is that when we use many expressions in that manner, we might not always care about what the return value is. The of part thus becomes a bit useless. Well good news, you can just give it up:

im_impressed() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

And now it’s a bit leaner!

Note: It is important to know that the protected part of an exception can’t be tail recursive. The VM must always keep a reference there in case there’s an exception popping up.

Because the try ... catch construct without the of part has nothing but a protected part, calling a recursive function from there might be dangerous for programs supposed to run for a long time (which is Erlang’s niche). After enough iterations, you’ll go out of memory or your program will get slower without really knowing why. By putting your recursive calls between the of and catch, you are not in a protected part and you will benefit from Last Call Optimisation.

Some people use try ... of ... catch rather than try ... catch by default to avoid unexpected errors of that kind, except for obviously non-recursive code with results that won’t be used by anything. You’re most likely able to make your own decision on what to do!

Wait, there’s more!

As if it wasn’t enough to be on par with most languages already, Erlang’s got yet another error handling structure. That structure is defined as the keyword catch and basically captures all types of exceptions on top of the good results. It’s a bit of a weird one because it displays a different representation of exceptions:

1> catch throw(whoa).
whoa
2> catch exit(die).
{'EXIT',die}
3> catch 1/0.
{'EXIT',{badarith,[{erlang,'/',[1,0]},
                   {erl_eval,do_apply,5},
                   {erl_eval,expr,5},
                   {shell,exprs,6},
                   {shell,eval_exprs,6},
                   {shell,eval_loop,3}]}}
4> catch 2+2.
4

What we can see from this is that throws remain the same, but that exits and errors are both represented as {'EXIT', Reason}. That’s due to errors being bolted to the language after exits (they kept a similar representation for backwards compatibility).

The way to read this stack trace is as follows:

5> catch doesnt:exist(a,4).              
{'EXIT',{undef,[{doesnt,exist,[a,4]},
                {erl_eval,do_apply,5},
                {erl_eval,expr,5},
                {shell,exprs,6},
                {shell,eval_exprs,6},
                {shell,eval_loop,3}]}}
  • The type of error is undef, which means the function you called is not defined (see the list at the beginning of this chapter)
  • The list right after the type of error is a stack trace
  • The tuple on top of the stack trace represents the last function to be called ({Module, Function, Arguments}). That’s your undefined function.
  • The tuples after that are the functions called before the error. This time they’re of the form {Module, Function, Arity}.
  • That’s all there is to it, really.

You can also manually get a stack trace by calling erlang:get_stacktrace/0 in the process that crashed.

You’ll often see catch written in the following manner (we’re still in exceptions.erl):

catcher(X,Y) ->
    case catch X/Y of
        {'EXIT', {badarith,_}} -> "uh oh";
        N -> N
    end.

And as expected:

6> c(exceptions).
{ok,exceptions}
7> exceptions:catcher(3,3).
1.0
8> exceptions:catcher(6,3).
2.0
9> exceptions:catcher(6,0).
"uh oh"

This sounds compact and easy to catch exceptions, but there are a few problems with catch. The first of it is operator precedence:

10> X = catch 4+2.
* 1: syntax error before: 'catch'
10> X = (catch 4+2).
6

That’s not exactly intuitive given that most expressions do not need to be wrapped in parentheses this way. Another problem with catch is that you can’t see the difference between what looks like the underlying representation of an exception and a real exception:

11> catch erlang:boat().
{'EXIT',{undef,[{erlang,boat,[]},
                {erl_eval,do_apply,5},
                {erl_eval,expr,5},
                {shell,exprs,6},
                {shell,eval_exprs,6},
                {shell,eval_loop,3}]}}
12> catch exit({undef, [{erlang,boat,[]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}). 
{'EXIT',{undef,[{erlang,boat,[]},
                {erl_eval,do_apply,5},
                {erl_eval,expr,5},
                {shell,exprs,6},
                {shell,eval_exprs,6},
                {shell,eval_loop,3}]}}

And you can’t know the difference between an error and an actual exit. You could also have used throw/1 to generate the above exception. In fact, a throw/1 in a catch might also be problematic in another scenario:

one_or_two(1) -> return;
one_or_two(2) -> throw(return).

And now the killer problem:

13> c(exceptions).
{ok,exceptions}
14> catch exceptions:one_or_two(1).
return
15> catch exceptions:one_or_two(2).
return

Because we’re behind a catch, we can never know if the function threw an exception or if it returned an actual value! This might not really happen a whole lot in practice, but it’s still a wart big enough to have warranted the addition of the try ... catch construct in the R10B release.

Try a try in a tree

To put exceptions in practice, we’ll do a little exercise requiring us to dig for our tree module. We’re going to add a function that lets us do a lookup in the tree to find out whether a value is already present in there or not. Because the tree is ordered by its keys and in this case we do not care about the keys, we’ll need to traverse the whole thing until we find the value.

The traversal of the tree will be roughly similar to what we did in tree:lookup/2, except this time we will always search down both the left branch and the right branch. To write the function, you’ll just need to remember that a tree node is either {node, {Key, Value, NodeLeft, NodeRight}} or {node, 'nil'} when empty. With this in hand, we can write a basic implementation without exceptions:

%% looks for a given value 'Val' in the tree.
has_value(_, {node, 'nil'}) ->
    false;
has_value(Val, {node, {_, Val, _, _}}) ->
    true;
has_value(Val, {node, {_, _, Left, Right}}) ->
    case has_value(Val, Left) of
        true -> true;
        false -> has_value(Val, Right)
    end.

The problem with this implementation is that every node of the tree we branch at has to test for the result of the previous branch:

A diagram of the tree with an arrow following every node checked while traversing the tree, and then when returning the result

This is a bit annoying. With the help of throws, we can make something that will require less comparisons:

has_value(Val, Tree) -> 
    try has_value1(Val, Tree) of
        false -> false
    catch
        true -> true
    end.

has_value1(_, {node, 'nil'}) ->
    false;
has_value1(Val, {node, {_, Val, _, _}}) ->
    throw(true);
has_value1(Val, {node, {_, _, Left, Right}}) ->
    has_value1(Val, Left),
    has_value1(Val, Right).

The execution of the code above is similar to the previous version, except that we never need to check for the return value: we don’t care about it at all. In this version, only a throw means the value was found. When this happens, the tree evaluation stops and it falls back to the catch on top. Otherwise, the execution keeps going until the last false is returned and that’s what the user sees:

A diagram of the tree with an arrow following every node checked while traversing the tree, and then skipping all the nodes on the way back up (thanks to a throw)

Of course, the implementation above is longer than the previous one. However, it is possible to realize gains in speed and in clarity by using non-local returns with a throw, depending on the operations you’re doing. The current example is a simple comparison and there’s not much to see, but the practice still makes sense with more complex data structures and operations.

That being said, we’re probably ready to solve real problems in sequential Erlang.

  • < Previous
  • Index
  • Next >

Dear %username%,

Let’s continue learning Erlang for the little ones.

In the previous post we’ve reviewed the way of declaring functions and uniting them into modules. In this post we’ll consider the syntax of functions in details.

The source code of all the examples in this post is here.

Pattern Matching

To begin with, let’s write a function that will greet a user and the welcome text will depend on user’s gender. In the form of the pseudo code our function will look like the following:

function greet(Gender,Name)
  if Gender == male then
    print("Hello, Mr. %s!", Name)
  else if Gender == female then
    print("Hello, Mrs. %s!", Name)
  else
    print("Hello, %s!", Name)
end

If we use pattern matching instead of the classic if then else construction, we can save plenty of the template code. That’s how this function will look in Erlang when using pattern matching:

greet(male, Name) -> 
    io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
    io:format("Hello, Mrs. ~s!", [Name]).

We use io:format() function for the formatted output to the terminal. Here we used pattern matching in the function argument list. This allowed us to assign the input values and chose that part of the function to be executed at the same time. Why would we first assign values and compare them in the function body later if we can do it concurrently and in a “more declarative” style?

In general terms such declaration looks like the following:

fnct_name(X) ->
  Extpression;
fnct_name(Y) ->
  Expression;
fnct_name(_) ->
  Expression.

We declare each branch as a full-fledged function, but there’s a semicolon at the end of it. Then we declare the next function and use a period after the last part.

Pay attention to the last pattern. What’s going to happen if we call greet() function and pass the unknown gender? We’ll get an exception saying that the input parameters do not match any of the patterns:

1> chapter03:greet(someOther, "Haru").
** exception error: no function clause matching chapter03:greet(someOther, "Haru")

That’s why it’s important to include declaration that will match any value. Besides, it should always be the last one. Otherwise we will never process the patterns declared after it.

Let’s rewrite our function so that it would process properly the incorrect input values:

greet(male, Name) ->
    io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
    io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
    io:format("Hello, ~s!", [Name]).

But pattern matching in declaring functions brings much more benefits than just reducing the code length. Let’s recall lists. A list consists of a head and the remaining part. Let’s write two functions that will return the first and the second elements of a list.

first([X|_]) -> 
    X.
second([_,X|_) -> 
    X.

Quite simple, isn’t it? There’s another interesting method based on the fact that we can assign variables just once.

same(X,X) ->
    true;
same(_,_) ->
    false.

This function returns true if its arguments are the same. Otherwise it returns false. How does it work? When calling chapter03::same(one, two) function we assign one value to X variable. Then we try to assign two to this variable. Since the value has already been assigned, the try fails and the template is rejected as unmatched. In the second case we do not indicate, which variables we should assign values to. Therefore, the pattern is matched and the function returns false. But if we pass the same values chapter03:same(3, 3), the first pattern will match and the function will return true.

Guards

The pattern matching has one big drawback. It’s insufficiently expressive. For instance, we can not specify the data type, a range and some other things with the help of it. Each pattern is an individual case. There are Guard clauses in Erlang to solve this problem. First of all, let’s write a function that will accept our body mass index and estimate our condition. The body mass of a person is his weight divided by the squared height.

bmi_tell(Bmi) when Bmi =< 18.5 ->
    "You're underweight.";
bmi_tell(Bmi) when Bmi =< 25 ->
   "You're supposedly normal.";
bmi_tell(Bmi) when Bmi =< 30 ->
   "You're fat.";
bmi_tell(_) ->
    "You're very fat.".

When referring to the function, we check the first condition after when (Bmi =< 18.5). If the expression returns true, the appropriate code path will be executed. Otherwise we perform the check of the next condition and do the same till the end of the code. At the end we’ll add a condition that will fit any value.

In the general case declaring of the function using guards looks like the following:

fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_1 ->
    Expression;
fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_2 ->
    Expression.

We are not restricted to just one expression in the condition. We can perform multiple checks. If we want both conditions to pass (analog of andalso) to perform the check, we should place a comma (,) between them.

lucky_number(X) when 10 < X, X > 20 ->
    true;
lucky_number(_) ->
    false.

If at least one condition (analog of orelse) is enough, we separate them with a semicolon (;).

lucky_atom(X) when X == atom1; X == anot2 ->
    true;
lucky_atom(_) ->
    false.

We can also use functions as guards. Here’s the function that divides one number by another one, but before it checks that the passed arguments were numbers and Y was not zero.

safe_division(X, Y) when is_integer(X), is_integer(Y), Y /= 0 ->
    X / Y;
safe_division(_, _) ->
    false.

Expressions

If Statement

Apart from the considered above expressions, there’s also a regular conditional if expression in Erlang. Let’s rewrite bmi_tell() function using it.

if_bmi_tell(Bmi) ->
    if Bmi =< 18.5 -> "You're underweight.";
       Bmi =< 25   -> "You're supposedly normal.";
       Bmi =< 30   -> "You're fat.";
       true        -> "You're very fat."
    end.

In the general case conditional if expression looks the following way:

if Rule_1 -> Expression;
   Rule_2 -> Expression;
   ...
   true -> Expression;
end.

Reminding you that unlike in Haskell, standoffs are of no importance here, except for being decorative. We’ve formatted the code just to make it clearer. You’re free to format it the way you like.

Rule_1 and Rule_2 expressions here are either one or more conditions. If the condition evaluates successfully, we’ll execute the Expression code block (it may contain one or several commands). You can have as many as you want of such branches.

Please pay attention to the last block: true -> «You’re very fat.». What kind of condition is it? It’s the alternative of orelse statement. The code block following it will be evaluated if none of the conditions pass the check. You should not forget to use this statement, since any expression in Erlang should return a result. If we do not describe this block, the module will compile anyway. But when the execution thread checks all conditions and will not find true block, it will generate an exception.

** exception error: no true branch found when evaluating an if expression
     in function  chapter03:if_bmi_tell/1 

case… of Expression

Besides if, there’s one more conditional expression in Erlang – case of. It’s like a function included into another one. It allows making a choice not only on the basis of a condition, but by using pattern matching and guards. It will look like the following way:

case Rule of
    Val_1 -> Expression;
    Val_2 -> Expression
    ...
    Val_n -> Expression

Rule here is a variable or an expression, the result of which we will check later. Then follow the already familiar “condition-expression” blocks (Val_1 -> Expression;). In conditions we can use pattern matching and guard expressions, which make this construction extremely flexible.

As an example, let’s write a function that will accept a tuple consisting of the temperature and the name of the measurement scale. We will estimate it on the basis of these data.

assessment_of_temp(Temp) ->
    case Temp of
        {X, celsius} when 20 =< X, X =< 45 ->
            'favorable';
        {X, kelvin} when 293 =< X, X =< 318 ->
            'scientifically favorable';
        {X, fahrenheit} when 68 =< X, X =< 113 ->
            'favorable in the US';
        _ ->
            'not the best tempperature'
  end.

When executing this function, our tuple will be compared to Temp variable and then we will find another pattern this tuple will fit. After that we will perform a check by guard expressions. If it’s successful – the function will return the appropriate string.

As well as before, we add an expression to the end. It will intercept all unsuitable variants.

As you can see, we can move case of expression almost in full to the area of declaring the function. So what is the preferable place to locate conditions? The answer is simple: place it wherever you like. Differences between these two constructions are minimal. Therefore, you should use the variant that is more clear for reading.

Summary

In this article we’ve reviewed the way of controlling the execution thread inside functions. We’ve also found out that there are several constructions for that purpose and learnt the way of using them. We also got familiar with guard expressions.

In the next article we’ll review the type system in Erlang.

Thank you for reading the post. Hope it’s been interesting. Please, share it with your friends.

Впервые было опубликовано в журнале «Системный администратор»
#9 за 2009 год

Продолжаем изучать язык программирования Erlang.

В статье «Знакомьтесь, Erlang» были рассмотрены особенности
языка, основные типы данных, переменные и сравнение с шаблонами. В этой статье
мы продолжим изучение Erlang и начнем писать программы, которые выполняются
последовательно.

Модули и функции

В Erlang программы строятся из функций, которые вызывают одна другую. Функции,
в свою очередь, группируются и определяются внутри модулей. Исходный код
модулей Erlang хранится в файлах с расширением .erl, при этом имя модуля должно
быть таким же, как и имя файла (без расширения). Перед тем как запустить модуль
его нужно скомпилировать. Компилированные модули хранятся в файлах с
расширением .beam.

Определение функции состоит из заголовка и тела функции. Заголовок функции
состоит из имени функции, которое является атомом, за которым в скобках следуют
формальные параметры функции. Количество параметров функции называется арностью
(arity). Функции в Erlang уникально определяются именем модуля, именем функции
и арностью, то есть две функции, находящиеся в одном модуле с одинаковыми
именами, но с разной арностью являются разными функциями. Стрелка ->
отделяет заголовок функции от ее тела.

Как уже было написано в прошлой статье, мы не можем определять функции в
интерактивной оболочке Erlang. Давайте напишем наш первый модуль и рассмотрим
его подробнее. Создадим файл с именем geometry.erl:

-module(geometry).
-export([area/1]).

% Функция для вычисления площади
area({square, Side}) ->
    Side * Side;
area({rectangle, Width, Height}) ->
    Width * Height;
area({circle, Radius}) ->
    3.1415926 * Radius * Radius.

В начале модуля находятся директивы модуля в следующем формате:
-директива(значение). Директива module описывает имя модуля, которое
должно совпадать с именем файла (без расширения). Директива export
описывает экспортируемые функции (которые будут доступны снаружи модуля) в виде
списка в формате имя/арность. В данном случае наш модуль называется geometry
и экспортирует одну функцию area с одним аргументом. Заметьте, что каждая
директива заканчивается точкой.

Строки, начинающиеся со знака %, являются комментариями, как уже было
рассмотрено в прошлой статье.

После комментария идет определение функции. В данном случае определение функции
состоит из трех предложений разделенных знаком ; и последнее выражение,
завершающее определение функции, заканчивается точкой. При вызове функции
переданные аргументы последовательно сравниваются с шаблонами формальных
параметров, пока не будет найдено нужное предложение. После того как найдено
нужно предложение, выполняется выражение, находящееся в теле этого предложения
и возвращается результат этого выражения. В данном случае шаблоны параметров
для предложений — взаимоисключающие, и порядок предложений не имеет значения,
но в других случаях порядок предложений может быть важен.

Попробуем выполнить функцию из нашего модуля в интерактивном режиме:

1> c(geometry).
{ok,geometry}
2> geometry:area({circle, 20}).
1256.63704
3> geometry:area({square, 20}).
400
4> geometry:area({rectangle, 10, 20}).
200
5> geometry:area({triangle, 10, 20, 30}).
** exception error: no function clause matching geometry:area({triangle,10,20,30})

В первой строке мы использовали функцию c(), определенную в оболочке для
компиляции нашего модуля. Эта функция возвращает кортеж {ok, geometry}, что
говорит об успешной компиляции модуля. Вне оболочки модуль может быть
скомпилирован с помощью утилиты erlc.

После компиляции мы делаем несколько вызовов нашей функции, используя нотацию
модуль:функция. Мы передаем различные кортежи в качестве аргументов, и
выполняется тело того предложения функции, с шаблоном которого совпадает
переданный аргумент. В пятой строке мы передали кортеж, который не совпадает ни
с одним из шаблонов в определении функции, и получили ошибку.

Более сложный пример

Теперь рассмотрим более сложный пример с использованием ввода/вывода и
рекурсии:

-module(persons).
-export([person_list/1]).

person_list(Persons) ->
    person_list(Persons, 0).

person_list([{person, FirstName, LastName} | Persons], N) ->
    io:format("~s ~s~n", [FirstName, LastName]),
    person_list(Persons, N + 1);
person_list([], N) ->
    io:format("Total: ~p~n", [N]).

Новый модуль называется persons и экспортирует функцию person_list/1 (с одним
аргументом). Заметьте, что в модуле так же есть функция person_list/2 (с двумя
аргументами), но в данном случае она будет видна только внутри модуля. Функция
person_list/1 вызывает вспомогательную функцию person_list/2.

Функции person_list/2 необходимо передать два аргумента: список пользователей и
начальное значение для аргумента-счетчика. Функция person_list/2 состоит из
двух предложений. В первом предложении функции мы отделяем первый элемент
списка пользователей (заметьте, что мы отделяем имя и фамилию прямо в шаблоне
аргумента). Затем используется функция format из библиотечного модуля io,
что бы вывести имя и фамилию пользователя на экран, и после этого мы вызываем
(рекурсивно) person_list/2 с оставшимися пользователями и увеличенным счетчиком
пользователей.

Библиотечной функции io:format/2 нужно передать два аргумента — формат вывода и
список аргументов. В данном случае формат вывода состоит из двух шаблонов для
вывода строк ~s и перевода строки ~n. Модуль io содержит большое
количество функций для работы со стандартным вводом/выводом.

Второе (и последнее) предложение функции print_list/2 вызывается, когда список
пользователей оказывается пустым (это происходит при окончании вывода
пользователей) и выводит общее количество выведенных имен пользователей с
использованием аргумента-счетчика. Во втором предложении мы так же используем
новый шаблон для io:format/2 — ~p, выводящий аргумент в формате в котором
это делает оболочка.

Давайте попробуем использовать наш модуль в интерактивной сессии:

1> c(persons).
{ok,persons}
2> persons:person_list([]).
Total: 0
ok
3> persons:person_list([{person, "Joe", "Armstrong"}]).
Joe Armstrong
Total: 1
ok
4> persons:person_list([{person, "Joe", "Armstrong"},
4> {person, "Mike", "Williams"},
4> {person, "Robert", "Virding"}]).
Joe Armstrong
Mike Williams
Robert Virding
Total: 3
ok

Как мы видим, функция работает, как мы и ожидали.

Ограничители

Часто сравнения с шаблоном для функций бывает недостаточно и здесь на помощь
приходят ограничители (guards), которые позволяют использовать простые тесты и
сравнения переменных вместе с шаблонами. Кроме функций, ограничители можно
использовать в некоторых других конструкциях условного выполнения, которые мы
рассмотрим ниже, например, в конструкции case. Для функций ограничители
должны быть расположены перед символами ->, разделяющими заголовок и тело
функции. Например, можно написать функцию для нахождения максимального значения
следующим образом:

max(X, Y) when X > Y ->
    X;
max(_X, Y) ->
    Y.

В первом предложении функции используются ограничители, которые начинаются со
слова when. Первое предложение выполняется только в случае если X > Y,
иначе выполняется второе предложение. Во втором предложении первая переменная
называется _X — использование подчеркивания в начале имени переменной позволяет
избежать предупреждения о неиспользуемой переменной, хотя этим нужно
пользоваться с осторожностью, что бы не пропустить ошибочные ситуации.

Ограничители представляют собой либо одно условное выражение, которое
возвращает true/false, либо могут быть записаны как составное выражение
следующим образом:

  • Последовательность ограничителей разделенных точкой с запятой ; истинна,
    если хотя бы один из ограничителей в последовательности возвращает true;
  • Последовательность ограничителей разделенных запятой , истинна только,
    если все ограничители в последовательности возвращают true;

Не все выражения доступны для использования в качестве ограничителей для
избежания возможных побочных эффектов. Вот список доступных выражений:

  • Атом true (истина);
  • Различные константы и переменные. В ограничителях все они представляют из
    себя ложные значения;
  • Функции для тестирования типов данных и некоторые встроенные функции,
    например: is_atom, is_boolean, is_tuple, size и др.;
  • Сравнение терминов, например =:=, =/=, <, > и т.п.;
  • Арифметические операции;
  • Булевские операции;
  • Булевские операции с короткой схемой вычисления (short-circuit);

Условное выполнение

В Erlang есть три формы условного выполнения, которые в большинстве случаев
могут быть взаимозаменяемы. С первой формой мы уже познакомились при изучении
функций — это использование сравнения с шаблонами (и ограничителей) в
определении функций. Ниже мы рассмотрим еще две формы условного выполнения —
конструкции case и if.

В конструкции case сначала выполняется выражение между case и of, и затем
результат последовательно сравнивается с шаблонами. Вместе с шаблонами так же
можно использовать и ограничители. Рассмотрим пример:

case is_boolean(Variable) of
    true ->
        1;
    false ->
        0
end

В этом (достаточно надуманном) примере в качестве выражения case … of
выполняется функция is_boolean и шаблонами служат true и false. Два
предложения разделены точкой с запятой, и конструкция заканчивается ключевым
словом end. В случае если подходящий шаблон не будет найден, то будет выкинуто
исключение.

Конструкция if использует только ограничители, которые последовательно
выполняются, пока не будет получено значение истина:

if
    X > Y ->
        true;
    true ->
        false
end

В данном случае ограничитель true действует как конструкция «иначе» в
других языках, то есть значением if будет false если X =< Y. В случае если ни
один из ограничителей не даст значения истина будет выкинуто исключение.

Анонимные функции

Анонимные функции определяются с ключевым словом fun и похожи на
определение обычных функций за исключением отсутствия имени. Рассмотрим
пример:

-module(times).
-export([times/1]).


times(N) ->
    fun
        (X) when is_number(X) ->
            X * N;
        (_) ->
            erlang:error(number_expected)
    end.

Здесь функция times является функцией высшего порядка, так как возвращает
другую функцию. Определение анонимной функции между ключевыми словами fun и
end состоит из двух предложений. В первом предложении с помощью ограничителя с
функцией is_number мы определяем, что передано число (число может быть
целым, или вещественным) и умножаем его на аргумент, переданный в основную
функцию при создании нашей анонимной функции. Во втором предложении мы немного
забегаем вперед и используем генерацию исключений, которая будет подробнее
рассмотрена в следующем разделе. Кроме этого шаблон второго выражения
использует знак подчеркивания, говорящий, что нам абсолютно не важна эта
переменная.

Попробуем нашу функцию:

1> c(times).
{ok,times}
2> N2 = times:times(2).
#Fun<times.0.120017377>
3> N2(4).
8
4> N10 = times:times(10).
#Fun<times.0.120017377>
5> N10(4).
40

В строке 2 мы используем функцию times:times для получения функции умножающей
значение на 2 и в строке 4 создается функция, умножающая значение на 10.

Стандартный модуль lists экспортирует некоторое количество функций, которые
принимают функции в качестве аргументов, например, функция lists:map вызывает
функцию с каждым элементом списка по очереди:

6> Double = times:times(2).
#Fun<times.0.120017377>
7> lists:map(Double, [1, 2, 3, 4]).
[2,4,6,8]

Обработка исключений

Обычно исключения генерируются в случае обнаружения ошибки. Наиболее часто
встречающиеся типы исключений — это исключения, связанные со сравнением
шаблонов (мы уже встречались с такими исключениями выше) и исключения,
связанные с неверными аргументами функций. Теперь давайте рассмотрим, как можно
перехватить и обработать различные типы исключений и как генерировать
исключения самостоятельно.

Исключения в своем коде можно создать, используя одну из встроенных функций:

  • exit(Why) — эта функция используется, когда нужно действительно прервать
    выполнение текущего процесса. Если это исключение не перехватывается, то всем
    процессам, присоединенным к данному, посылается сообщение {‘EXIT’, Pid, Why}.
    Подробнее соединенные процессы будут рассматриваться в одной из следующих
    статей.
  • throw(Why) — эта функция используется для генерации исключения, которое
    вызывающая сторона, скорее всего, захочет перехватить. Таким образом, мы
    документируем, что наша функция может генерировать данное исключение. В
    большинстве случаев рекомендуется не выкидывать это исключение за пределы
    модуля.
  • erlang:error(Why) — эта функция используется для аварийных ситуаций,
    которые не ожидает вызывающая сторона.

Теперь разберемся, как эти исключения обрабатывать. В Erlang существует два
способа обработки исключений — выражение catch и конструкция
try/catch.

Выражение catch возвращает либо значение под-выражения, либо информацию об
ошибке в зависимости от типа ошибки. Рассмотрим на примере:

1> catch 2 + 2.
4
2> catch 2 + a.
{'EXIT',{badarith,[{erlang,'+',[2,a]},
                   {erl_eval,do_apply,5},
                   {erl_eval,expr,5},
                   {shell,exprs,6},
                   {shell,eval_exprs,6},
                   {shell,eval_loop,3}]}}
3> catch exit("Exit").
{'EXIT',"Exit"}
4> catch throw("Throw").
"Throw"
5> catch erlang:error("Error").
{'EXIT',{"Error",
         [{erl_eval,do_apply,5},
          {erl_eval,expr,5},
          {shell,exprs,6},
          {shell,eval_exprs,6},
          {shell,eval_loop,3}]}}

В первой строке мы пробуем catch с выражением 2 + 2, которое успешно
выполняется, возвращая 4. Во второй строке делается попытка сложить целое и
атом и catch возвращает описание ошибки в виде {‘EXIT’, {ошибка, стек
вызовов}}. Следующие три строки показывают возвращаемые значения в зависимости
от способа генерации исключений. Часто catch используют совместно с
конструкцией case для обработки ошибок в выражениях.

Конструкция try/catch позволяет обрабатывать только необходимые для
обработки типы ошибок и даже может быть совмещена с конструкцией похожей на
case. Рассмотрим пример:

try 2 + a of
    Value ->
        ok
catch
    error:_ ->
        error
end.

В этом примере мы пытаемся выполнить выражение 2 + a и шаблоны между of …
catch соответствуют шаблонам в выражении case. Шаблоны между catch … end (в
которых так же можно использовать ограничители) используются для сопоставления
с ошибками, где ошибка описывается как тип:значение.

Библиотечные модули

В состав Erlang включено большое количество стандартных библиотечных модулей.
Подробное описание модулей, идущих вместе с языком, можно найти по следующей
ссылке: http://erlang.org/doc/man_index.html. Ниже описываются наиболее
полезные модули:

  • erlang — модуль, который содержит большинство встроенных функции Erlang.
    Большинство функций из этого модуля доступны без указания имени модуля, но к
    остальным нужно обращаться только по полному имени, с указанием модуля;
  • file — интерфейс к файловой системе, содержащий функции для работы с
    файлами;
  • io — интерфейс к стандартному серверу ввода/вывода. Содержит функции для
    чтения/записи файлов, в том числе для стандартных устройств ввода/вывода;
  • lists — скорее всего самый используемый модуль, содержит функции для
    работы со списками;
  • math — модуль, содержащий стандартные математические функции;
  • string — содержит функции для работы со строками;

Chapter 4. Logic and Recursion

So far, Erlang seems logical but fairly simple. Pattern matching controls the flow through a program, and requests that match a form return certain responses. While this is enough to get many things done, there are times when you’ll want more powerful options, especially as you start working with larger and more complicated data structures.

Logic Inside of Functions

Pattern matching and guards are powerful tools, but there are times when it’s much easier to do some comparisons inside of a function clause instead of creating new functions. Erlang’s designers agreed, and created two constructs for evaluating conditions inside of functions: the case expression and the less frequently used if expression.

The case construct lets you use pattern matching and guards inside of a function clause. It reads most clearly when a single value (or set of values) needs to be compared with multiple possibilities. The if construct evaluates only a series of guards, without pattern matching. The if construct tends to produce more readable code in situations where the multiple possibilities are specified by combinations of different values.

Both constructs return a value your code can capture.

Evaluating Cases

The case construct lets you perform pattern-matching inside of your function clause. If you found the multiple function clauses of Example 3-2 hard to read, you might prefer to create a version that looks like Example 4-1, which you can find in ch04/ex1-case.

Example 4-1. Moving pattern matching inside the function

-module(drop).
-export([fall_velocity/2]).


fall_velocity(Planemo, Distance) when Distance >= 0  ->
  case Planemo of
    earth -> math:sqrt(2 * 9.8 * Distance);
    moon ->  math:sqrt(2 * 1.6 * Distance);
    mars ->  math:sqrt(2 * 3.71 * Distance)  % no closing period!
  end.

The case construct will compare the atom in Planemo to the values listed, going down the list in order. It won’t process beyond the match it finds. The case construct will return the result of different calculations based on which atom is used, and because the case construct returns the last value in the function clause, the function will return that value as well.

Note

You can use the underscore (_) for your pattern match if you want a choice that matches “everything else.” However, you should always put that last—nothing that comes after that will ever be evaluated.

The results should look familiar:

1> c(drop).
{ok,drop}
2> drop:fall_velocity(earth,20).
19.79898987322333
3> drop:fall_velocity(moon,20).
8.0
4> drop:fall_velocity(mars,20).
12.181953866272849
5> drop:fall_velocity(mars,-20).
** exception error: no function clause matching
      drop:fall_velocity(mars,-20) (drop.erl, line 5)

The case construct switches among planemos, while the guard clause on the function definition keeps out negative distances, producing (rightly) the error on line 5. This way the guard needs to appear only once.

You can also use the return value from the case construct to reduce duplicate code and make the logic of your program clearer. In this case, the only difference between the calculations for earth, moon, and mars is a gravitational constant. Example 4-2, which you can find in ch04/ex2-case, shows how to make the case construct return the gravitational constant for use in a single calculation at the end.

Example 4-2. Using the return value of the case construct to clean up the function

-module(drop).
-export([fall_velocity/2]).


fall_velocity(Planemo, Distance) when Distance >= 0  ->
  Gravity = case Planemo of
    earth -> 9.8;
    moon ->  1.6;
    mars ->  3.71
  end,  % note comma - function isn't done yet

  math:sqrt(2 * Gravity * Distance).

This time, the Gravity variable is set to the return value of the case construct. Note the comma after the end. This function isn’t done yet! Commas let you separate constructs inside of function declarations. The now more readable formula math:sqrt(2 * Gravity * Distance). is the last line of the function, and the value it produces will be the return value.

You can also use guards with a case statement, as shown, perhaps less than elegantly, in Example 4-3, which is in ch04/ex3-case. This might make more sense if there were different planemos with different rules about distances.

Example 4-3. Moving guards into the case statement

-module(drop).
-export([fall_velocity/2]).

fall_velocity(Planemo, Distance)  ->
  Gravity = case Planemo of
    earth when Distance >= 0 ->  9.8;
    moon  when Distance >= 0 ->  1.6;
    mars  when Distance >= 0 ->  3.71
  end,  % note comma - function isn't done yet

  math:sqrt(2 * Gravity * Distance).

This produces similar results, except that the error message at the end changes from no function clause matching drop:fall_velocity(mars,-20) to no case clause matching mars in function drop:fall_velocity/2:

1> c(drop).
{ok,drop}
2> drop:fall_velocity(mars,20).
12.181953866272849
3> drop:fall_velocity(mars,-20).
** exception error: no case clause matching mars
     in function  drop:fall_velocity/2 (drop.erl, line 6)

The error is correct, in that the case construct is trying to match mars, but misleading because the problem isn’t with mars but rather with the guard that’s checking the Distance variable. If Erlang tells you that your case doesn’t match but a match is obviously right there in front of you, check your guard statements.

If This, Then That

The if construct is broadly similar to the case statement, but without the pattern matching. This allows you to write a catch-all clause—a guard matching true at the end if you would like, and often makes it easier to express logic based on broader comparisons than simple matching.

Suppose, for example, that the precision of the fall_velocity function is too much. Instead of an actual speed you’d like to describe the speed produced by dropping from a tower of a given height. You can add an if construct that does that to the earlier code from Example 4-2, as shown in Example 4-4, in ch04/ex4-if.

Example 4-4. Adding an if construct to convert numbers into atoms

-module(drop).
-export([fall_velocity/2]).


fall_velocity(Planemo, Distance) when Distance >= 0  ->
  Gravity = case Planemo of
    earth -> 9.8;
    moon ->  1.6;
    mars ->  3.71
  end,

  Velocity = math:sqrt(2 * Gravity * Distance),

  if
    Velocity == 0 -> 'stable';
    Velocity < 5 -> 'slow';
    Velocity >= 5, Velocity < 10 -> 'moving';
    Velocity >= 10, Velocity < 20 -> 'fast';
    Velocity >= 20 -> 'speedy'
  end.

This time, the if construct returns a value (an atom describing the velocity) based on the many guards it includes. Because that value is the last thing returned within the function, that becomes the return value of the function.

Note

The commas in the if behave like the and operator.

The results are a little different from past trials:

1> c(drop).
{ok,drop}
2> drop:fall_velocity(earth,20).
fast
3> drop:fall_velocity(moon,20).
moving
4> drop:fall_velocity(mars,20).
fast
5> drop:fall_velocity(earth,30).
speedy

If you want to capture the value produced by the if construct and use it for something else, you can. Example 4-5, in ch04/ex5-if, sends a warning to standard output (in this case the Erlang shell) if you drop an object too fast.

Example 4-5. Sending an extra warning if the velocity is too high

-module(drop).
-export([fall_velocity/2]).


fall_velocity(Planemo, Distance) when Distance >= 0  ->
  Gravity = case Planemo of
    earth -> 9.8;
    moon ->  1.6;
    mars ->  3.71
  end,

  Velocity = math:sqrt(2 * Gravity * Distance),

  Description = if
    Velocity == 0 -> 'stable';
    Velocity < 5 -> 'slow';
    (Velocity >= 5) and (Velocity < 10) -> 'moving';
    (Velocity >= 10) and (Velocity < 20) -> 'fast';
    Velocity >= 20 -> 'speedy'
  end,

  if
    (Velocity > 40) -> io:format("Look out below!~n") ;
    true -> true
  end,

  Description.

The new (second) if clause checks the Velocity variable to see if it’s above 40. If it is, it calls io:format, which creates a side effect: a message on the screen. However, every if must find some true statement or it will report an error in those cases when nothing matches. Here, you could add an explicit case matching when the Velocity is less than or equal to 40. In many cases, however, it won’t matter. The true -> true line is a catch-all that returns true no matter what reaches it. After the if concludes, the single line Description. returns the contents of the Description variable from the function.

Note

The catchall approach works in cases where you only want to test for a subset of cases among a complicated set of possibilities. In cases as simple as this example, however, it’s probably cleaner to create a more explicit test.

The function produces an extra result—the message—when the distance is large enough (and the planemo’s gravity strong enough) to produce a velocity faster than 40 meters per second:

1> c(drop).
{ok,drop}
2> drop:fall_velocity(earth,10).
fast
3> drop:fall_velocity(earth,200).
Look out below!
speedy

Variable Assignment in case and if Constructs

Every possible path created in a case or if statement has the opportunity to bind values to variables. This is usually a wonderful thing, but could let you create unstable programs by assigning different variables in different clauses. This might look something like Example 4-6, which you can find in ch04/ex6-broken.

Example 4-6. A badly broken if construct

-module(broken).
-export([bad_if/1]).

bad_if(Test_val) ->

  if
    Test_val < 0 ->  X = 1;
    Test_val >= 0 -> Y = 2
  end,

  X+Y.

In theory, after the case or if is over, the program might crash because of unbound variables. However, Erlang won’t let you get that far:

1> c(broken).
broken.erl:11: variable 'X' unsafe in 'if' (line 6)
broken.erl:11: variable 'Y' unsafe in 'if' (line 6)
error

The compilation errors turn up where your program actually uses the variables. The Erlang compiler double-checks to make sure that the variables it’s about to put to use are properly defined. It won’t let you compile something this broken.

You can bind variables in an if or case construct. You have to define all of the variables in every single clause, however. If you’re defining only one variable, it’s also much cleaner to bind the return value of the if or case clause to a variable instead of defining that variable in every clause.

The Gentlest Side Effect: io:format

Up until Example 4-5, all of the Erlang examples you’ve seen focused on a single path through a group of functions. You put an argument or arguments in, and got a return value back. That approach is the cleanest way to do things: you can count on things that worked before to work again because there’s no opportunity to muck up the system with leftovers of past processing.

Example 4-5 stepped outside of that model, creating a side effect that will linger after the function is complete. The side effect is just a message that appears in the shell (or in standard output when you start running Erlang outside of the shell). Applications that share information with multiple users or keep information around for longer than a brief processing cycle will need stronger side effects, like storing information in databases.

Erlang best practice suggests using side effects only when you really need to. An application that presents an interface to a database, for example, really will need to read and write that database. An application that interacts with users will need to put information on the screen (or other interface) so that users can figure out what they’re expected to do.

Side effects are also extremely useful for tracing logic when you are first starting out. The simplest way to see what a program is doing, before you’ve learned how to use Erlang’s built-in tracing and debugging tools for processes, is to have the program report its status at points you consider interesting. This is not a feature you want to leave in shipping code, but when you’re getting started, it can give you an easily understandable window into your code’s behavior.

The io:format function lets you send information to the console, or, when you’re eventually running code outside of the console, to other places. For now, you’ll just use it to send messages from the program to the console. Example 4-5 showed the simplest way to use io:format, just printing a message it takes in double quotes:

io:format("Look out below!~n") ;

The ~n represents a newline, telling the console to start any new messages it sends at the beginning of the next line. It makes your results look a bit neater.

The more typical way to use io:format includes two arguments: a double-quoted formatting string, and a list of values that can be included in the string. In this case (which you can see in ch04/ex7-format), it might look like the following:

io:format("Look out below!  ~w is too high.~n", [Distance]) ;

or:

io:format("Look out below!  ~w is too high on ~w.~n", [Distance, Planemo]) ;

io:format/2 offers many formatting options beyond ~w and ~n. You’ll encounter them as they become necessary, but if you’re impatient, there’s a list in Appendix A. You may also want to explore the section on error logging in Chapter 9, if you find yourself using io:format for tasks that might be helped by more sophisticated logging tools.

Note

Erlang flatly prohibits operations that could cause side effects in guard expressions. If side effects were allowed in guards, then any time a guard expression was evaluated—whether it returned true or false—the side effect would happen. io:format wouldn’t likely do anything terrible, but these rules mean that it too is blocked from use in guard expressions.

Simple Recursion

Because variables can’t change values, the main tool you’ll use to repeat actions is recursion: having a function call itself until it’s (hopefully) reached a conclusion. This can sound complicated, but it doesn’t have to be.

There are two basic kinds of useful recursion. In some situations, you can count on the recursion to reach a natural end. The process runs out of items to work on, or reaches a natural limit. In other situations, there is no natural end, and you need to keep track of the result so the process will end. If you can master these two basic forms, you’ll be able to create many more complex variations.

Warning

There is a third form, in which the recursive calls never reach an end. This is called an infinite loop, and is an error you’ll want to avoid.

Counting Down

The simplest model of recursion with a natural limit is a countdown, like the one used for rockets. You start with a large number, and count down to zero. When you reach zero, you’re done (and the rocket takes off, if there is one).

To implement this in Erlang, you’ll pass a starting number to an Erlang function. If the number is greater than zero, it will then announce the number and call itself with the number minus one as the argument. If the number is zero (or less), it will announce blastoff! and end. Example 4-7, found in ch04/ex8-countdown, shows one way to do this.

Example 4-7. Counting down

-module(count).
-export([countdown/1]).


countdown(From) when From > 0 ->
  io:format("~w!~n", [From]),
  countdown(From-1);

countdown(From) ->
  io:format("blastoff!~n").

The last clause could have a guard—when From =< 0—but it would be useful only to make clear when the blastoff happens to human readers. Unnecessary guard clauses may lead to weird errors, so brevity is probably the best option here, though you’ll get a warning that From is unused in the final clause. Here’s a test run:

1>  c(count).
count.erl:9: Warning: variable 'From' is unused
{ok,count}
2> count:countdown(2).
2!
1!
blastoff!
ok

The first time through, Erlang chose the first clause of countdown(From), passing it a value of 2. That clause printed 2, plus an exclamation point and a newline, and then it called the countdown function again, passing it a value of 1. That triggered the first clause again. It printed 1, plus an exclamation point and a newline, and then it called the countdown function again—this time passing it a value of 0.

The value of 0 triggered the second clause, which printed blastoff! and ended. After running three values through the same set of code, the function comes to a neat conclusion.

Note

You could also implement this conclusion with an if statement inside a single countdown(From) function clause. This is unusual in Erlang. I find guards more readable in these cases, but you may see things differently.

Counting Up

Counting up is trickier because there’s no natural endpoint, so you can’t model your code on Example 4-7. Erlang’s single-assignment approach to variables rules out some approaches, but there’s another way to make this work, using an accumulator. An accumulator is an extra argument that keeps track of the current result of past work, passing it back into a recursive function. (You can have more than one accumulator argument if you need, though one is often sufficient.) Example 4-8, which you can find in ch04/ex9-countup, shows how to add a countup function to the count module, which lets Erlang count up to a number.

Example 4-8. Counting up

-module(count).
-export([countdown/1, countup/1]).

countup(Limit) ->
  countup(1, Limit).

countup(Count, Limit) when Count =< Limit ->
    io:format("~w!~n", [Count]),
    countup(Count+1, Limit);

countup(Count, Limit) ->
    io:format("Finished.~n").

...

It produces results such as the following:

1>  c(count).
{ok,count}
2> count:countup(2).
1!
2!
Finished.
ok

The export directive makes the countup/1 function visible (as well as the earlier countdown/1, which you’ll find in the sample code).

The countup/2 function, which does most of the work, remains private, not exported. This isn’t mandatory; you might make it public if you wanted to support counting between arbitrary values, but it’s common Erlang practice. Keeping the recursive internal functions private makes it less likely that someone will misuse them for purposes they’re not well-suited to. In this case, it doesn’t matter at all, but it can make a big difference in other more complex situations, especially when data is modified.

When you call countup/1, it calls countup/2 with an argument of 1 (for the current count) and the Limit value you provided for the upper limit.

If the current count is less than or equal to the upper limit, the first clause of the countup/2 function reports the current Count value with io:format. Then it calls itself again, increasing the Count by one but leaving the Limit alone.

If the current count is greater than the upper limit, it fails the guard on the first clause, so the second clause kicks in, reports “Finished.”, and is done.

Warning

The guards here are sufficient to avoid infinite loops. You can enter zero, negative numbers, or decimals as arguments to countup/1 and it will terminate neatly. You can get into serious trouble, however, if your termination test relies on == or =:= for a more exact comparison rather than >= or =< for a rough comparison.

Recursing with Return Values

The counting examples are simple—they demonstrate how recursion works, but just discard the return values. There are return values—the io:format calls return the atom ok—but they aren’t of much use. More typically, a recursive function call will make use of the return value.

A classic recursive call calculates factorials. A factorial is the product of all positive integers equal to or less than the argument. The factorial of 1 is 1; 1 by itself yields 1. The factorial of 2 is 2; 2 × 1 yields 2. It starts to get interesting at 3, where 3 × 2 × 1 is six. At 4, 4 × 3 × 2 × 1 is 24, and the results get larger rapidly with larger arguments.

There was a pattern to that, though. You can calculate any factorial by multiplying the integer by the factorial of one less. That makes it a perfect case for using recursion, using the results of smaller integers to calculate the larger ones. This approach is similar to the countdown logic, but instead of just counting, the program collects calculated results. That could look like Example 4-9, which you’ll find in ch04/ex10-factorial-down.

Example 4-9. A factorial written with the counting down approach

-module(fact).
-export([factorial/1]).

factorial(N) when N > 1->
  N * factorial(N-1);

factorial(N) when N =< 1 ->
  1.

The first clause of factorial uses the pattern previously described. The first clause, used for numbers above one, returns a value that is the number N times the factorial of the next integer down. The second clause returns the value 1 when it reaches 1. Using =< in that comparison, rather than ==, gives the function more resilience against non-integer or negative arguments, though the answers it returns aren’t quite right: factorials really only work for integers of 1 or higher. The results are as previously suggested:

1> c(fact).
{ok,fact}
2> fact:factorial(1).
1
3> fact:factorial(3).
6
4> fact:factorial(4).
24
5> fact:factorial(40).
815915283247897734345611269596115894272000000000

This works, but it may not be clear why it works. Yes, the function counts down and collects the values, but if you want to see the mechanism, you need to add some io:format calls into the code, as shown in Example 4-10. (You can find this at ch04/ex10-factorial-down-instrumented.)

Example 4-10. Looking into the factorial recursion calls

-module(fact).
-export([factorial/1]).

factorial(N) when N > 1->
  io:format("Calling from ~w.~n", [N]),
  Result = N * factorial(N-1),
  io:format("~w yields ~w.~n", [N, Result]),
  Result;

factorial(N) when N =< 1 ->
  io:format("Calling from 1.~n"),
  io:format("1 yields 1.~n"),
  1.

There’s a bit more overhead here. To present the result of the recursive call and still return that value to the next recursive call requires storing it in a variable, here called Result. The io:format call makes visible which value produced the result. Then, because the last value expression in a function clause is the return value, Result appears again. The second clause for 1 is similar, except that it can report simply that 1 yields 1. because it always will.

When you compile this and run it, you’ll see something such as the following:

7> fact:factorial(4).
Calling from 4.
Calling from 3.
Calling from 2.
Calling from 1.
1 yields 1.
2 yields 2.
3 yields 6.
4 yields 24.
24

Although the calls count down the values, as the logic would suggest, the messages about results don’t appear until the countdown is complete, and then they all appear in order, counting up.

The reason this happens is that the function calls don’t return values until the countdown is complete. Until then, the Erlang runtime builds a stack of frames corresponding to the function calls. You can think of the frames as paused versions of the function logic, waiting for an answer to come back. Once the call with an argument of 1 returns a simple value, not calling any further, Erlang can unwind those frames and calculate the Result. That unwinding presents the results—“X yields Y”—in the order that the frames unwind.

That “unwinding” also means that the code in Example 4-9 and Example 4-10 is not tail recursive. When Erlang encounters code that ends with a simple recursive call, it can optimize the handling to avoid keeping that stack of calls around. This probably doesn’t matter for a one-time calculation, but it makes a huge difference when you write code that will stay running for a long time.

You can achieve tail recursion for factorials by applying the counting up approach to factorials. You’ll get the same results (at least for integer values), but the calculations will work a little differently, as shown in Example 4-11, at ch04/ex12-factorial-up.

Example 4-11. A factorial written with the counting up approach

-module(fact).
-export([factorial/1]).


factorial(N) ->
  factorial(1, N, 1).

factorial(Current, N, Result) when Current =< N ->
    NewResult = Result*Current,
    io:format("~w yields ~w!~n", [Current, NewResult]),
    factorial(Current+1, N, NewResult);

factorial(Current, N, Result) ->
    io:format("Finished.~n"),
    Result.

As in the counting up example, the main function call, here factorial/1, calls a private function, factorial/3. In this case, there are two accumulators. Current stores the current position in the count, whereas Result is the answer from the previous multiplication. When the value of Current climbs past the limiting value N, the first guard fails, the second clause is invoked, and the function is finished and returns the Result. (You’ll get a compilation warning because the final clause doesn’t use the accumulator variables Current or N. You can ignore it.)

Because factorial/3’s last call in the recursive section is to itself, without any complications to track, it is tail recursive. Erlang can minimize the amount of information it has to keep around while the calls all happen.

The calculation produces the same results, but does the math in a different order:

9> fact:factorial(4).
1 yields 1!
2 yields 2!
3 yields 6!
4 yields 24!
Finished.
24

Although the code is tracking more values, the Erlang runtime has less to do. When it finally hits the final result, there’s no further calculation needed. That result is the result, and it passes back through to the original call. This also makes it easier to structure the io:format calls. If you remove them or comment them out, the rest of the code stays the same.

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement. We’ll occasionally send you account related emails.

Already on GitHub?
Sign in
to your account


Closed

travcunn opened this issue

Aug 21, 2018

· 4 comments

Comments

@travcunn

In one of our production systems, I am seeing the following in the logs hundreds of times. Should this exception be handled by the Elasticsearch Elixir client? The original function call is Elasticsearch.put_document.

(elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3

 ** (FunctionClauseError) no function clause matching in Elasticsearch.Exception.build/2

 (elasticsearch) lib/elasticsearch/exception.ex:35: Elasticsearch.Exception.build(%{"message" => nil}, nil)

 (elasticsearch) lib/elasticsearch/exception.ex:22: Elasticsearch.Exception.exception/1

 (elasticsearch) lib/elasticsearch.ex:389: Elasticsearch.format/1

@travcunn
travcunn

changed the title
Error: No function clause matching

FunctionClauseError: No function clause matching

Aug 21, 2018

@danielberkompas

Yes, this should be caught by the library. Do you know what the error payload Elasticsearch is returning looks like? I can see that it includes %{"message" => nil}, but wondered if there’s any more detail in the Elasticsearch response than that.

@danielberkompas

@travcunn it looks to me like you’re not using the latest version of the library. lib/elasticsearch.ex:389 doesn’t call Elasticsearch.format anymore. Have you tried upgrading to the latest version?

@travcunn

I found out why this was happening in our system. There was a loop that was calling Elasticsearch.put_document thousands of times. I would be interested in making an improvement to this project in such a way that unhandled errors just spit out raw HTTP errors so it is easier to debug Elasticsearch queries.

@ammbot

I got the same error

Elixir.FunctionClauseError: no function clause matching in Elasticsearch.Exception.build/2
  File "lib/elasticsearch/exception.ex", line 35, in Elasticsearch.Exception.build/2
  File "lib/elasticsearch/exception.ex", line 22, in Elasticsearch.Exception.exception/1
  File "lib/elasticsearch.ex", line 389, in Elasticsearch.format/1

the argument to build/2 is
%{"message" => "Unable to connect to the server.", "ok" => false} and nil

Понравилась статья? Поделить с друзьями:
  • Exception error dota 2
  • Exception error 15 modbus
  • Exception erangeerror in module interfaceeditor asi at 00014c6b samp range check error
  • Exception erangeerror in module gfxhack asi at 00007e9c range check error
  • Exception efc create error in module