Let’s continue what we started in Part I. We learned how to print some output once:
-module(hello).
-export([start/0]).
start() -> io:format("Hello world!~n").
Now let’s try doing it many times, and for doing anything multiple times we’ll need to work with lists. We’ve already used one in our export
statement and in some of the code in Part I as start(X) -> io:format("X is ~w!~n", [X]).
when we passed a list as an argument, but we never handled one ourselves so let’s change that. Let’s update the program to print a compile-time list:
-module(hello).
-export([start/0]).
start() ->
io:format("[ "),
print([1, 2, 3, 4]).
print([Head | Tail]) -> io:format("~w, ", [Head]), print(Tail);
print([]) -> io:format("]~n ").
The print
function handles lists the way functional languages do by recursively splitting them into a head and a tail and walking over the list this way. We detect reaching the end via pattern matching on []
and printing the terminator accordingly. Putting the code above in hello.erl
and running it through the Erlang shell erl
the output is this:
1> c(hello).
{ok,hello}
2> hello:start().
[ 1, 2, 3, 4, ]
ok
Now let’s get rid of that trailing comma in the end - we’ll modify the print
function and add a version to match a list with exactly one item and print that without a trailing comma:
-module(hello).
-export([start/0]).
start() ->
io:format("[ "),
print([1, 2, 3, 4]).
print([LastItem | []]) -> io:format("~w ]~n", [LastItem]);
print([Head | Tail]) -> io:format("~w, ", [Head]), print(Tail);
print([]) -> io:format("]~n ").
The output from this looks slightly better:
1> c(hello).
{ok,hello}
2> hello:start().
[ 1, 2, 3, 4 ]
ok
With this specific list we don’t actually need the third definition of print
matching an empty list, but we’ll keep it there since we plan to extend the code to handle any lists, even empty ones. And to do that we’ll have to dynamically generate lists, which we’ll be doing using the seq/2
function in the lists
module:
1> lists:seq(1, 4).
[1,2,3,4]
Let’s update our program to print the list from 1 to N:
-module(hello).
-export([start/1]).
start(N) ->
io:format("[ "),
print(lists:seq(1, N)).
print([LastItem | []]) -> io:format("~w ]~n", [LastItem]);
print([Head | Tail]) -> io:format("~w, ", [Head]), print(Tail);
print([]) -> io:format("]~n ").
And try running it with a few values:
1> c(hello).
{ok,hello}
2> hello:start(4).
[ 1, 2, 3, 4 ]
ok
3> hello:start(6).
[ 1, 2, 3, 4, 5, 6 ]
ok
While we can manually iterate over lists like this, Erlang has a lists:foreach/2
function where the first argument is a function and the second argument is a list, and it does exactly what it says on the can. Let’s change our program to print lists vertically using foreach
:
-module(hello).
-export([start/1]).
start(N) ->
io:format("[~n"),
lists:foreach(fun print_element/1, lists:seq(1, N)),
io:format("]~n").
print_element(Element) -> io:format("~w~n", [Element]).
Note the syntax being fun [name/arity]
. Compiling and running this produces the following output:
1> c(hello).
{ok,hello}
2> hello:start(3).
[
1
2
3
]
ok
We can also do this with fun
s, which you can think of as Erlang’s lambdas. The syntax is fun([argument list]) -> [statements] end
. Rewriting the code to use fun
s yields this:
-module(hello).
-export([start/1]).
start(N) ->
io:format("[~n"),
lists:foreach(fun(Element) -> io:format("~w~n", [Element]) end, lists:seq(1, N)),
io:format("]~n").
And the output is the same:
49> c(hello).
{ok,hello}
50> hello:start(3).
[
1
2
3
]
ok
Lastly, let’s dip our toes into concurrency. Erlang has the concept of processes, which are internal to the Erlang VM and so do not map 1:1 to operating system processes - they are much lighter weight, you can have many thousands of them, and context switching is cheap. They also share no data and communication happens mostly via argument passing on creation and message passing during runtime. Processes are spawned using the intuitively named spawn
function, so let’s do just that:
-module(hello).
-export([start/0]).
start() ->
spawn(fun () -> io:format("Hello from spawned process~n") end),
io:format("Hello from spawner process~n").
The output in my case was
1> c(hello).
{ok,hello}
2> hello:start().
Hello from spawner process
Hello from spawned process
ok
But as with any parallel process, the ordering isn’t deterministic so YMMV. Let’s extend this to create N processes that print their own process index M times:
-module(hello).
-export([start/2]).
start(N, M) ->
io:format("Beginning to spawn...~n"),
lists:foreach(fun (Index) -> create_child(Index, M) end, lists:seq(1, N)),
io:format("Spawning done.~n").
create_child(ChildIndex, Count) -> spawn(fun () -> child_main(ChildIndex, Count) end).
child_main(ChildIndex, Count) ->
lists:foreach(fun (Iteration) -> io:format("Child ~w | Iteration ~w ~n", [ChildIndex, Iteration]) end, lists:seq(1, Count)).
Here’s the output from my run:
1> c(hello).
{ok,hello}
2> hello:start(3, 4).
Beginning to spawn...
Spawning done.
Child 1 | Iteration 1
Child 2 | Iteration 1
Child 3 | Iteration 1
Child 1 | Iteration 2
Child 2 | Iteration 2
Child 3 | Iteration 2
ok
Child 1 | Iteration 3
Child 2 | Iteration 3
Child 3 | Iteration 3
Child 1 | Iteration 4
Child 2 | Iteration 4
Child 3 | Iteration 4
I’ll conclude Part II at this point - in Part III I’ll use the primitives described so far to write an actual server application: solving the C10k problem.