📜 ⬆️ ⬇️

Erlang for the little ones. Chapter 3: Basic Function Syntax

image Good night, Habr! We continue to study Erlang for the little ones.

In the last chapter, we looked at how to declare functions and how to combine them into modules. In this chapter, we will look at the syntax of functions in more detail.



The source codes for the chapter are here .
')

Pattern matching


First, let's write a function that will greet the user and the greeting text will depend on their gender. In the form of pseudocode, our function will be as follows:
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 instead of the classic if then else , pattern matching is used, you can save a bunch of sample code. This is how this function will look like on Erlang if you use pattern matching:
 greet(male, Name) -> io:format("Hello, Mr. ~s!", [Name]); greet(female, Name) -> io:format("Hello, Mrs. ~s!", [Name]). 

The io:format() function is used for formatted output to the terminal. Here we used pattern matching in the description of the function argument list. This allowed us to simultaneously assign input values ​​and select the part of the function to be executed. Why first assign values, and then compare them in the function body, if you can do it at the same time and in a “more declarative” style?

In general, the declaration of such a function is as follows:
 fnct_name(X) -> Extpression; fnct_name(Y) -> Expression; fnct_name(_) -> Expression. 

Each branch of the function is declared as a complete function but ends with a semicolon ( ; ), followed by the next one. After the last part of the set point.

Pay attention to the latest sample. What happens if we call our greet() function and specify an unintended floor? We will get an exception that the input parameters do not match any of the samples:
 1> chapter03:greet(someOther, "Haru"). ** exception error: no function clause matching chapter03:greet(someOther, "Haru") 

Therefore, it is important to include in the ad a sample that fits any value. At the same time it should be the last. Otherwise, samples declared after it will never be processed.
Let's rewrite our function, so that it correctly handles invalid 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 the declaration of functions brings much more benefit than just reducing the amount of code. Recall the lists: the list consists of the head and the rest. Let's write two functions that will return the first and second elements of the resulting list.
 first([X|_]) -> X. second([_,X|_) -> X. 

Simple enough, right? There is another interesting trick based on the fact that variables in Erlang can be assigned only once.
 same(X,X) -> true; same(_,_) -> false. 

This function returns true if its arguments are the same, otherwise it returns false . How it works? When calling the function chapter03::same(one, two). the variable X assigned the value one , then an attempt is made to assign the same variable to the value two . Because the value has already been assigned, the attempt fails and the template is rejected as inappropriate. Since in the second case we do not explicitly indicate which variables need to be assigned values, the template is suitable and the function returns false . If we pass the same values ​​to the function, chapter03:same(3, 3). then the first template will do, and the function will return true .

Security expressions


There is one major flaw in pattern matching: it is not expressive enough. With it, you can not specify the data type, range and other similar generalizations. Each sample is a specific case. To solve this problem in Erlang there are Security expressions (or watchdog conditions). First, let's write a function that will take our BMI (body mass index) and evaluate our condition. The BMI of a person is equal to his weight divided by the square of 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.". 

Here, when the function is accessed, the first condition is checked, which is after the word when ( Bmi =< 18.5 ). If this expression returns true , the corresponding code branch will be executed. Otherwise, the next condition is checked and so on until the end. And here we also added a condition to the end under which any value would fit.

In the general case, a function declaration using security expressions is as follows:
 fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_1 -> Expression; fnct_name(Arg_1, Arg_1, ..., Arg_n) when rule_2 -> Expression. 

Also, we are not limited to one expression in the condition. We can conduct several checks. If both conditions must be met (analog andalso ) to pass the check, a comma ( , ) is put between them.
 lucky_number(X) when 10 < X, X > 20 -> true; lucky_number(_) -> false. 

If at least one is enough (an orelse ), they are separated by a semicolon ( ; ).
 lucky_atom(X) when X == atom1; X == anot2 -> true; lucky_atom(_) -> false. 

You can use functions in security expressions. Here is a function that divides one number by another, but before that checks that the arguments passed are numbers and that Y not zero:
 safe_division(X, Y) when is_integer(X), is_integer(Y), Y /= 0 -> X / Y; safe_division(_, _) -> false. 


Branching


if


In addition to the above expressions in Erlang, there is the usual if branching operator. Let's rewrite our bmi_tell() function using this operator.
 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, the if branching if looks like this:
 if Rule_1 -> Expression; Rule_2 -> Expression; ... true -> Expression end. 

I want to remind that, unlike Haskell, indents here do not play any role except decorative and the code is formatted in such a way only that it would be easier to perceive. You are free to format it as you like.
Here the expressions rule_1 and rule_2 are one or more conditions. If the condition is successful, the Expression code block (may contain one or several commands) following it will be executed. Such logical branches can be any number.

Pay attention to the last block true -> "You're very fat. What is this condition? This is an alternative to the orelse operator. The code block following it will be executed if no condition passes the test. It is important to remember that this operator is mandatory. After all as we remember, in Erlang, any expression must return a result. If you do not describe this block, the module will compile, but when the execution thread checks all conditions and does not detect the block true , an exception will be generated.
 ** exception error: no true branch found when evaluating an if expression in function chapter03:if_bmi_tell/1 


The case ... of operator


In addition to if , Erlang has another branching operator - case of . And this operator is like an entire function inserted into another function. It allows you to make a choice not only on the basis of the condition, but also makes it possible to use pattern matching and guard expressions. In general, it looks like this:
 case Rule of Val_1 -> Expression; Val_2 -> Expression ... Val_n -> Expression 

Here Rule is a variable or expression whose result will be checked. Next are the condition-expression blocks already known to us ( Val_1 -> Expression; ). You can use pattern matching and guard expressions in the condition, which makes this design incredibly flexible.

Let us, for example, write a function that will accept a tuple consisting of temperature and the name of its measurement scale and estimate it on the basis of this 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 mapped to the Temp variable, then a pattern will be found under which this tuple will fit. Then it will be checked by security expressions, and if it is passed, the function will return the corresponding string.
Here, we also add an expression to the end, which will intercept all unsuitable options.

As we see, the expression of case of almost completely can be moved to the scope of the function declaration. So where is the best place to post conditions? The answer is simple: where you like more. The differences between these two designs are minimal. Therefore, use the option that is easier for you to read.

Conclusion


In this chapter, we looked at how you can control the flow of execution within functions. We learned what constructions exist for this and how to use them. We also met with security expressions.

In the next chapter, we will take a closer look at the type system in the Erlang language.

Thank you for reading to the end. I hope it was interesting. I apologize for such a long delay. There is no free time at all.

PS On typos and errors, please report in a personal, and not in the comments. Thank you for understanding.

Source: https://habr.com/ru/post/211815/


All Articles