Check out my new course Learn you some Lambda best practice for great good! and learn the best practices for performance, cost, security, resilience, observability and scalability.
In F#, if-else if-else control flow is expressed in the form if Exp then Exp elif Exp then Exp else Exp:
let f n =
if n = 42 then
printfn "%d is the answer to the ultimate question of life, the universe and everything" n
elif n % 2 = 0 then
printfn "%d is even" n
printfn "%d is odd" n
In Erlang, there’s not explicit else if or else clauses, instead you just specify multiple clauses and the last catch-all (true –> …) is Erlang’s equivalent of the ‘else’ clause.
f (N) –>
if N =:= 42 -> io:format("~p is the answer to the ultimate question of life, the universe and everything", [N]);
N rem 2 =:= 0 -> io:format("~p is even", [N]);
true -> io:format("~p is odd", [N]) % this is Erlang’s equivalent to an ‘else’
When you’re writing Erlang you’re usually discouraged from using ‘else’ or ‘true’ branches altogether and should instead replace them with if clauses that cover all logical branches to help avoid masking otherwise hard-to-detect bugs.
The preferred way (for me at least) to do conditional branching in F# is to use the match-with expression rather than using ifs. The logic expressed in the function f from the above example can easily be expressed in a match-with instead:
let g n =
match n with
| 42 -> printfn "%d is the answer to the ultimate question of life, the universe and everything" n
| _ when n % 2 = 0 -> printfn "%d is even" n
| _ -> printfn "%d is odd" n
Erlang’s counterpart to match-with is the case-of expression, which looks a lot like match-with:
g (N) –>
case N of
42 -> io:format("~p is the answer to the ultimate question of life, the universe and everything", [N]);
_ when N rem 2 =:= 0 -> io:format("~p is even", [N]);
_ -> io:format("~p is odd", [N])
In F#, you can cast a value of type T into a value of type V like this:
let n = int “42”
provided that there exists an explicit cast operator that lets you convert a value of type T to type V.
In Erlang, type conversion is achieved with the help of built-in functions that are generally named T_to_V where T and V are the names of the source and destination types:
N = list_to_integer(“42”).
There are many other such built-in functions, including: atom_to_binary, atom_to_list, binary_to_atom, list_to_atom, integer_to_list, …
From the above example, you might be wondering why the built-in function that converts a string to an integer is called list_to_integer as opposed to string_to_integer. This is because there is no real string type in Erlang!
In Erlang, strings are lists of integers whose elements are all integers that represent printable characters:
1> [52, 50].
2> % 2 is not a printable character, so this is treated as an integer list
2> [52, 50, 2].
[52, 50, 2]
3> [ 104, 101, 108, 108, 111 ].
4> % you can use the $ operator to find out the integer value of a character
I’m sure many a developer has enjoyed a wonderful WTF moment when they realised the lack of a native string type in Erlang, but given its origin in the telecom world where string manipulation is seldom required it’s perhaps not surprising that the language designers never saw fit to make string manipulation more natural.
But seriously, WTF!
Type testing is performed with the : ? operator in F#:
let f (n : obj) =
match n with
| : ? int as nInt -> printfn "%d is an integer" nInt
| : ? float as nFloat -> printfn "%f is a float" nFloat
| : ? string as nStr -> printfn "%s is a string" nStr
In Erlang, you can use a number of built-in functions named is_T where T is the name of the type:
f (N) –>
case N of
_ when is_integer(N) -> io:format("~p is an integer", [N]);
_ when is_float(N) -> io:format("~p is a float", [N]);
_ when is_list(N) -> io:format("~p is a list", [N])
A recursive function in F# needs to be decorated with the rec keyword, a factorial function in F# would look like this:
let rec fac n =
match n with
| 0 –> 1
| _ -> n * fac(n – 1)
In Erlang, you can specify multiple overloads (called function clauses) for the same function in a style similar to facts in Prolog, recursion is a simple matter of having one or more of these clauses call itself:
fac (N) when N =:= 0 -> 1;
fac (N) when N > 0 -> N * fac(N – 1).
You might notice that neither implementations above are tail recursive. To make tail recursive versions of the above examples, simply add an accumulator to the function, the F# version will probably look something along the lines of:
let fac n =
let rec tailFac n acc =
match n with
| 0 –> acc
| _ -> tailFac (n – 1) (n * acc)
tailFac n 1
Notice how I implemented the tail recursive function as an inner function to fac so that the consumers of fac have a natural and easy to use API and don’t have to be aware that the accumulator needs to be initialized with 1 for it to function correctly (which is an implementation detail that one should not depend upon).
This type of encapsulation is easy to achieve in Erlang and the above will translate nicely into:
-export([fac/1]). % only expose the fac (N) function to consumers of this module
fac (N) -> tail_fac(N, 1).
%% these functions are private to the module
tail_fac (0, Acc) -> Acc;
tail_fac (N, Acc) when N > 0 -> tail_fac(N – 1, N * Acc).
Higher Order Functions
In F#, the List module defines a number of high-order functions that lets you perform map, filter, zip, etc. operations against a list, e.g. to double every element in a list you can write:
let doubles = List.map (fun x -> x * 2) [1..10]
// you can also write the above as the following
let doubles2 = List.map ((*) 2) [1..10]
This code will look something along the line of:
Doubles = lists:map(fun (X) -> X * 2 end, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).
With Erlang’s anonymous functions you can also specify multiple function clauses like you can with a normal function and make use of when guards:
NewList = lists:map(fun (1) –> 10;
(X) when X rem 2 =:= 0 -> X * 2;
(X) -> X * 3 end,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).
I specialise in rapidly transitioning teams to serverless and building production-ready services on AWS.
Are you struggling with serverless or need guidance on best practices? Do you want someone to review your architecture and help you avoid costly mistakes down the line? Whatever the case, I’m here to help.
Check out my new course, Learn you some Lambda best practice for great good! In this course, you will learn best practices for working with AWS Lambda in terms of performance, cost, security, scalability, resilience and observability. Enrol now and enjoy a special preorder price of £9.99 (~$13).
Are you working with Serverless and looking for expert training to level-up your skills? Or are you looking for a solid foundation to start from? Look no further, register for my Production-Ready Serverless workshop to learn how to build production-grade Serverless applications!
Here is a complete list of all my posts on serverless and AWS Lambda. In the meantime, here are a few of my most popular blog posts.
- Lambda optimization tip – enable HTTP keep-alive
- You are thinking about serverless costs all wrong
- Many faced threats to Serverless security
- We can do better than percentile latencies
- I’m afraid you’re thinking about AWS Lambda cold starts all wrong
- Yubl’s road to Serverless
- AWS Lambda – should you have few monolithic functions or many single-purposed functions?
- AWS Lambda – compare coldstart time with different languages, memory and code sizes
- Guys, we’re doing pagination wrong