Программирование в Erlang
case
В Erlang есть три вида условных выражений.
Первый вид - выбор уравнения в определении функции при сопоставлении значения с образцами аргументов функции.
Второй вид - case-выражения, их выполнение похоже на выполнение уравнений в определении функции.
Третий вид - это if-выражения, которые можно представить как упрощённую версию case-выражений.
Общий вид case-выражений:
case conditional-expression of
Patternl -> expressionl, expression2, ..;
Pattern2 -> expressionl, expression2, ..;
...
Patternn - > expressionl, expression!, ..
end
В следующем примере в case-выражении вычисляется, принадлежность ли атом foo списку List.
Вычисления производятся с помощью стандартной функции member из библиотеки lists .
Если это так, возвращается атом ok, если нет, возвращается кортеж {error, unknown_element}:
case lists:member(foo,List) of
true -> ok;
false -> {error, unknown_element}
end
Case-выражения всегда возвращают значение, и вы запросто можете связать его с переменной.
Если в case-выражении сопоставление с каждым из уравнений пройдёт безуспешно, программа остановится с ошибкой.
if
If-выражения похожи на case-выражения без выражения-условия и ключевого
слова of:
if
Guardl -> expressionll, expressionl2, ...;
Guard2 -> expression21, expression22, ...;
...
Guardn -> expressionnl, expressionn2, ...
end
В качестве результата if-выражение возвращает значение последнего выражения, вычисленного в теле этого уравнения.
В следующем примере для переменной X проверяется содержит, ли она значе
ние, меньшее, большее или равное единице.
В следующем примере определяется четность числа двумя способами - с помощью операторов if и case:
if case X rem 2 of
X rem 2 == 1 -> odd; 1 -> odd;
X rem 2 == 0 -> even; 2 -> even
end end
Контролеры (guards)
Контролеры представляют собой дополнительные ограничения, которые могут быть применены в функциональных уравнениях либо case.
Вы можете использовать контролеры и при определении функции, в их заголовках, где
они следуют за ключевым словом when. Либо же вы можете их использовать в любом
месте программы, где можно использовать выражение. И если контролеры
используются как выражение, то они вычисляются до одного из атомов: true или false.
Считается, что если вычисление контролера привело к true, то его вычисление прошло
успешно. В противном случае - неудачным.
Примеры контролеров:
f(X,Y) when is_integer(X), X > Y, Y < 6 -> ...
Эта запись означает, что "когда Х это целое число и Х больше чем У и У меньше чем 6,
то ...". Запятая, разделяющая отдельные проверки в контролере означает логическое "И".
Точка с запятой (;) означает логическое "ИЛИ".
Пример:
my_add(X,Y) when no t ( ( ( X>Y ) or not ( is_ atom ( X ))) and (is_atom(Y) or (X==3.4))) -> X+Y.
Контролеры могут состоять из сложных сочетаний предикатов, но не могут включать функции, определённые пользователем.
Предыдущий пример можно записать и так:
my_add2(X,Y) when not(X>Y) , is_atom(X) ; not(is_atom(Y)) , X=/=3.4 -> X+Y.
Встроенные функции
Встроенные функции (BIF - built-in function) написаны в основном на С. Они являются частью виртуальной машины (ВМ).
Изначально предполагалось, что эти функции будут включены только в модуль
erlang. Но позже для повышения эффективности эти функции проникли и в другие модули. Среди них модули ets и lists.
Большинство BIF являются частью языка Erlang, но есть и такие, что зависят от
конкретной версии ВМ или даже реализации ВМ для данной операционной системы.
Стандартные встроенные функции импортируются автоматически, и вам не надо выписывать имя модуля перед именем такой функции.
Огромное количество BIF предназначено для работы с кортежами и списками:
hd - Возвращает первый элемент списка.
tl - Возвращает элементы списка, оставшиеся после изъятия первого элемента.
length - Возвращает длину списка,
size - Возвращает число элементов в кортеже.
element - Возвращает n-ый элемент кортежа.
setelement - Обновляет элемент кортежа и возвращает новый кортеж,
appendelement - Добавляет элемент в конец кортежа.
Примеры:
> List = [one , two , three, four , five].
[one,two,three,four,five]
> hd(List).
one
> tl(List).
[two,three,four,five]
> length(List).
5
> hd(tl(List)).
two
> Tuple = {1,2,3,4,5}.
{1,2,3,4,5}
> tuple size(Tuple).
5
> element(2,Tuple).
2
> setelement(3, Tuple, three).
{1,2,three,4,5}
> erlang:appendelement(Tuple, 6 ) .
{1,2,3,4,5,6}
Функция apply принимает три аргумента: имя модуля, имя экспортируемой
функции и список аргументов. Эта функция возвращает в качестве результата результат применения указанной в первом
и втором аргументах функции к аргументам, переданным третьим аргументом.
Функция apply может динамически определять, какую функцию вызвать, что лежит в основе обобщённых (generic) алгоритмов:
> Module = examples.
examples
> Function = even.
even
> Arguments = [10].
10
> apply(Module,Function,Arguments).
true
Функция date возвращает текущий календарный день в формате {Год,
Месяц, День}, а функция time возвращает текущее время в виде кортежа {Час,
Минута, Секунда}, функция now возвращает кортеж {Мегасекунды, Секунды,
Микросекунды}, сколько времени прошло с полуночи первого января 1970 года.
Функция now для каждого узла Erlang вернёт уникальное значение, даже если
она будет вызвана несколько раз в микросекунду, её можно использовать в качестве уникального идентификатора.
Ввод и вывод производятся с помощью функций из модуля io.
Операции для работы с файлами определены в модуле file.
Для чтения со стандартного ввода существует функция io:get line/1, принимающая на вход строку или атом,
которые будут показаны в качестве приглашения:
1> io:get_line( "line>" ) .
line>blablabla .
"blablabla, \n"
Также можно считать определённое число символов:
2> io :get_chars("tell me>",2).
tell me> er
"er"
Наиболее полезной является функция io:read/l, читающая терм Erlang со стандартного ввода:
> io: read ("ok, then»").
ok, then»atom.
{ok,atom}
> io: read ("ok, then»").
ok, then»{2,tue,{mon,"weds"}}.
{ok,{2,tue,{mon,"weds"}}}
> io: read ("ok, then»").
ok, then»2+3.
{error , { 1 . erl_parse , " bad term"}}
Вывод в Erlang производится с помощью функции io:write, которая распечатывает терм Erlang.
Но чаще всего пользуются функцией io:format, которая выполняет форматированный вывод. Функция io:format принимает следующие
аргументы:
• строку (или двоичные данные), определяющую формат вывода;
• список значений для печати.
Управляющая последовательность начинается с символа тильда (~), и в простейшем случае за ним следует один символ:
> List = [ 2 , 3 , math:pi()].
[2,3,3.141592653589793]
> Sum = lists:sum(List).
8.141592653589793
> io:format("Hello, world!~n",[]).
Hello, world!
ok
> io:format("the sum of ~w is ~w.~n", [[2,3,4],lists:sum([2,3,4])]).
the sum of [2,3,4] is 9.
ok
> io:format("the sum of ~w is ~w.~n", [List,Sum]).
the sum of [2,3,3.141592653589793] is 8.141592653589793.
ok
> io:format("the sum of ~W is ~w.~n", [List,3,Sum]).
the sum of [2,3j ... 3 is 8.141592653589793.
ok
7> io:format("the sum of ~W is ~f.~n", [List,3,Sum]).
the sum of [2,3|...] is 8.141593.
ok
Рекурсия
Задача: имеется список. Прибавим ко всем элементам списка единицу.
Прийдется создать новый список, куда будет записан новый результат.
Функцию назовем bump.
Исходный список может быть пуст - в этом случае получаем:
bump([]) -> [] ;
Если список содержит хотя бы один элемент, разделим его на голову и хвост и
создадим новый список, голова которого будет содержать сумму единицы с голо
вой исходного списка:
bump([Head | Tail]) -> [Head + 1 | bump(Tail).
Решение заключается в рекурсивном вызове функции на оставшейся части списка.
Т.е. функция имеет окончательный вид:
Ьшпр(П) -> [ ] ;
bump([Head | Tail]) -> [Head + 1 | bump(Tail)].
Мы вызываем ту же функцию, используя в ней те же переменные.
Переменные уникальны для каждого вызова, и на каждой итерации они считаются несвязанными.
Для каждого вызова функции в стеке вызовов создаётся новый фрэйм, в котором хранится информация о том,
в какое место нужно вернуть результат функции, параметры функции и локальные
переменные. Это важный аспект реализации вычислений в Erlang, несмотря на то
что вы не сталкиваетесь с ним напрямую. Мы к нему ещё вернёмся, когда речь
пойдёт о хвостовой рекурсии.
Рассмотрим более сложный пример.
Вычислим среднее арифметическое списка чисел. Назовём функцию average. Что такое среднее арифметическое?
Это есть сумма всех элементов списка, разделённая на его длину. Мы можем определить average следующим образом:
average(List) -> sum(List) / len(List).
Нужно определить 2 дополнительных функции - sum и len.
Окончательный вариант:
average(List) -> sum(List) / len(List).
sum([]) -> 0;
sum([Head | Tail]) -> Head + sum(Tail).
len([]) -> 0;
len([_ | Tail]) -> 1 + len(Tail).
Мы специально использовали функцию len вместо стандартной length, чтобы показать, как можно переопределить функцию.
Следующая задача: оставить в списке только четные элементы.
Есть три возможных случая: список пуст, голова списка четная, голова списка нечетная:
even([]) -> [ ] ;
even([Head | Tail]) when Head rem 2 == 0 -> [Head | even(Tail)];
even([_ | Tail]) -> even(Tail).
Следующая задача: напишем замену для стандартной функции member, которая определяет, принадлежит ли
элемент списку. Здесь возможна остановка рекурсии, которая может отработать не до конца, это называется
базой рекурсии (base case).
В этой задаче также три случая: список пуст, искомый элемент совпадает с головой списка,
искомый элемент не совпадает с головой списка:
member(_, [ ] ) -> false ;
member(H, [H | ]) -> true;
member(H, [ | Т]) -> member(H, T).
Теперь рассмотрим хвостовую рекурсию.
Вернемся к старом примеру с суммой списка:
sum([]) -> 0;
sum([Head | Tail]) -> Head + sum(Tail).
Здесь мы видим пример прямой рекурсии. Эту функцию можно определить по-другому, добавив дополнительный параметр
для самой суммы. В этом случае сумма будет выглядеть так:
sum_acc([],Sum) -> Sum;
sum_acc([Head|Tail], Sum) -> sum_acc(Tail, Head+Sum).
Эта рекурсия называется хвостовой, потому что ее тело состоит из вызова себя самой.
Перепишем функцию bump, которая прибавляет к каждому элементу списка единицу,
с помощью хвостовой рекурсии. Разница в том, что результирующий список нужно перевернуть:
bump(L) -> bump_acc(L, [ ] ) .
bump_acc([], Асc) -> reverse(Acc);
bump_acc([H | T ] , Acc) -> bump_acc(T, [H + 1 | Acc]).
А теперь сравните два алгоритма работы функции bump - старый с прямой рекурсией и новый с хвостовой.
Старый:
bump([1,2,3]) => [1+1 | bump([2,3])
bump([2,3]) => [2+1 | bump([3])
bump([3]) => [3+1 | bump([])
bump([]) => []
[4 | []]
[3 | [4]] => [3,4]
[2 | [3,4]] => [2,3,4]
Новый:
bump([1, 2, 3])
=> bump_acc([1, 2, 3 ] , [ ] )
=> bump_acc([2, 3 ] , [2])
=> bump_acc([3], [3, 2])
=> bump_acc([], [4, 3, 2])
=> reverse([4, 3, 2])
=> [2, 3, 4]
Т.е. мы видим, что в первом случае мы 2 раза проходим по списку.
Следующий пример: напишем функцию, которая складывает числа от 1 до заданной границы,
которая передаётся в функцию в качестве аргумента. Функция на С будет выглядеть так:
int sum(int boundary) {
int i, sum = 0;
for(i = 1 ; i <= boundary; i++)
sum += i;
return sum;
}
Аналогичная функция на эрланге:
sum(Boundary) -> sum_acc(l, Boundary, 0 ) .
sum_acc(Index, Boundary, Sum) when Index =< Boundary ->
sum_acc(Index + 1, Boundary, Sum + Index);
sum_acc(_I, B, Sum) -> Sum.
Рекурсивный вызов соответствует телу for-цикла.
Обработка ошибок
Основные типы ошибок:
function clause
case_clause
if_clause
badmatch
badarg
undef
badarith
Обрабатываются ошибки стандартным образом - с помощью try... catch
try Exprs of
Patternl [when Guardl] ->
ExpressionBodyl;
...
PatternN [when GuardN] ->
ExpressionBodyN
catch
[Classl: ]Excepti.onalPatternl
[when ExceptionGuardSeq 1] ->
ExceptionBodyl;
...
[ ClassN: ] ExceptionatPatternN
[when ExceptionGuardSeqN ]ExceptioriBodyN
end
Пример использования:
Х=2.
try (X=3) of
Val -> {normal, Val}
catch
_:_ -> 43
end.
Здесь все образцы ошибок из всех классов (соответствует образцу _:_ ) отображаются в число 43, которое возвращается в качестве результата.
В следующем варианте мы получим более конкретную ошибку:
try (X=3) of
Val -> {normal, Val}
catch
error:Error -> {error , Error }
4> end.
> {error,{badmatch,3}}
Функция throw позволяет нам возвращать ошибки собственного типа и обрабатывать их внутри t гу ... catch-выражения:
try (throw(nonnormalreturn)) of
Val -> {normal, Val}
catch
throw:Error -> {throw, Error}
end.
> {throw, non normal return}
Модули
Список наиболее важных стандартных библиотечных модулей эрланга:
array
calendar
diсt
erlang
file
filename
io
lists
math
random
string
timer
Примеры
Написать функцию sum2, которая по двум целым числам N и М, при N =< М, возвращает сумму всех чисел из интервала от N до М:
sum2(Min, Max) -> sum2(Min, Min, Max, 0 ) .
sum2(Index, Min, Max, Sum) when (Index =< Max) and (Index >= Min) ->
sum2(Index + 1, Min, Max, Sum + Index);
sum2(_I, _Min, _Max, Sum) -> Sum.
Написать программу, которая распечатывает числа в диапазоне от Min до Max:
n_print(Min, Max) -> n_print(Min, Min, Max) .
n_print(Index, Min, Max) when (Index =< Max) and (Index >= Min) ->
io:format("~p ~n",[Index]),
n_print(Index + 1, Min, Max);
n_print(_I, _Min, _Max) -> 0.
Для предыдущей задачи - дополнительное условие: распечатывать только четные числа:
n_print(Min, Max) -> n_print(Min, Min, Max) .
n_print(Index, Min, Max) when (Index =< Max) and (Index >= Min) ->
if Index rem 2 == 0 -> io:format("~p ~n",[Index]);
Index rem 2 == 1 -> false
end,
n_print(Index + 1, Min, Max);
n_print(_I, _Min, _Max) -> 0.
Функция, которая принимает на вход список целых чисел и одно целое число,
а возвращает список всех элементов списка, которые меньше либо
равны числу, переданному вторым аргументом - привожу три решения:
Вариант 1:
filter([H|T], N) when H>N -> filter(T, N);
filter([H|T], N) -> [H|filter(T, N)];
filter([], _) -> [].
Вариант 2:
filter([], _Max, Result) -> Result;
filter([Head | Tail], Max, Result) when Head =< Max ->
filter(Tail, Max, Result ++ [Head]);
filter([_Head | Tail], Max, Result) -> filter(Tail, Max, Result).
filter(List, Max) -> filter(List, Max, []).
Вариант 3:
filter([], _, Acc) -> lists:reverse(Acc);
filter([H|T], X, Acc) when H > X -> filter(T,X,Acc);
filter([H|T], X, Acc) -> filter(T,X, [H|Acc]).
filter(L,X) -> filter(L,X,[]).
Функция, которая преобразует список плоских списков в один список, соединяя все списки-элементы:
Вариант 1:
concatenate(L) ->
concatenate(L, []).
concatenate([], Result) ->
Result;
concatenate([Head | Tail], Result) ->
concatenate(Tail, Result ++ Head).
Функция, которая преобразует список списков произвольного уровня вложенности в один список, соединяя все списки-элементы
привожу два решения :
Вариант 1:
flatten(List) ->
flatten(List, []).
flatten([], Result) ->
Result;
flatten([Head | Tail], Result) ->
flatten(Tail, Result ++ flatten(Head));
flatten(Value, Result) ->
Result ++ [Value].
Вариант 2:
flatten([ ] ) -> [];
flatten([_|_]=L) -> flatten(L, []).
flatten([[_|_]=H | T], Tail) -> flatten(H, flatten(T, Tail));
flatten([[ ] | T], Tail) -> flatten(T, Tail) ;
flatten([ H | T], Tail) -> [H| flatten(T, Tail)];
flatten([ ], Tail) -> Tail.
Переходим к сортировке.
Реализуем следующие алгоритмы сортировки списков:
Быстрая сортировка
Голова списка считается опорным элементом. Затем список разделяется на
два: из тех элементов, что меньше опорного элемента, и остальных. Далее
эти списки сортируются так же, как и исходный список, и объединяются с
опорным элементом между ними.
Сортировка слиянием
Список разбивается на два с примерно одинаковым числом элементов в каждом их них.
После чего каждый из списков сортируется отдельно, и затем они
сливаются в один список.
Сначала быстрая сортировка:
Вариант 1:
quicksort([]) -> [];
quicksort([P|T]) ->
[L, H] = split(P, T, [], []),
quicksort(L) ++ [P] ++ quicksort(H).
split(P, [F|T], L, H) when F < P -> split(P, T, [F|L], H);
split(P, [F|T], L, H) -> split(P, T, L, [F|H]);
split(_, [], L, H) -> [L, H].
Вариант 2:
quicksort2([]) -> [];
quicksort2([Pivot | Rest]) ->
quicksort2([L || L <- Rest, L < Pivot]) ++ [Pivot] ++ quicksort2([R || R <- Rest, R >= Pivot]).
Теперь сортировка слиянием:
Вариант 1:
mergesort(L) ->
{L1B, L2B} = lists:split(length(L) div 2, L),
L1 = quicksort(L1B),
L2 = quicksort(L2B),
merge(L1, L2).
merge(L1, []) -> L1;
merge([], L2) -> L2;
merge([H1 | T1], [H2 | T2]) when H1 =< H2 -> [H1 | merge(T1, [H2 | T2])];
merge([H1 | T1], [H2 | T2]) -> [H2 | merge([H1 | T1], T2)].
Вариант 2:
mergesort2([]) -> [];
mergesort2(L) ->
[R] = merge2([[X]||X<-L]), R.
merge2([]) -> [];
merge2([A, B|T]) -> merge2([merge2(A, B)|merge2(T)]);
merge2([A]) -> [A].
merge2([A|TA], [B|_] = LB) when A [A|merge2(TA, LB)];
merge2([_|_] = LA, [B|TB]) -> [B|merge2(LA, TB)];
merge2([], LB) -> LB;
merge2(LA, []) -> LA.
Прочитать файл построчно и сохранить строки в списке:
read_file(F) ->
{ok, Data} = file:read_file(F),
rawdoc(binary_to_list(Data)).
rawdoc([]) -> [];
rawdoc(Data) -> rawdoc(Data, []).
rawdoc([], Line) ->
[lists:reverse(Line)];
rawdoc([$\n|Rest], Line) ->
[lists:reverse(Line)|rawdoc(Rest)];
rawdoc([C|Rest], Line) ->
rawdoc(Rest, [C|Line]).
Если вы хотите запустить исходник не из интерпретатора, а напрямую из командной строки,
делается это так: напишем следующий код и сохранима его в файле с именем sample.erl:
-module(sample).
-export([main/1]).
main([A]) ->
I = list_to_integer(atom_to_list(A)),
F = fac(I),
io:format("factorial ~w = ~w~n" ,[I, F]),
init:stop().
fac(0) -> 1;
fac(N) -> N*fac(N-1).
В нем мы определили функцию для нахождения факториала, а так же главную функцию main, которая будет читать
параметр, переданный из командной строки - в данном случае это будет 25 - и передавать его в функцию для вычисления
факториала. Для сборки и выполнения этой программы нужно набрать две команды в командной строке:
erlc sample.erl
erl -noshell -s sample main 25
Пакетная компиляция достигается в эрланге с помощью make-файлов. Ниже представлен шаблон для такого
make-файла. MODS - список всех модулей, которые надо скомпилировать.
Каждый модуль из строки MODS будет скомпилирован командой erlc Mod.erl .
Некоторые модули - такие, как special - будут подвергнуты специальной обработке.
В этом make-файле есть несколько целей - all, compile, special.beam .
# leave these lines alone
.SUFFIXES: .erl .beam .yrl
.erl.beam:
erlc -W $<
.yrl.erl:
erlc -W $<
ERL = erl -boot start_clean
# список модулей для компиляции
# Edit the lines below
MODS = module1 module2 \
module3 ... special1 ...\
...
moduleN
# The first target in any makefile is the default target.
# If you just type "make" then "make all" is assumed (because
# "all" is the first target in this makefile)
all: compile
compile: ${MODS:%=%.beam} subdirs
## special compilation requirements are added here
special1.beam: special1.erl
${ERL} -Dflag1 -W0 special1.erl
## run an application from the makefile
application1: compile
${ERL} -pa Dir1 -s application1 start Arg1 Arg2
# the subdirs target compiles any code in
# sub-directories
subdirs:
cd dir1; make
cd dir2; make
...
# remove all the code
clean:
rm -rf *.beam erl_crash.dump
cd dir1; make clean
cd dir2; make clean
Для того, чтобы запустить это make-файл, нужно выполнить команду, где Target -
необязательный параметр. По умолчанию это all.
> make[Target]
Еще короче make-файл можно записать так:
.SUFFIXES: .erl .beam
.erl.beam:
erlc -W $<
ERL = erl -boot start_clean
MODS = module1 module2 module3
all: compile
${ERL} -pa '/home/joe/.../this/dir' -s module1 start
compile:
${MODS:%=%.beam}
clean:
rm -rf *.beam erl_crash.dump
|