Writing syntax in Erlang can be a bit of an ordeal if you're not familiar with functional languages. The code doesn't follow a familiar pattern, the necessity of using recursion means that execution jumps around a lot and can be hard to follow.
Erlang syntax is not any harder to learn and substantially easier to write than some popular languages. There are a few basic ways to write even simple functions in Erlang, so in this article we'll take a simple function, complicate the algorithm to add more depth and write it in a few different ways to demonstrate the options you've got available.
The function we're looking at is about as basic as they come, determining if two positive integers are divisible. The shortest and clearest way of writing a function like this is by using the inbuilt remainder function:
isdivby(N, D) when (N > 0) and (D > 0) -> N rem D == 0; isdivby(_, _) -> false.
Since the result of the last expression is returned as the result of the function, this will return true as N and D are both positive and N divided by D leaves no remainder.
The _ character can be used whenever you don't care about the value of a result, we use it here because we know anything valid has been matched by the function definition directly above. In Erlang, functions are always matched from top to bottom.
Rather than use inbuilt functions relying upon division, we could write another algorithm, which uses an accumulator to repeatedly subtract D from N. If we reach exactly zero then it is true that N is divisible by D and false otherwise.
We need to use another function here so we can test that the original arguments are positive: The isdivby2_acc function uses N == 0 as a stopping condition, however no numbers divide 0.
isdivby2(N, D) when (N > 0) and (D > 0) -> isdivby2_acc(D, N); isdivby2(_, _) -> false. isdivby2_acc(_, 0) -> true; isdivby2_acc(_, Acc) when Acc false; isdivby2_acc(D, Acc) -> isdivby2_acc(D, Acc-D).
This is still too simple to demonstrate the options available for structuring branches, let's take an overcomplicated algorithm to the problem based upon an old C algorithm for fast prime division checking. The only operators allowed are binary division and remainder (since they were very fast on C integers) and addition. This algorithm breaks the problem into branches:
- if N is equal to D then they are divisible.
- If N is smaller than D then N is not divisible by D
- If N is divisible by 2 and D is divisible by 2 then N is divisible by D only if N/2 is divisible by D/2.
- If N is divisible by 2 and D is not, then N is divisible by D only if N/2 is divisible by D
- If neither N or D is divisible by 2, then N is divisible by D only if (N + D)/2 is divisible by D
- If N is not divisible but 2 by D is then N is not divisible by D
There are three ways to approach this: nested if statements, a flattened if statement and pattern matching with guards.
Nested if statements
isdivby3(N, D) when (N > 0) and (D > 0) -> if N == D -> true; (N false; N rem 2 == 0 -> if D rem 2 == 0 -> isdivby3(N div 2, D div 2); true -> isdivby3(N div 2, D) end; true -> if D rem 2 == 1 -> isdivby3((N + D) div 2, D); true -> false end end; isdivby3(_, _) -> false.
In this example, we first branch on whether or not N is divisible by 2, and then later branch depending on whether D is. This most closely represents the tree of possible results, but at the cost of a long, and deep, function implementation.
Flattened if statement
isdivby4(N, D) when (N > 0) and (D > 0)-> if N == D -> true; (N false; (N rem 2 == 0) and (D rem 2 == 0) -> isdivby4(N div 2, D div 2); (N rem 2 == 0) and (D rem 2 == 1) -> isdivby4(N div 2, D); (N rem 2 == 1) and (D rem 2 == 1) -> isdivby4((N + D) div 2, D); (N rem 2 == 1) and (D rem 2 == 0) -> false end; isdivby4(_, _) -> false;
This is clearer, we now see that there are six clear options. We don't actually need to write out each guard in full, since we know the order in which they are encountered, we can omit any guard that is true by inference — ie. at the (N rem 2 == 1) and (D rem 2 == 1) guard, we know that N rem 2 must be 1, since if it was 0 it would be matched by one of the above two guards.
Pattern matching and guards
isdivby5(N, D) if (N false isdivby5(N, N) -> true; isdivby5(N, D) when (N false; isdivby5(N, D) when (N rem 2 == 0) and (D rem 2 == 0) -> isdivby5(N div 2, D div 2); isdivby5(N, D) when (N rem 2 == 0) -> isdivby5(N div 2, D); isdivby5(N, D) when (D rem 2 == 1) -> isdivby5((N + D) div 2, D); isdivby5(_, _) -> false.
Pattern matching and guards can be used to replace many traditional constructs in Erlang. In some cases they make the functions more readable, in others they do not — efficiency is almost always identical. It's up to you in each case to work out what's best.
In this case, we can use pattern matching to replace the case where N == D by using the same name for both inputs. This is a more succinct approach than
isdivby5(N, D) when N == D -> false;
Likewise, at the end of the function, we know that we have identified all cases where the result is anything other than false, so we can catch all other inputs with the isdivby5(_, _) pattern.
Which approach is best for you in a particular situation will always depend on the specifics of the situation, but by knowing a few different ways to write your functions, you'll be able to pick the best one when the time comes.