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.
0 comments
Upload image