Erlang Syntax for Haskellers

Module system and Imports

Erlang supports an ‘import’ functionality, but it’s rarely used. Instead it’s customary to use the fully-qualified path of any non-system function. You don’t import Foo and then call its bar function. Instead you just write foo:bar() everywhere in the program.

There are no “hierarchical modules”. Every module is self-contained and has no sub-modules.

In order for foo:bar() to be legal, the foo module must export the bar function. Any module can specify lists of functions it exports, as follows:

-export([bar/0, baz/2]).

There can be more than one -export() line. The numbers after the slashes are mandatory, and specify how many arguments the function has. This is necessary because in Erlang, unlike haskell, it’s possible to have two functions named the same that take different numbers of arguments.

Don’t forget to export whatever functions you write in your module that you plan to call from another module. Only exported functions can be called by functions outside that module. Put another way, any non-exported functions are “internal” to the module and cannot be called from outside.

Variables

Variables in Erlang start with capital letter, and anything starting with a capital letter (or underscore) is interpreted as a variable.

Variables are immutable; that is, they cannot be reassigned to. In this sense they operate like haskell’s let definitions.

Erlang’s = operator, like Haskell’s, performs matching, not assignment. For example, {X,Y} = Foo extracts the members of the 2-tuple stored in Foo . If Foo is not a 2-tuple, this pattern match fails and Erlang throws a runtime error. This is not checked at compile-time! Because Erlang does late binding, the compiler really only checks the syntax of your code. It does no type-checking whatsoever.

Atoms

Erlang makes very heavy use of atoms, which are string-like symbols. These are just bare lowercase words with no quotes. For example, many functions which perform some side effect, but don’t return anything meaningful, return the atom ok by convention when there’s no error, or a tuple {error, Reason} when something is wrong.

In Haskell, you would do this by defining a datatype with the status code, something like:

data Status = Ok | Error String

Erlang atoms are just symbols. They cannot be broken apart, concatenated, or have their letters rearranged like strings. They are used exclusively for pattern-matching, and you match on the entire atom. You cannot pattern-match on whether an atom starts with a given letter; for that you need a string.

Strings

Strings in Erlang are delimited by double-quotes only. “lol wut” is a string. Erlang strings have some peculiarities. Like Haskell strings, “foo” is actually syntactic sugar for a list of 3 characters, which in Erlang is represented by [$f, $o, $o]. You can therefore easily pattern-match on string prefixes, using the ++ operator (which works as it does in Haskell, as a general list-append operator):

case Str of "h" ++ _ -> "starts with h"; _ -> "does not start with h" end,

This also has the effect that to convert a string to an integer, you use a function called list_to_integer .

Unlike in Haskell, there’s no function like putStrLn , or if there is, it’s not favored. Instead, the usual practice is to use the io:format(Str, [Values]) function, where Str is a string containing formatting placeholders like ~p , which pretty-prints any Erlang term, ~n which prints a newline, etc, and Vars is a list of the arguments to fill in, in order. Much like C’s printf() .

print_numbers(Num1, Num2) -> io:format("first number is ~p, second is ~p~n", [Num1, Num2]).

The format escape ~p means to pretty-print the given Erlang term. There is anothter format ~w that simply string-izes a term and writes it out. However, because strings are lists of integers corresponding to unicode points, if you use ~w instead of ~p with a string, it will be rendered as a list of integers.

Eshell V5.8.3 (abort with ^G) 1> io:format("~w~n", ["foo"]). [102,111,111] ok 2> io:format("~p~n", ["foo"]). "foo" ok

Top-level functions

There are no type signatures in Erlang. The compiler does not check arguments or return values. The value of the final expression in the function body becomes the return value.The syntax looks like this:

foo() -> do_stuff(), 27.

This function calls the do_stuff() function, then returns the value 27. Note that if do_stuff returns a value, that value is simply discarded.

Punctuation

Unlike haskell, whitespace is never significant. Instead, Erlang relies on natural-language-like statement separators. These are inherited from the Prolog programming language, an early ancestor of Erlang. Mostly this is not too bad, but there’s a major nuisance about it: in a group of N clauses, the Nth clause uses a different statement separator than all the others.

Statements in a function are separated by commas. The final statement must be followed by a period.

The case that trips up beginners is the separation of clauses in pattern-matching. This occurs both with top-level functions (analogous to multiple equations in haskell) and with case expressions. Here are examples of both. Note the semicolons and periods.

% first clause of function foo(1) -> "the value is one"; % second clause foo(2) -> "the value is two"; % final clause foo(_) -> "the value is neither one nor two".

Here is an example of a case statement inside a function. This function inspects the variable X , prints a message to the terminal depending on its value, and returns the atom ok .

foo(X) -> case X of 1 -> io:format("one"); 2 -> io:format("two"); % no punctuation here! _ -> io:format("neither one nor two") end, ok.

Pro tip: Think of the statement separators as follows:

semicolon = “next clause”

comma = “next statement”

period = “end of statements”

This will help you avoid sticking extra (illegal) statement separators in your code.

List constructors

Haskell: head (x:xs) = x

Erlang: head([X|Xs]) -> X.

Anonymous functions (lambdas)

Haskell: let f = \x y -> x + y

Erlang: F = fun(X,Y) -> X + Y end

Functional Programming Idioms

The syntax in Erlang is clumsier, but it’s all there: map, fold, etc.

Haskell: map (\x -> x +1) [1,2,3]

Erlang: lists:map(fun(X) -> X + 1 end, [1,2,3]).

An annoying aspect of Erlang is that you cannot simply supply the name of a top-level function as an argument to higher-order functions like lists:map/2 .

myFunc(X) -> X + 1. lists:map(myFunc, [1,2,3]), % INCORRECT lists:map(fun myFunc/1, [1,2,3]). % CORRECT

You can see from this example that in order to pass a top-level function to another function, you have to write fun funcName/Arity , instead of just the name of the function. However, you can pass functions from other modules by tacking on the module name, i.e. fun my_module:my_func/Arity .

Erlang has list comprehensions, but the syntax is slightly different from Haskell’s:

Haskell: [ (x,y) | x <- [1,2], y <- [4,5] ]

Erlang: [ {X,Y} || X <- [1,2], Y <- [4,5] ].

As a Haskeller, you’re going to get annoyed at some point by Erlang’s lack of support for currying or partial applicaiton. This makes functions like map and fold somewhat less useful, but you can usually get around this omission by using closures.

The Shell

Haskell: ghci foo.hs – OR – ghci then :load foo.hs

Erlang: erl

Then when the shell loads, type c(foo). Don’t forget the period, and note that you don’t supply the .erl extension – it is assumed.

You can also compile files on the commandline by using erlc . A file foo.erl compiles to foo.beam . You can then run the shell, specifying directories containing .beam files with the -pa switch: erl -pa dir1/ dir2/ . See man erl for more information on this.

The Erlang shell has a lot of cool stuff having to do with inspecting currently running processes, debugging, etc. Consult the manual page for the info.

Records

Erlang’s record system is one of its weaknesses, mostly because it’s a bolt-on compiler trick rather than a fully supported language feature. This post gives a good tutorial introduction. The main thing the beginner needs to remember is that you always have to write the record name when dealing with a record. Suppose we have this record definition:

-record(person, { name, age}).

Now we can write P = #person{ name = "ben", age = 99} . However, to access the age field of P , you can’t write P.age . Instead you must write P#person.age .