What does this F# code look like in Erlang – Part 2 of N

Related Posts

 

if-else

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

    else

        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’

    end.

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.

 

match-with

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

    end.

 

Type Conversions

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

“42”

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

"hello"

4> % you can use the $ operator to find out the integer value of a character

4> $/.

47

5> $n.

110

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 Tests

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

    end.

 

Recursive Functions

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:

-module(recursion).

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