Back to Home

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

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 ...

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

  • Tutorial
imageGood 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:
functiongreet(Gender,Name)if Gender == male thenprint("Hello, Mr. %s!", Name)
  elseif Gender == female thenprint("Hello, Mrs. %s!", Name)
  elseprint("Hello, %s!", Name)
end

If instead of the classical construction to if then elseuse pattern matching, 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 function is io:format()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 that should 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 full 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 function greet()and specify an unintended gender? 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 your 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 processes 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 trueif its arguments are the same, otherwise it returns false. How it works? When a function is called, a chapter03::same(one, two).variable is Xassigned a value one, then an attempt is made to assign a value to the same variable two. Due to the fact that 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 the same values ​​are passed to the function chapter03:same(3, 3)., the first template will fit 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. A person's BMI 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 branch of the code 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)whenrule_1 ->
    Expression;
fnct_name(Arg_1, Arg_1, ..., Arg_n)whenrule_2 ->
    Expression.

Also, we are not limited to one expression in the condition. We can conduct several checks. If both conditions (analog andalso) must be fulfilled 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 (analog orelse) is sufficient , 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 would Ynot be zero:
safe_division(X, Y)whenis_integer(X), is_integer(Y), Y /= 0 ->
    X / Y;
safe_division(_, _) ->false.


Branching


Operator if


In addition to the above expressions in Erlang, there is the usual branch operator if. Let's rewrite our function bmi_tell()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 branch operator iflooks 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, expressions rule_1and rule_2are one or more conditions. If the condition is successful, the code block will be executed Expression(it may contain one or several commands) following it. 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 operator.orelse. The code block following it will be executed if no conditions pass the test. It is important to remember that this operator is required. After all, as we remember, in Erlang, any expression should return a result. If this block is not described, the module will be compiled, 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


Operator case ... of


In addition ifto Erlang, there is 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 Ruleis a variable or expression whose result will be checked. This is followed by the condition-expression blocks ( Val_1 -> Expression;) already familiar to us . 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 evaluate it on the basis of this data.
assessment_of_temp(Temp) ->case Temp of
        {X, celsius} when20 =< X, X =< 45 ->
            'favorable';
        {X, kelvin} when293 =< X, X =< 318 ->
            'scientifically favorable';
        {X, fahrenheit} when68 =< X, X =< 113 ->
            'favorable in the US';
        _ ->
            'not the best tempperature'
  end.

When executing this function, our tuple will be mapped to a variable Temp, 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 variants.

As we see, the expression case ofcan be almost completely moved to the function declaration area. 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 About typos and errors please report in a personal, and not in the comments. Thank you for understanding.

Read Next