| Open Source Erlang |
|
|
The original article and examples was written by Claes Wikstrom in 1998. In april 2008, We removed an example that no longer works and revised the examples to eliminate warnings and use the modern type tests (when is_list(List) instead of when list(List)). In this document I aim to provide a number of small examples of nice Erlang programs. The goal of these examples is to let the reader get started immediataly doing some interesting stuff with the erlang programming environment. The document assumes some knowledge about erlang programming in general. Programs in this documentFor more hardcore like people who just want to see the source code, I provide a list of all Erlang examples described and commented in this document immediataly.
We start off with some examples of file handling.
Most off the stuff related to file handling resides in the
module called file.
The easist method of finding out which functions that
are available in the file module is
to read the manual page. The manual page is available
on UNIX systems by invoking the command Anyway, let's get started with some file IO. File IOCounting x'esOur first example is real simple, the idea is to open a file read the contents from the file and count the number of characters in the file. A file can be in either of two modes,binary or normal. In all our examples here all IO will
be in binary mode. This means that all IO that comes from
the file are Erlang binary data objects.
So lets create a new Erlang module. We do that by invoking
our favourite editor on a file, let's call it "count_chars.erl".
If we use the famous emacs editor, we can get a whole lot
of support in our Erlang programming. Turn on all the
bells and whistles, font-lock-mode and everything.
Anyway, The head of the file shall be:
%%%----------------------------------------------------------------------
%%% File : count_chars.erl
%%% Author : Claes Wikstrom <klacke@bluetail.com>
%%% Purpose : Count the x chars in a file
%%% Created : 20 Oct 1998 by Claes Wikstrom <klacke@bluetail.com>
%%%----------------------------------------------------------------------
-module(count_chars).
-author('klacke@bluetail.com').
-export([file/1]).
The actual code to open the file is contained in a function
file/1. This function has a minor flaw which we shall
soon rectify. But here goes:
file(Fname) ->
case file:open(Fname, [read, raw, binary]) of
{ok, Fd} ->
scan_file(Fd, 0, file:read(Fd, 1024));
{error, Reason} ->
{error, Reason}
end.
scan_file(Fd, Occurs, {ok, Binary}) ->
scan_file(Fd, Occurs + count_x(Binary), file:read(Fd, 1024));
scan_file(Fd, Occurs, eof) ->
file:close(Fd),
Occurs;
scan_file(Fd, _Occurs, {error, Reason}) ->
file:close(Fd),
{error, Reason}.
The
count_x(Bin) ->
count_x(binary_to_list(Bin), 0).
count_x([$x|Tail], Ack) ->
count_x(Tail, Ack+1);
count_x([_|Tail], Ack) ->
count_x(Tail, Ack);
count_x([], Ack) ->
Ack.
Now to compile and run this code we invoke the erlang system at the unix prompt and enter the following commands:
% erl
Erlang (JAM) emulator version 4.7.3
Eshell V4.7.3 (abort with ^G)
1> c:c(count_chars).
{ok,count_chars}
2> count_chars:file("count_chars.erl").
17
3>
So the file "count_chars.erl" contains 17 x'es. The abovementioned flaw is that we do not close the file in the same function where we opened the file. In this particular case we close it in the function just below, but it is a general good rule to release resources in the source code where they are allocated. One way to rectify this here is to rewrite the code like:
file(Fname) ->
case file:open(Fname, [read, raw, binary]) of
{ok, Fd} ->
Res = scan_file(Fd, 0, file:read(Fd, 1024)),
file:close(Fd),
Res;
....
However we can do better, we can write a general
purpose function General purpose file IOThe technique of generalizing a common pattern of functionality into a framework which uses higher order functions to do the work is a powerful lines-of-code saver. So the general purpose function is:
with_file(File, Fun, Initial) ->
case file:open(File, [read, raw, binary]) of
{ok, Fd} ->
Res = feed(Fd, file:read(Fd, 1024), Fun, Initial),
file:close(Fd),
Res;
{error, Reason} ->
{error, Reason}
end.
feed(Fd, {ok, Bin}, Fun, Farg) ->
case Fun(Bin, Farg) of
{done, Res} ->
Res;
{more, Ack} ->
feed(Fd, file:read(Fd, 1024), Fun, Ack)
end;
feed(Fd, eof, Fun, Ack) ->
Ack;
feed(_Fd, {error, Reason}, _Fun, _Ack) ->
{error, Reason}.
The user provides three arguments to the function.
Now the original
file1(File) ->
F = fun(Bin, Int) ->
{more, count_x(Bin) + Int}
end,
klib:with_file(File, F, 0).
As a matter of fact we can do even better than that, we can use a function defined in "lists.erl" which folds over a list. This is admittedly not code that ought to be in the first section of a beginner guide but we provide it anyway. We have:
file2(File) ->
{ok,B} = file:read_file(File),
lists:foldl(fun($x, Ack) ->
1 + Ack;
(_, Ack) ->
Ack
end, 0, binary_to_list(B)).
Now we have three version of the same function say we want to meassure how fast they are. The mudule "timer" has a function called "tc(Mod, Fun, Args)" which executes a function and times it. Lets do it at the shell prompt:
32> timer:tc(count_chars, file, ["index.html"]).
{59554,68}
33> timer:tc(count_chars, file1, ["index.html"]).
{69939,68}
34> timer:tc(count_chars, file2, ["index.html"]).
{335579,68}
The We see that the first (and most complicated) version is the fastest.
It takes 63 milli seconds whereas the next version which uses
the Word countWe continue with a program which is a little bit more useful than just counting the 'x' characters in a random file. We want to write a program that counts the number of words, chars and lines in a file. We wish the function to have an interface We wish the module to have an interface
-module(wc).
-import(lists, [map/2, foreach/2]).
-export([file/1, files/1]).
file(File) ->
output([gfile(File)]).
gfile(File) ->
Fun = fun(Bin, Count) ->
count_bin(binary_to_list(Bin), inspace, Count)
end,
{File, klib:with_file(File, Fun, {0,0,0})}.
count_bin([H|T], Where, {C,W,L}) ->
case classify_char(H) of
newline when Where == inspace ->
count_bin(T, inspace, {C+1, W, L+1});
newline when Where == inword ->
count_bin(T, inspace, {C+1, W+1, L+1});
space when Where == inspace ->
count_bin(T, inspace, {C+1, W, L});
space when Where == inword ->
count_bin(T, inspace, {C+1, W+1, L});
char ->
count_bin(T, inword, {C+1, W, L})
end;
count_bin([], inword, {C, W, L}) ->
{more, {C, W+1, L}};
count_bin([], inspace, {C, W, L}) ->
{more, {C, W, L}}.
classify_char($ ) ->
space;
classify_char($\t) ->
space;
classify_char($\n) ->
newline;
classify_char(_) ->
char.
files(Files) ->
output(map(fun(F) -> gfile(F) end, Files)).
output(Counts) ->
io:format("~-25s ~-10s ~-10s ~-10s~n",
["file", "chars", "words", "lines"]),
foreach(fun({File, {C,W,L}}) ->
ok = io:format("~-25s ~-10w ~-10w ~-10w~n",
[File, C, W, L])
end, Counts).
This function is really not very representable as an
example of file IO. All we do is call on the
previously define klib:with_file/3 function
with an appropriate Fun. An example session with the erlang
shell is:
60> {ok, L} = file:list_dir("."), wc:files(L).
file chars words lines
wc.beam 2079 40 21
count_chars.beam 1796 31 22
klacke_ex.html~ 6173 897 219
test 11 3 1
wc.erl~ 587 46 27
klacke_ex.html.orig 6173 897 219
klacke_ex.html 7642 1079 286
wc.erl 1661 176 73
count_chars.erl 1767 198 87
Finding filesNext example is a function that 'finds' files and does things with the files it finds. For example we might want to look for all erlang files in a directory tree and recompile them. We want to be able to write things like
find:files("/home/klacke",
".*\.erl", fun(F) -> {File, c:c(File)} end)
In order to fins all my erlang files and compile them.
We have three arguments.
-module(find).
-author('klacke@bluetail.com').
-include_lib("kernel/include/file.hrl").
-export([files/3]).
files(Top, Re, Action) ->
case file:list_dir(Top) of
{ok, Files} ->
files(Top, Files, Re, Action, []);
{error, Reason} ->
{error, {Top, Reason}}
end.
files(Top, [F|Tail], Re, Action, Ack) ->
F2 = Top ++ "/" ++ F,
case file:read_file_info(F2) of
{ok, FileInfo} when FileInfo#file_info.type == directory ->
case files(F2, Re, Action) of
{error, Reason} ->
{error, Reason};
List ->
files(Top, Tail, Re, Action, List ++ Ack)
end;
{error, Reason} ->
{error, {F2, Reason}};
{ok, FileInfo} when FileInfo#file_info.type == regular ->
case catch regexp:match(F, Re) of
{match, _,_} ->
files(Top, Tail, Re, Action, [Action(F2) | Ack]);
nomatch ->
files(Top, Tail, Re, Action, Ack);
{error, Reason} ->
{error, {F2, {regexp, Reason}}}
end;
Other ->
files(Top, Tail, Re, Action, Ack)
end;
files(_Top, [], _Re, _Action, Ack) ->
Ack.
The code includes the file_info record definition
from the file.hrl include file. The code is
completely straigtforward since it simply reads the
file_info records recursively and applies the
supplied function. It also includes the ".hrl" file
by means of an "include_lib" compiler dirctive.
A simple term loggerIn this final section on file IO we provide a simple term logger that writes Erlang terms to a file. The BIF
i32(B) when is_binary(B) ->
i32(binary_to_list(B, 1, 4));
i32([X1, X2, X3, X4]) ->
(X1 bsl 24) bor (X2 bsl 16) bor (X3 bsl 8) bor X4;
i32(Int) when integer(Int) ->
[(Int bsr 24) band 255,
(Int bsr 16) band 255,
(Int bsr 8) band 255,
Int band 255].
It is also nice to be able to read an integer from a file, the code in klib.erl is:
getint32(F) ->
{ok, B} = file:read(F, 4),
i32(B).
This is what I call an aggressive function. It
assumes that everything goes well and crashes
hard if for example the read call returns eof
or if the read attempt should fail for any other reason.
The module exports a number of functions, in
particular:
-module(slogger).
-author('klacke@bluetail.com').
-export([start/0, start/1, stop/0, log/1, upread/1, truncate/0]).
We need to be able to stop and start the server. The client functions are:
-define(LOGFILE, "slog.log").
start() ->
start(?LOGFILE).
start(F) ->
case whereis(?MODULE) of
undefined ->
register(?MODULE, spawn(?MODULE, loop0, [F]));
Pid ->
true
end.
stop()->
req(stop).
req(R) ->
?MODULE ! {self(), R},
receive
{?MODULE, Reply} ->
Reply
end.
The loop function that is spawned by the the start function has two part, an initialization part and a loop part. The coe is:
loop0(FileName) ->
case file:open(FileName, [read, write, raw, binary]) of
{ok, Fd} ->
{ok, Eof} = file:position(Fd, eof),
file:position(Fd, bof),
FilePos = position_fd(Fd, 0),
maybe_warn(FilePos, Eof),
loop(Fd);
{error, Reason} ->
exit(Reason)
end.
maybe_warn(FilePos, Eof) ->
if
FilePos == Eof ->
ok;
true ->
warn("~w bytes truncated \n",
[Eof - FilePos])
end.
We have added some extra code to check the logfile
when it is opened. We need to position the file descriptor
to the end of the logfile. To do that we could have
called
position_fd(Fd, LastPos) ->
case catch getint32(Fd) of
Int when is_integer(Int) ->
case file:read(Fd, Int) of
{ok, B} when size(B) == Int ->
position_fd(Fd, LastPos + 4 + Int);
_ ->
file:position(Fd, LastPos),
file:truncate(Fd)
end;
_ ->
file:position(Fd, LastPos),
file:truncate(Fd),
LastPos
end.
The
loop(Fd) ->
receive
{From, {log, Bin}} ->
From ! {?MODULE, log_binary(Fd, Bin)};
{From, {upread, Fun}} ->
From ! {?MODULE, upread(Fd, Fun)};
{From, truncate} ->
file:position(Fd, bof),
file:truncate(Fd),
From ! {?MODULE, ok};
{From, stop} ->
file:close(Fd),
From ! {?MODULE, stopped},
exit(normal)
end,
loop(Fd).
The
log_binary(Fd, Bin) ->
Sz = size(Bin),
case file:write(Fd, [i32(Sz), Bin]) of
ok ->
ok;
{error, Reason} ->
warn("Cant't write logfile ~p ", [Reason]),
{error, Reason}
end.
The only noticeable thing here is the type of the
structure that is passed to Finally the function to do the upreading, i.e a function that blocks the server for logging and then traverses the log and applies a function to each and every logged item is:
upread(Fd, Fun) ->
{ok, Curr} = file:position(Fd, cur),
file:position(Fd, bof),
upread(Fd, catch get_term(Fd), Fun).
upread(Fd, {'EXIT', _}, Fun) ->
ok;
upread(Fd, Term, Fun) ->
Fun(Term),
upread(Fd, catch get_term(Fd), Fun).
get_term(Fd) ->
I = getint32(Fd),
{ok, B} = file:read(Fd, I),
binary_to_term(B).
And finally the client functions to access the server:
upread(Fun) ->
req({upread, Fun}).
truncate() ->
req(truncate).
log(Term) ->
req({log, term_to_binary(Term)}).
A typical little caveat here with the
2> slogger:start().
true
3> slogger:log({abc, "cba"}).
ok
4> slogger:log(code:which(slogger)).
ok
5> slogger:upread(fun(X) -> io:format("~p~n", [X]) end).
{abc,"cba"}
"/home/super/klacke/doc/examples/slogger.jam"
ok
6>
A simple term logger (again)In this section we have rewritten the above term logger but this time using thegen_server module
as a utility to get help with the process structure.
The entire framework of a "do nothing" gen server
is generated and we just fill in the details. This way we
get all the goodies that come from gen servers in general.
They can be upgraded and downgraded wile running, they can
be debugged and traced and they fit into the general
The code is available in glogger.erl. Socket IOIn this section we shall give a number of examples of tcp/ip socket IO. Erlang is well suited for the implementation of a number of the classical internet tcp/ip based protocols. The API to the TCP/UDP/IP etc can be found it thegen_tcp, gen_udp
and the inet modules. All socket code written in
erlang is the same regardless of the underlying operating system
We start with a simple time server, it receives requests on TCP port 2345 and replies with a string indicating the current time. A time serverThe complete code is here and it is extremely simple. It just opens a socket a replies with the current data and time to anyone that connects to the server. We have:
start(Pno) ->
spawn(?MODULE, loop0, [Pno]).
loop0(Port) ->
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of
{ok, LSock} ->
loop(LSock);
_ ->
stop
end.
loop(Listen) ->
case gen_tcp:accept(Listen) of
{ok, S} ->
gen_tcp:send(S, io_lib:format("~p~n", [{date(), time()}])),
gen_tcp:close(S),
loop(Listen);
_ ->
loop(Listen)
end.
This is is a so called iterative server. It finishes each request until it accepts a new request. Another type of TCP/IP server creates a new process for each request. This is known as a concurrent or a threaded server. As we can expect erlang is well suited for the implementation of threaded servers. A threaded chargen serverOne of the most strange servers on many UNIX machines is the
It can thus act as an indefinite source of network input, typically used to test various applications. Nevertheless, this type of server need to be threaded. Thus we provide an example here with a threaded chargen server. The best way to write a threaded TCP server in Erlang is to
first create the listen socket, then create a new worker process which
does the call to
-define(PORTNO, 2019).
start_link() ->
start_link(?PORTNO).
start_link(P) ->
spawn_link(?MODULE, loop0, [P]).
loop0(Port) ->
case gen_tcp:listen(Port, [binary, {reuseaddr, true},
{packet, 0}, {active, false}]) of
{ok, LSock} ->
spawn(?MODULE, worker, [self(), LSock]),
loop(LSock);
Other ->
io:format("Can't listen to socket ~p~n", [Other])
end.
This code spawns off the initial worker process and goes into a loop. The loop is simple:
loop(S) ->
receive
next_worker ->
spawn_link(?MODULE, worker, [self(), S])
end,
loop(S).
It listens for
worker(Server, LS) ->
case gen_tcp:accept(LS) of
{ok, Socket} ->
Server ! next_worker,
gen_chars(Socket, 32);
{error, Reason} ->
Server ! next_worker,
io:format("Can't accept socket ~p~n", [Reason])
end.
gen_chars(Socket, Char) ->
Line = make_line(Char, 0),
case gen_tcp:send(Socket, Line) of
{error, Reason} -> exit(normal);
ok -> gen_chars(Socket, upchar(Char))
end.
make_line(Char, 70) ->
[10];
make_line(127, Num) ->
make_line(32, Num);
make_line(Char, Num) ->
[Char | make_line(Char+1, Num+1)].
upchar(Char) ->
if
Char + 70 > 127 ->
32 + (127 - Char);
true ->
Char + 70
end.
Name serverWe continue with a simple name server, a server that sits and
listens for TCP request on a port and then either responds to
"GET" requests or "PUT" request. We want the server to be threaded
so we have a similar setup phase as the Socket server frameworkThe
with_socket(Port, Fun) ->
spawn(?MODULE, with_socket0, [Port, Fun]).
with_socket0(Port, Fun) ->
process_flag(trap_exit, true),
case gen_tcp:listen(Port, [binary, {packet, 0},
{reuseaddr, true},
{active, false}]) of
{ok, LSock} ->
P = spawn_link(?MODULE, sock_handler, [self(), Fun, LSock]),
sock_loop(P, Fun, 1, nil, LSock);
Other ->
Other
end.
An ftp daemonIn this section we introduce a complete implementation of an ftp daemon according to RFC 765. We don't comment on the code merely provide it. The server is almost 1000 lines of Erlang code. The code is at ftpd.erl Gui programmingWe provide the boring game of mines as an example of an gs based program: gsmines.erl |
|||||
| Last updated 2009-10-13 16:39 UTC | ||||||