Введение в Erlang
Эрланг обычно входит в стандартный список пакетов наиболее популярных дистрибутивов.
Для пакетной установки эрланга в зависимости от вашего дистрибутива нужно набрать что-то типа
yum install erlang
или
zypper install erlang
Можно собрать и поставить эрланг из исходников - смотрите http://www.erlang.org/doc/installation_guide/INSTALL.html
Итак, эрланг установлен, запускаем в командной строке интерпретатор erl
$ erl
Erlang R15B03 (erts-5.9.3) [source] [smp:2:2]
Eshell V5.9.3 (abort with ^G)
1>
Базовые типы
Набираем комментарий и арифметическое выражение:
1> % вычислим сумму двух чисел
1> 20+30.
50
2>
Обратите внимание, что после суммы мы ставим точку, это говорит интерпретатору, что команда закончена,
и нажав enter, мы получаем ответ - 50.
Можно проверить, как работает арифметика больших чисел:
2> 1234567878 * 345345345345345345.
426352270180180179753827910
Числа с основанием, отличным от 10, записываются в следующем виде: Основание#3начение.
Основание является целым числом из интервала от 2 до 16, а
значение — числом, имеющим заданное основание. К примеру, 2#1010 означает
число 10 с основанием 2 и - 1б#ЕА означает число -234 с основанием 16, поскольку
буквы от А до F кодируют числа от 10 до 15 для чисел с основанием 16.
В Erlang действительные числа (floats) представлены типом Float, например:
1.234Е-10
Запись Е-10 означает, что точка смещена на 10 разрядов влево. Так, выражение
- 1 01.234Е-10 является записью числа 1.234 • Ю или 0.0000000001234. Точность
действительных чисел представлена в виде 64 битов и соответствует стандарту
IEEE 754-1985.
Теперь переходим к переменным. Все переменные должны начинаться с большой буквы, за ней
могут следовать заглавные и строчные буквы, целые числа, а также символ подчеркивание:
> My_var = 1.
1
Если мы попробуем этой переменной присвоить другое значение - получим ошибку:
> My_var = 2.
** exception error: no match of right hand side value 2
Здесь нас ждет шок: переменные в эрланге не изменяются, их можно проинициализировать всего один раз.
Авторы языка говорят, что это дисциплинирует мышление.
Во всех языках программирования оператор = означает присваивание, а в эрланге это не так -
здесь это сопоставление по образцу. В эрланге переменная - это указатель на область памяти, которую нельзя изменить.
В эрланге это сделано для того, чтобы не было проблем с разделяемой памятью и блокировками,
которые есть в сях или жабе.
В Erlang вызов переменных является вызовом по значению (call by value): все аргументы функции вычисляются перед выполнением тела функции.
Вызов по ссылке невозможен.
Erlang является языком с динамической типизацией (dynamic type system).
Типы определяются во время исполнения, также как и правомерность применения операций к переменным.
В Erlang нет хорошей системы типов, потому что на момент создания языка
ни один из разработчиков не знал, как её реализовать.
Несколько человек пытались добавить к Erlang статическую типизацию.
К сожалению, некоторые особенности языка, появившиеся тогда, когда он был
только изобретен, не позволили ни одной из групп разработчиков в этом преуспеть.
В текущей реализации ВМ Erlang используется копирующий, последовательный сборщик мусора.
Сборка мусора проводится отдельно для каждого запущенного процесса: если одному из процессов не хватает памяти, запускается сборщик мусора.
Теперь перейдем к атомам.
Они служат тем же целям, что и тип перечисление.
Если взять например си, то в нем константы записываются так:
#define OP_READ 1
#define OP_WRITE 2
#define OP_SEEK 3
В эрланге все атомы начинаются с прописной буквы - это основное отличие от переменных.
Атомы можно заключить в одинарные кавычки.
В Erlang не выделено отдельного типа для логических значений (booleans). Вместо этого
совместно с операциями сравнения используются атомы true и false.
> not((l < 3) and (2==2)).
false
Логические операторы:
and
andalso
or
orelse
xor
not.
Операторы сравнения:
== Равно
/= Не равно
=:= В точности равно
=/= В точности не равно
=< Меньше либо равно
< Меньше
>= Больше либо равно
> Больше
Кортежи
Кортеж (tuple) - это коллекция фиксированного размера,
данные внутри одного кортежа могут быть произвольного типа. Кортеж заключен в фигурные скобки:
> {f1, 2}.
Данный кортеж состоит из атома и целого числа. Кортежи похожи на сишные структуры, только анонимные.
Например, в си можно создать структуру точка и потом ее проинициализировать с помощью оператора точка:
Если на первом месте кортежа стоит атом, то этот элемент называют тегом (tag).
struct point
{
int x;
int y;
} P;
P.x = 10; P.y = 45;
В эрланге нет оператора точка. Ту же структуру, только сразу проинициализированную, в эрланге можно написать
с помощью кортежа так:
> P = {10, 45}
Но лучше для удобства записать так -
здесь атом фактически является именем структуры - так рекомендовано отцами языка :-)
> P = {point, 10, 45}
Чтобы извлечь данные из этой структуры в переменные X и Y:
> {point, X, Y} = Point.
Если у вас имеется сложный кортеж и вы хотите извлечь из него данные, то вы можете
написать кортеж такой же формы (структуры) как и ваш, но в тех местах откуда вы
хотите извлечь данные поместите несвязанные переменные. (Этот метод извлечения
данных путем сопоставления по образцу называется унификацией и используется во
множестве функциональных и логических языков программирования.)
Кортежи могут быть вложенными:
> Person = {person,
{name, joe},
{height, 1.82},
{footsize, 42},
{eyecolour, brown}}.
Обращаю внимание, что для цвета глаз используются атомы как для задания имени поля, так и для его значения.
Как извлечь отдельное поле из какой-то сложной структуры ?
Сначала проинициализируем сложную структуру:
> Person={person,{name,{first,joe},{last,armstrong}},{footsize,42}}.
После чего напишем образец для инициализации отдельного поля:
> {_,{_,{_,Who},_},_} = Person.
Здесь символ подчеркивания называется анонимной переменной.
У кортежей есть встроенные функции, далее по порядку применяются функции:
запрос размера, запрос элемента по номеру, обновление элемента кортежа и сравнение кортежей:
1> tuple_size({abc, {def, 123}, ghi}).
3
2> element(2,{abc, {def, 123}, ghi}).
{def,123}
3> setelement(2, {abc, {def, 123}}, def).
{abcdef}
4> {1,2}<{1,3}.
true
Кортежи индексируются с 1, а не с 0.
Списки
Списки, как и кортежи, могут хранить обьекты произвольного типа.
Списки - коллекции, заключенные в квадратные скобки:
> ThingsToBuy = [{apples,10},{pears,6},{milk,3}].
> [1+7,hello,2-2,{cost, apple, 30-20},3].
Первый элемент списка называется головой списка, все остальное - хвост.
Голову списка можно отделить от хвоста вертикальной чертой:
> [1 | 2].
Для списка [1,2,3] 1 будет являться головой списка, а [2,3] — хвостом, этот
список может быть записан в виде [ 1 | [2,3]]. Выполнив аналогичную операцию с
хвостом списка, получим [1| [2| [3]]] и далее [ 1 | [ 2 | [ 3 | [ ] ] ] ] . Этот список мож
но записать ещё одним способом [ 1 , 2 | [ 3 | [ ] ] ] , перед применением конструк
тора списков могут следовать несколько элементов, разделённых запятыми. Все
эти списки эквивалентны исходному списку [1,2,3]. Список, последний хвост
которого равен пустому списку, называют регулярным (proper list) или правильно
построенным (well-formed list). В следующем примере все списки эквивалентны:
[one, two, three, four]
[one, two, three, f o u r | [ ] ]
[one, two|[three, f o u r] ]
[one, t w o | [ t h r e e | [ f o u r | [ ] ] ]
[one|[two|[three|[four| [ ] ] ] ] ]
Как и из всего остального, мы можем извлекать элементы из списка с помощью
оператора сопоставления по образцу. Если у нас имеется не пустой список L , тогда
выражение [X|Y]=L , где Х и У - это несвязанные переменные, поместит голову списка в
Х, а хвост списка - в У.
В кортеже нет возможности отделить голову.
У списков есть встроенные функции, которые нужно вызывать с префиксом lists :
> lists:max([1,2,3]).
> lists:reverse([1,2,3]).
> lists:sort([2,l,3]).
> lists:split(2,[3,4,10,7,9]).
> lists:zip([l,2,3],[5.6,7]).
> lists:delete(2,[l,2,3,2,4,2]).
> lists:last([l,2,3]).
> lists:member(5,[1,24]).
> lists:nth(2.[3,4,16,7,9]).
> length([l,2,3]).
Списки можно складывать:
> [l,2,3]++[4,5,6].
[1,2,3,4,5,6]
Списки можно вычитать:
> [l,2,2,3,4,4]--[2,4].
[1,2,3,4]
Прибавить элемент к списку можно двумя способами - в голову с посощью конструктора:
> [1 | [2,3,4]].
и с помощью сложения:
> [1] ++ [2,3,4].
Конструктор быстрее, чем сложение.
Строки в эрланге являются частным случаем списков.
> [65,66,67].
"ABC"
Строку можно проинициализировать с помощью двойных кавычек :
> Str = "asdfsdf".
Две строки можно сложить так:
> S1="123".
"123"
> S2="456".
"456"
> S3 = S1 ++ S2.
"123456"
Но это медленно. Быстрее так:
> S3 = string:concat("123","456").
У строк отсутствуют встроенные функции. Любителям императивных языков тут будет неуютно.
Оператор for
В эрланге нет стандартного цикла for, нам прийдется изобретать его :-)
Напишем свою функцию for, который используем для создания списка чисел от 1 до 10.
Создадим файл my_func.erl следующего содержания:
-module(my_func).
-export([for/3]).
for(Max, Max, F) -> [F(Max)];
for(I, Max, F) -> [F(I)|for(I+1, Max, F)].
Здесь используется все тот же эрланговский алгоритм разбиения списка на голову и хвост.
В первой итерации это будет [F(1)|for(2,10,F)], во второй - [F(2)|for(3,10,F)], и т.д.
Запускаем интерпретатор:
> c(my_func).
> my_func:for(1,10,fun(I) -> I end).
[1,2,3,4,5,6,7,8,9,10]
Теперь напишем функцию sum, вычисляющую сумму элементов списка:
Добавляем в файл my_func.erl код:
-export([sum/1]).
sum([H|T]) -> H + sum(T);
sum([]) -> 0.
Запускаем интерпретатор:
> c(my_func).
> L = [1,2,3,4,5].
> my_func:sum(L).
15
Здесь мы используем все тот же алгоритм отсечения головы от хвоста.
Теперь напишем функцию map, которая удваивает все элементы списка.
Добавляем в файл my_func.erl код:
-export([map/2]).
map(_, []) -> [];
map(F, [H|T]) -> [F(H)|map(F, T)].
Запускаем интерпретатор:
> c(my_func).
> L = [1,2,3,4,5].
[1,2,3,4,5]
> my_func:map(fun(X) -> 2*X end, L).
[2,4,6,8,10]
Обработчики списков
Обработчики списков - это выражения, которые создают списки без использования
анонимных функций, отображений (maps) или фильтров. Это делает нашу программу
еще проще и доступнее для понимания.
Так, удвоить каждый элемент списка можно вот таким обработчиком:
> [2*X || X <- L ].
То, что стоит слева от двойной вертикальной черты - это паттерн, а справа - конструктор.
Реализуем сортировку списка: добавим в файл my_func.erl код:
-export([qsort/1]).
qsort([]) -> [];
qsort([Pivot|T]) ->
qsort([X || X <- T, X < Pivot])
++ [Pivot] ++
qsort([X || X <- T, X >= Pivot]).
Здесь ++ - это инфиксный оператор добавления. А Pivot переводится как "центр вращения".
Запускаем интерпретатор:
> c(my_func).
> L=[23,6,2,9,27,400,78,45,61,82,14].
> my_func:qsort(L).
[2,6,9,14,23,27,45,61,78,82,400]
Как работает эта сортировка ?
Сначала срабатывает вторая клауза функции qsort:
[Pivot|T] = L.
которая связывает переменные Pivot -> 23 и T -> [6,2,9,27,400,78,45,61,82,14].
Теперь мы разделяем список Т на два списка : один из элементов, которые меньше
чем Pivot, а второй - из элементов, которые больше или равны Pivot.
4> Smaller = [X || X <- T, X < Pivot].
[6,2,9,14]
5> Bigger = [X || X <- T, X >= Pivot].
[27,400,78,45,61,82]
Теперь мы можем отсортировать списки Smaller и Bigger и соединить их обратно с Pivot:
qsort( [6,2,9,14] ) ++ [23] ++ qsort( [27,400,78,45,61,82] )
= [2,6,9,14] ++ [23] ++ [27,45,61,78,82,400]
= [2,6,9,14,23,27,45,61,78,82,400]
Записи (record)
Записи обьявляются с помощью следующего синтаксиса:
-record(Name, {
key1 = Default1,
key2 = Default2,
...
key3,
...
}).
В командном интерпретаторе вместо record нужно использовать rr.
В вышеуказанном примере, Name - это имя всей этой записи. key1, key2 и так далее -
это имена полей этой записи. Все эти имена должны быть атомами Эрланга. При этом,
key1 и key2 имеют значения по-умолчанию (Default1 и Default2, соответственно),
которые присваиваются этим полям при создании новой записи Name, если для них не
указано другого значения. Поле key3 является изначально неопределенным полем
записи.
Определения записей могут быть либо сразу включены в файлы с исходным кодом Эрланга, либо помещены
в файлы с расширением .hrl и потом включены в файлы с исходным кодом (что
является единственным способом, чтобы в разных Эрланг-файлах было одно и тоже
определение этих записей).
Функции и модули
В основу эрланга положена модульная организация кода.
Модули в свою очередь состоят из функций.
Модули Эрланга сохраняются в файлах с расширением .erl.
Модули должны быть откомпилированы перед тем как их содержимое будет готово к
выполнению. Скомпилированный модуль будет иметь расширение .beam.
Имя модуля должно совпадать с именем файла.
В качестве примера создадим пару структур данных,
представляющих собой прямоугольник и круг. Затем извлечем из этих структур
значения длин сторон для прямоугольника и радиуса для круга. Например, вот так:
> Rectangle = {rectangle, 10, 5}.
> Circle = {circle, 4}.
> {rectangle, Width, Ht} = Rectangle.
> {circle, R} = Circle.
Теперь напишем функцию, вычисляющую площади прямоугольника и круга.
Сначала напишем текст модуля geometry, который сохраним в отдельном файле geometry.erl:
-module(geometry).
-export([area/1]).
area({rectangle, Width, Ht}) -> Width * Ht;
area({circle, R}) -> 3.14159 * R * R.
В модуле мы записали 2 варианта функции area - они называются клаузами.
У каждой клаузы есть заголовок, шаблон аргументов, тело, состоящее из выражений.
Шаблоны аргументов у клауз должны быть взаимоисключающие.
Теперь запускаем в интерпретаторе компиляцию модуля:
> c(geometry).
У вас должен появиться бинарник geometry.beam, Теперь вызываем функцию area:
> geometry:area({rectangle, 10, 5}).
50
> geometry:area({circle, 1.4}).
6.15752
В следующем примере мы напишем два модуля. При этом один модуль будет использовать функцию другого модуля.
У нас имеется список покупок, состоящий из имен и их количества:
1> Buy = [{oranges,4}, {newspaper,1}, {apples,10}, {pears,6}, {milk,3}].
Первый модуль - shop.erl - будет возвращать стоимость одного товара в зависимости от его вида:
-module(shop).
-export([cost/1]).
cost(oranges) -> 5;
cost(newspaper) -> 8;
cost(apples) -> 2;
cost(pears) -> 9;
cost(milk) -> 7.
Теперь напишем второй модуль shop2.erl, в котором хотим посчитать общую стоимость покупок:
-module(shop2).
-export([total/1]).
total([{What, N}|T]) -> shop:cost(What) * N + total(T);
total([]) -> 0.
Теперь в командной строке компилируем модули и проверяем:
> c(shop).
> c(shop2).
> shop2:total([{milk,3}]).
21
> shop1:total(Buy).
123
Синтаксис эрланга имеет три вида пунктуации:
1 Запятые (,) разделяют аргументы в вызовах функции, конструкторах данных и шаблонах.
2 Точки (.) (с последующим пробелом) разделяют функции и выражения в оболочке Эрланга.
3 Точка с запятой (;) разделяет клаузы, которые мы используем в различных
контекстах: в объявлении функций, а так же в блоках case, if, try..catch и в
выражениях receive.
Арность(arity) функции - это количество аргументов, принимаемых этой функцией. В
Эрланге, две функции,объявленные в одном модуле, с одним именем, но разным
количеством аргументов, представляют собой две различные функции - классический пример перегрузки.
В эрланге анонимные функции - fun - это функции без имени.
Создадим переменную Z, присвоив ее анонимной функции:
> Z = fun(X) -> 2*X end.
Применим ее:
> Z(2).
4
Эрланг - это функциональный язык программирования. Кроме всего прочего это
означает, что анонимные функции могут быть переданы как аргументы для функции, а
также, что функции (или анонимные функции) могут возвращать анонимные функции в
качестве результата.
Функция, которая возвращает другие функции, или же может принимать другие
функции в качестве своих аргументов, называется функцией высшего порядка.
Модуль lists, входящий в стандартные библиотеки Эрланга, экспортирует несколько
функций, которые принимают другие функции в качестве аргументов. Наиболее
полезная из них это функция lists:map(F, L). Она возвращает список, созданный
применением функции F к каждому элементу из списка L. В питоне есть похожая одноименная функция map.
Создадим список и применим к ней функцию Z, созданную нами выше:
> L = [1,2,3,4].
> lists:map(Z, L).
[2,4,6,8]
Другая полезная функция - lists:filter(P, L), она возвращает новый список из таких
элементов E списка L, для которых функция P(E) равна true.
Давайте создадим анонимную функцию Even(X), которая возвращает true, если Х - это четное
число:
> Even = fun(X) -> (X rem 2) =:= 0 end.
Применим ее:
18> lists:filter(Even, [1,2,3,4,5,6,8]).
[2,4,6,8]
Функции могут использоваться не только в качестве аргументов других функций (таких
как map и filter). Функции могут также возвращаться другими функциями -
это обобщение (generalization).
Приведем пример - допустим, что у нас есть список чего-либо, предположим фруктов:
> Fruit = [apple,pear,orange].
Теперь объявим функцию MakeTest(L), которая преобразует любой список (L) в
функцию, которая проверяет, находится ли ее аргумент в этом списке L:
> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
> IsFruit = MakeTest(Fruit).
lists:member(X, L) возвращает true если X находится в списке L, в противном случае она
возвращает false. Давайте протестируем нашу функцию:
> IsFruit(pear).
true
Также мы можем использовать ее как аргумент функции lists:filter:
> lists:filter(IsFruit, [dog,orange,cat,apple,bear]).
[orange,apple]
|