Syntax in functions

Khớp mẫu (Pattern Matching)

A snail greeting you.

Trong chuơng trước bạn đã tìm hiểu về module và học cách lưu và biên dịch đoan mã của bạn cũng như viết một module đơn giản cùng định nghĩa một số hàm cơ bản. Trong chương này chúng ta sẽ bắt đàu đi sâu hơn về hàm và học cách viết các hàm phức tạp hơn. . Các hàm mà chúng ta đã viết trong chương trước khá đơn giả và có đôi chút thất vọng. do đó chúng ta sẽ bắt đầu những điều thú vị hơn về chúng trong chương này. đầu tiên chúng ta sẽ viết một hàm đưa ra một lời chào dựa trên giới tính của người đó. trong hầu hêt các ngôn ngữ chúng sẽ viết như sau:

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

đơn giản chúng ra sử dụng mệnh lệnh điều kiện để kiểm tra giới tính và đưa ra thông báo dựa trên điều kiện đó, vậy trong Erlang làm thế nào ? Erlang cũng hỗ trợ kiểu điều kiện nhưng chúng ra sẽ không dùng , thay vào đó hãy nhớ lại , những chương trước đó chúng ta đã học về khớp mẫu, vậy hãy sử dụng nó trong Erlang, nó sẽ giúp bạn tiết kiệm được kha khá đoạn mã đấy. hàm này sẽ được viết trong Erlang như sau:

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

Phải thừa nhận rằng hàm in ra màn hình của Erlang trông thật khó chịu, tồi khi so sánh với các ngôn ngữ khác, nhưng đó không phải là vấn đề , sự khác biệt chính ở đây chính là việc chúng ra sử dụng khớp mẫu đề xác định thành phần của một hàm được sử dụng và bind giá trị mà chúng ta cần ở cùng một thời điểm The main difference here is that we used pattern matching to define both what parts of a function should be used and bind the values we need at the same time . Và chúng ta sẽ không cần phải gán giá trị đó với biến và so sánh chúng, vi thế thay vì :

function(Args)
   if X then
      Expression
   else if Y then
      Expression
   else
      Expression

sẽ viết thánh:

function(X) ->
  Expression;
function(Y) ->
  Expression;
function(_) ->
  Expression.

kết quar đạt được sẹ tương tự , nhưng cách khai báo này tốt hơn nhiều. mỗi khai báo hàm function trên được gọi là một function clause. các 'function clauses' phải phân tách nhau bởi đấu chấm phẩy (;) và một môtk kiểu mẫu function declaration. một A function declaration sẽ được coi như một biểu thức lớn ( một tập các biểu thức ), đó là lí do vì sao function clause cuối cùng kết thúc bằng dấu .. Điều này thật "lạ" khi sử dụng tokens để xác định luồng công việc, nhưng bạn sẽ sớm cảm thấy quen thuộc thôi. hi vọng là bạn sẽ sớm cảm thấy quen bởi vì không có cách nào khác!

Chú ý: hàm io:format's formatting is done with the help of tokens being replaced in a string. The character used to denote a token is the tilde (~). Some tokens are built-in such as ~n, which will be changed to a line-break. Most other tokens denote a way to format data. The function call io:format("~s!~n",["Hello"]). includes the token ~s, which accepts strings and bitstrings as arguments, and ~n. The final output message would thus be "Hello!\n". Another widely used token is ~p, which will print an Erlang term in a nice way (adding in indentation and everything).

The io:format function will be seen in more details in later chapters dealing with input/output with more depth, but in the meantime you can try the following calls to see what they do: io:format("~s~n",[<<"Hello">>]), io:format("~p~n",[<<"Hello">>]), io:format("~~~n"), io:format("~f~n", [4.0]), io:format("~30f~n", [4.0]). They're a small part of all that's possible and all in all they look a bit like printf in many other languages. If you can't wait until the chapter about I/O, you can read the online documentation to know more.

sủ dung khớp mẫu trong hàm có nhiều hình thức phức tạp và mạnh hơn. có thể bạn quên hoặc nhớ trong các chương trước , chúng ta có thẻ sử dung khớp mẫu cùng với cấu trúc dữ liệu danh sách để lấy ra phần đàu ( head ) và phần đuôi ( tail ) của một danh sách. Giờ chúng ta tiếp tục làm vậy nhưng thay vì viết trong shell, chúng ta sẽ viết thành một module , nào hãy tạo và đặt tên module đó là functions , trong module này chúng ta sẽ đinh nghĩa một số hàm để tìm hiểu sâu hơn về cách sử dụng khóp mẫu:

-module(functions).
-compile(export_all). %% replace with -export() later, for God's sake!

hàm đầu tiền mà chúng ra sẽ viết là hàm head/1, nó tương tự hàm erlang:hd/1 , hàm này nhận một đối số và yêu cầu đầu vào là một danh sách và giá trị trả về là phần tư đầu tiên trong danh sách đó. chúng ta sẽ hoàn thành với sự giúp đỡ của toán tử cons (|):

head([H|_]) -> H.

Nào hãy biên dịch code và gọi hàm đó trong shell, hãy gõ functions:head([1,2,3,4])., bạn sẽ nhận được giá trị là '1'. Tiếp tục , để nhận được phần từ thứ hai trong danh sách bạn cần phải tạo một hàm:

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

dánh sách sẽ được phân tích bởi Erlang để sao cho nó khớp với mẫu. Nào hãy biên dịch module và gõ thử trong shell!

1> c(functions).
{ok, functions}
2> functions:head([1,2,3,4]).
1
3> functions:second([1,2,3,4]).
2

Bạn có thể lặp lại với các danh sách bao nhiêu bạn muốn, nhưng sẽ không thực tế nếu làm vậy với hàng nghìn phần, . vì vậy bạn có thể sửa lại bằng cách thay thế bằng một hàm đệ quy, và chúng ta sẽ tìm hiểu cách viết này ở các chương sau . Còn giờ, hãy tập trung hơn vào khớp mẫu let's. Khái nhiệm về biến tự do và ràng buộc chúng ta đã thảo luận trong chương Starting Out (for real) vẫn đúng khi áp dụng với hàm: chúng ta có thể so sánh nếu biết hai tham số đưa vào một hàm có khớp hay không . Đối với vd trên, ta có thể tạo mới một hàm gọi là same/2 , hàm này nhận hai tham só truyền vào và trả lại giá trị nếu chúng khớp với nhau:

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

thật đơn giản. trước khi đi sâu vào giải thích chi tiêý hàm same hoạt động như thế nào, chúng ta hay nhớ lại khái niệm về biến ràng buộc và không ràng buộcsssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss ( bound and unbound variable ), hãy xem ví sau:

A game of musical chair with 3 chairs: 
 two occupied by a cat and a snake, 
 and one free. 
 The cat and snake are labelled 'values', 
 the chairs on which they sit are labelled 'bound', 
 and the free chair is labelled 'unbound'

Nếu ta coi trò chơi này Erlang, và bạn muốn ngồi vào chiếc ghế trống còn lại. và nhưng chiếc ghế đã được ngồi rồi thì không thể ngồi vào được nữa! Joking aside, biên ràng buộc ( unbound variables ) laf những biến không chưa giá trị hay chưa được liên kết với bất kỳ giá trị nào ( vd như chiếc ghế còn trống kia) . binding một biến chỉ đơn giản là việc ta liên kêt một giá trị với một biến không ràng buộc ( unbound variable ). Trong trường hợp của, khi bạn muốn gán một giá trị với một biến đã được ràng buộc rồi, một thông báo lỗi sẽ xảy ra unless the new value is the same as the old one. Tiếp đó hãy nhìn vào con rắn: giờ nếu có một con rắn mới xuất hiện, trò chơi sẽ không ảnh hưởng nhiều tới trò chơi, thay vào đó cùng lắm bạn chỉ có nhiều con rắn hơn. nhưng nếu một con vật khác xuất hiên tới ngồi ( vd một con ong mật ) mọi việc sẽ trở lên tồi tề . tương tự với một biên ràng buộc lên cùng một giá trị, nếu khác giá trị thật sự tồi tệ. bạn có thể xem lại chương Invariable Variables nếu vd trên chưa làm bạn hiểu rõ.

Quay trở lại vd code: điều gì sẽ xảy ra sau khi bạn gọi same(a,a), đầu tiên chúng ta nhìn thấy biến X chưa được gán, liên kết bất kỳ giá tri nào : và nó sẽ tự động khớp với giá trị a. Sau đó Erlang sẽ tiếp tục tìm tới đối số thứ hai, lúc này X đã là một biến đã ràng buộc và được gán giá trị . nó sẽ so sánh vơi giá trị a ở tham số thứ hai. Nếu nó khớp , phép khớp mẫu chính xác và hàm sẽ trả về giá trị true. Ngược lại nếu hai giá trị không khớp, nó sẽ di chuyển hàm thứ hai ( second function clause ), tại hàm này nó không quan tâm về tham số truyền vào ( khi không còn lựa chọn nào nữa hay chỉ còn một lựa chọn cuối cùng!) và trả về giá trị false. chú ý hàm này có hiệu quả đối với bất kỳ loại tham số nào! nó hoạt động với bất kỳ kiểu dữ liệu nào ( không chỉ với mỗi kiểu danh sách hay biên đơn). dưới đây là một vd nâng cao hàm sau sẽ hiển thị theo định dạng ngày:

valid_time({Date = {Y,M,D}, Time = {H,Min,S}}) ->
    io:format("The Date tuple (~p) says today is: ~p/~p/~p,~n",[Date,Y,M,D]),
    io:format("The time tuple (~p) indicates: ~p:~p:~p.~n", [Time,H,Min,S]);
valid_time(_) ->
    io:format("Stop feeding me wrong data!~n").

Một điều lưu ý là bạn cũng có thể sử dụng toán tử =, nó cho phép bạn khớp nội dùng tơi một bộ ({Y,M,D}) và biến (Date) gán tới bộ đó. Hãy mở shell và thử kiểm tra cách hoạt động của hàm trên:

4> c(functions).
{ok, functions}
5> functions:valid_time({{2011,09,06},{09,04,43}}).
The Date tuple ({2011,9,6}) says today is: 2011/9/6,
The time tuple ({9,4,43}) indicates: 9:4:43.
ok
6> functions:valid_time({{2011,09,06},{09,04}}).
Stop feeding me wrong data!
ok

Hm có một vấn đề ở đây! hàm trên chấp nhận bất kỳ giá trị, thậm chú là cả text hay atom nhưng chỉ có tham số truyền vào là bộ theo cấu trúc {{A,B,C}, {D,E,F}} là hợp lệ, . Đấy là một trong những hạn chế của khớp mẫu: Tuy nhiên để có thể xác định chính xác các giá trị như atom, giá tri trừu tượng như head|tail trong danh sách, một bộ gồm N phần tử , hay bất cứ thứ gì với ( _ và biến không ràng buộc ), etc. Chúng ta sẽ sử dụng chốt canh ( guards ).

Guards, Guards!

A baby driving a car

Chốt canh là các mệnh đề được dùng để bổ sung đầu một hàm, nó kết hợp với khớp mẫu để tăng khả năng diễn đạt. như đã đề cập phía trên , khớp mẫu có một hạn chế là không thể diễn đạt mọi thứ đc vd như đội rộng của một giá trị hay kiểu dữ liệu nhất định. vd chúng ta không thể nào diễn đat được: một cậu bé 12 tuổi là quá thấp khi chơi bóng rổ với cầu thủ chuyên nghiệp hay ko ? khoảng cách này liệu có thực sự quá dài để đi trong lòng bàn tàu của bạn hay ko ? bạn quá giá hay không đủ tuổi để điều khiển xe ô tô hay không ? tât cả những cậu hỏi trên bạn không thể trả lời được nếu chỉ dùng với khớp mẫu hay nếu có thì nó cũng rất phức tạp. vd về độ tuổi lái xe bạn có thể viết như sau:

old_enough(0) -> false;
old_enough(1) -> false;
old_enough(2) -> false;
...
old_enough(14) -> false;
old_enough(15) -> false;
old_enough(_) -> true.

vd trên, ta phải lặp lại một hàm quá nhiều và nó thực sự tồi tệ, tất nhiễn bạn vẫn có thể làm nó, nhưng bạn sẽ phải lặp đi lặp lại những dòng code đó một mình một cách tẻ nhạt. Nếu bạn thực sự muốn kết bạn, hãy bắt đầu cùng với module chốt canh guards và chúng ta có giải quyết một cách ngắn gọn cậu hỏi đó bằng:

old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.

Đó là những gì mà bạn phải làm! và như bạn thấy cách làm này gọn và ngắn hơn rất nhièu. chủ ý là khi sủ dụng biểu thức chôt canh giá bên cạnh hàm trị trả về của một hàm đó luôn quy ước trả về true. nếu giá trị trả về là false hoặc một ngoại lệ sẽ được ném ra thì chốt canh sẽ không hoạt động. Giả sử chúng ta ko cho phép người già quá 104 lái xe. Khi đó giá trị hợp lệ là từ 16 tới 104 tuổi. Làm thế nào để chúng ta có thể kiểm tra điều kiên này ? Điều này khá đơn giản, chỉ cần thêm một biẻu thức chốt canh thứ hai vào:

right_age(X) when X >= 16, X =< 104 ->
    true;
right_age(_) ->
    false.

đấy phẩy (,) ở đầy giống với toán tử andalso và đấu hay chấm (;) giống với toán tử orelse (bạn có thể xem lại cách hoạt động của hay toán tử ở chương "Starting Out (for real)"). biểu thức chốt canh sẽ thành khi thỏa mãn cả hai điều kiện chốt canh đó. hay chúng ta cũng có thể biểu diễn tinh chất của hàm này theo hướng ngược lại:

wrong_age(X) when X < 16; X > 104 ->
    true;
wrong_age(_) ->
    false.
Guard

Kết quả nhận được sẽ tương tự hàm 'right_age'. Bạn có thể kiểm tra nếu bạn muốn. Trong các biểu thức chôt canh, dấu (;) có tác dụng giống toán tử orelse : nếu điều kiện chốt canh đầu tiên sai , ngay sau đó nó sẽ chuyển qua kiểm tra điều kiện chốt canh thứ hai, và cứ thê tiếp tục cho tới khi có bất kỳ điều kiện nào đúng hay tất cả đều sai.

Bên cạnh đó bạn cũng có thể sử dụng nhiều hơn một hàm để so sách và kiểm tra boolean trong hàm, bao gồm các biểu thức toán học (A*B/C >= 0) , các hàm về kiểm tra kiểu dư liệu như is_integer/1, is_atom/1, etc. (chúng ta sẽ đi sâu hơn trong các chương tiếp theo ). một điều One negative point about guards is that they will not accept user-defined functions because of side effects. Erlang không phải là ngôn ngũ lập trình hàm thuần tùy (so với Haskell ) bởi vì nó có khá nhiều side side effects ( đây là điều không được phép trong hàm thuần túy): vd bạn có sử dụng các thao tác nhập xuất ( I/O ), trao đổi các thông điệp qua lại giữa các actor và đưa ra lỗi như bạn muôn ở mọi lúc , mọi nơi. Rõ ràng không có cách nào để đảm bảo xác định một hàm sử dụng chốt canh sẽ hay không in ra text hay bắt các lỗi quan trọng mỗi khi nó được kiềm tra qua nhiều function clauses. do đó thay vì So instead, Erlang just doesn't trust you (and it may be right to do so!)

Thành thực mà nói, bạn lên hiểu rõ cú pháp cơ bản của chốt canh để sao không bị bối rối khi gặp chúng.

Chú ý: ở phàn trên tôi đã so sánh ,; trong chốt canh với các toán tử in andalsoorelse. Mặc dù vậy chức nằng của chúng không chính xác giống nhau. The former pair will catch exceptions as they happen while the latter won't. What this means is that if there is an error thrown in the first part of the guard X >= N; N >= 0, the second part can still be evaluated and the guard might succeed; if an error was thrown in the first part of X >= N orelse N >= 0, the second part will also be skipped and the whole guard will fail.

However (there is always a 'however'), only andalso and orelse can be nested inside guards. This means (A orelse B) andalso C is a valid guard, while (A; B), C is not. Given their different use, the best strategy is often to mix them as necessary.

What the If!?

If có chức nằng và cú pháp tươn tự như chôt canh, but outside of a function clause's head. thực tế , if còn được gọi tên là Guard Patterns. if trong Erlang thì khác so với biểu thức if mà bạn đã học trong các hầu hết ngôn ngữ khác; compared to them they're weird creatures that might have been more accepted had they had a different name. Khi bạn gia nhập Erlang bạn lên quên hết tất cả When entering Erlang's country, you should leave all you know about ifs at the door. Take a seat because we're going for a ride.

Để nhin thấy sự tương đồng giữa chốt canh và biểu thức ìf , hãy nhìn vào ví dụ sau đây:

-module(what_the_if).
-export([heh_fine/0]).


heh_fine() ->
    if 1 =:= 1 ->
        works
    end,
    if 1 =:= 2; 1 =:= 1 ->
        works
    end,
    if 1 =:= 2, 1 =:= 1 ->
        fails
    end.

Hãy lưu đoạn code trên vào một file đặt tên là what_the_if.erl và hay thử chạy nó:

1> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
2> what_the_if:heh_fine().
** exception error: no true branch found when evaluating an if expression
     in function  what_the_if:heh_fine/0
Labyrinth with no exit

Uh oh! bộ biện dịch ngay lập tức cảnh báo chúng ta rằng không có biểu thức điều kiện 'if' ở dòng 12 sex khớp bởi vì chỉ có chốt canh the compiler is warning us that no clause from the if on line 12 (1 =:= 2, 1 =:= 1) will ever match because its only guard evaluates to false. Remember, in Erlang, everything has to return something, and if expressions are no exception to the rule. As such, when Erlang can't find a way to have a guard succeed, it will crash: it cannot not return something. As such, we need to add a catch-all branch that will always succeed no matter what. In most languages, this would be called an 'else'. In Erlang, we use 'true' (this explains why the VM has thrown "no true branch found" when it got mad):

oh_god(N) ->
    if N =:= 2 -> might_succeed;
       true -> always_does  %% this is Erlang's if's 'else!'
    end.

chúng ta hãy thử kiểm tra xem hàm mới được cập nhật lại xem ( khi bạn biên dịch và gọi module, nếu bạn vẫn đặt hàm 'heh_fine' bên trong file thì bộ biên dịch vẫn đưa ra cảnh báo, tuy nhiên đừng bận tậm, hãy bỏ qua cảnh báo đó đi , thay vào đó mục đích hiện tại của chúng ta là kiểm tra hàm mới viết này):

3> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
4> what_the_if:oh_god(2).
might_succeed
5> what_the_if:oh_god(3).
always_does

Một cách khác để sử nhièu biểu thức chôt canh cùng với biẻu thức if. The function also illustrates how any expression must return something: Talk has the result of the if expression bound to it, and is then concatenated in a string, inside a tuple. When reading the code, it's easy to see how the lack of a true branch would mess things up, considering Erlang has no such thing as a null value (ie.: Lisp's NIL, C's NULL, Python's None, etc):

%% note, this one would be better as a pattern match in function heads!
%% I'm doing it this way for the sake of the example.
help_me(Animal) ->
    Talk = if Animal == cat  -> "meow";
              Animal == beef -> "mooo";
              Animal == dog  -> "bark";
              Animal == tree -> "bark";
              true -> "fgdadfgna"
           end,
    {Animal, "says " ++ Talk ++ "!"}.

And now we try it:

6> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
7> what_the_if:help_me(dog).
{dog,"says bark!"}
8> what_the_if:help_me("it hurts!").
{"it hurts!","says fgdadfgna!"}

có thể bạn giống như một số lập trình viên Erlang khác thắc mắc là tạo sao 'true' thì được ưu tiên hơn 'else' và coi như một atom để điều khiển luông; Tóm lại, 'else' nhin vẫn thân thiện hơn. Richard O'Keefe đã giải đáp trong Erlang mailing lists: ( tôi sẽ trích dẫn lại câu trả lời bởi vì tóm tắt nó ):

It may be more FAMILIAR, but that doesn't mean 'else' is a good thing. I know that writing '; true ->' is a very easy way to get 'else' in Erlang, but we have a couple of decades of psychology-of-programming results to show that it's a bad idea. I have started to replace:

                          by
	if X > Y -> a()		if X > Y  -> a()
	 ; true  -> b()		 ; X =< Y -> b()
	end		     	end

	if X > Y -> a()		if X > Y -> a()
	 ; X < Y -> b()		 ; X < Y -> b()
	 ; true  -> c()		 ; X ==Y -> c()
	end			end
	

which I find mildly annoying when _writing_ the code but enormously helpful when _reading_ it.

'Else' hay 'true' tránh lên "tránh" đi cùng nhau: if thì thường dễ đọc hơn khi bạn tât các các are usually easier to read when you cover all logical ends rather than relying on a "catch all" clause.

như đã đề cập trước đó, cố một số cá hàm mà nếu dùng biểu thức chốt canh sẽ bị hạn chế (chúng ta sẽ tìm hiểu chi tiết hơn trong Types (or lack thereof)). và đó là This is where the real conditional powers of Erlang must be conjured. tơi sẽ giới thiêu tới bạn: biểu thức case!

Note: All this horror expressed by the function names in what_the_if.erl is expressed in regards to the if language construct when seen from the perspective of any other languages' if. In Erlang's context, it turns out to be a perfectly logical construct with a confusing name.

In Case ... of

If the if expression is like a guard, a case ... of expression is like the whole function head: you can have the complex pattern matching you can use with each argument, and you can have guards on top of it!

As you're probably getting pretty familiar with the syntax, we won't need too many examples. For this one, we'll write the append function for sets (a collection of unique values) that we will represent as an unordered list. This is possibly the worst implementation possible in terms of efficiency, but what we want here is the syntax:

insert(X,[]) ->
    [X];
insert(X,Set) ->
    case lists:member(X,Set) of
        true  -> Set;
        false -> [X|Set]
    end.

If we send in an empty set (list) and a term X to be added, it returns us a list containing only X. Otherwise, the function lists:member/2 checks whether an element is part of a list and returns true if it is, false if it is not. In the case we already had the element X in the set, we do not need to modify the list. Otherwise, we add X as the list's first element.

In this case, the pattern matching was really simple. It can get more complex (you can compare your code with mine):

beach(Temperature) ->
    case Temperature of
        {celsius, N} when N >= 20, N =< 45 ->
            'favorable';
        {kelvin, N} when N >= 293, N =< 318 ->
            'scientifically favorable';
        {fahrenheit, N} when N >= 68, N =< 113 ->
            'favorable in the US';
        _ ->
            'avoid beach'
    end.

Here, the answer of "is it the right time to go to the beach" is given in 3 different temperature systems: Celsius, Kelvins and Fahrenheit degrees. Pattern matching and guards are combined in order to return an answer satisfying all uses. As pointed out earlier, case ... of expressions are pretty much the same thing as a bunch of function heads with guards. In fact we could have written our code the following way:

beachf({celsius, N}) when N >= 20, N =< 45 ->
    'favorable';
...
beachf(_) ->
    'avoid beach'.

This raises the question: when should we use if, case ... of or functions to do conditional expressions?

parody of the coppertone logo mixed with the squid on the tunnel page of this site

Which to use?

Which to use is rather hard to answer. The difference between function calls and case ... of are very minimal: in fact, they are represented the same way at a lower level, and using one or the other effectively has the same cost in terms of performance. One difference between both is when more than one argument needs to be evaluated: function(A,B) -> ... end. can have guards and values to match against A and B, but a case expression would need to be formulated a bit like:

case {A,B} of
    Pattern Guards -> ...
end.

This form is rarely seen and might surprise the reader a bit. In similar situations, using a function call might be more appropriate. On the other hand the insert/2 function we had written earlier is arguably cleaner the way it is rather than having an immediate function call to track down on a simple true or false clause.

Then the other question is why would you ever use if, given cases and functions are flexible enough to even encompass if through guards? The rationale behind if is quite simple: it was added to the language as a short way to have guards without needing to write the whole pattern matching part when it wasn't needed.

Of course, all of this is more about personal preferences and what you may encounter more often. There is no good solid answer. The whole topic is still debated by the Erlang community from time to time. Nobody's going to go try to beat you up because of what you've chosen, as long as it is easy to understand. As Ward Cunningham once put it, "Clean code is when you look at a routine and it's pretty much what you expected."