Erlang: začínáme programovat

Alois Vitásek 28. 7. 2014

Programovací jazyk Erlang je určen k vytváření distribuovaných systémů pro zpracování velkého množství paralelních úloh (např. backend aplikace WhatsApp). V prvním díle jsme si stručně představili samotný jazyk a jeho hlavní vlastnosti. Dnes se společně pustíme konečně do opravdového programování.

Instalace a spuštění

Erlang lze získat ze stánky www.erlang.org. V obvyklých distribucích se jeho balíčky se nacházejí (bývá rozkouskovaný na více balíčků podle knihoven). Existuje verze pro Windows, pro světy Apple či *BSD. Kompilace ze zdrojových kódů mi šla bez problémů i na hodně dlouho neaktualizovaných počítačích.

Po spouštění příkazem erl se objeví Erlang shell.

$ erl
Erlang R16B (erts-5.10.1) [smp:2:2] [async-threads:10]

Eshell V5.10.1  (abort with ^G)
1>

Ve Windows se po kliknutí na příslušnou ikonu spustí Erlang shell ve slušivém grafickém okně.

Ze shellu se zadávají příkazy runtime prostředí. Něco podobného, jako je např. práce s databází pomocí textového klienta. S tím rozdílem, že v tomto případě je runtime prostředí a shell jeden a ten samý program. Erlang shellem se lze připojit k již běžícímu prostředí (a to i vzdáleně), ale o tom někdy příště.

Erlang lze nejjednodušeji ukončit zavoláním funkce halt() (zkratka v Erlang shellu q()).

$ erl
Erlang R16B (erts-5.10.1) [smp:2:2] [async-threads:10]

Eshell V5.10.1  (abort with ^G)
1> halt().

$

Všimněte si kulatých závorek (jedná se o volání funkce) a tečky na konci, kterou se ukončuje příkaz.

V shellu lze zadávat výrazy:

1> 40 + 2.
42

Volat funkce:

2> io:format("Hello world.~n").
Hello world.
ok

Nebo zadávat příkazy shellu:

3> help().
** shell internal commands **
b()        -- display all variable bindings
e(N)       -- repeat the expression in query
f()        -- forget all variable bindings
f(X)       -- forget the binding of variable X
h()        -- history
...

Erlang shell je základní prostředí, ve kterém programátor pracuje. Typická práce při vývoji může vypadat tak, že ve svém oblíbeném textovém editoru vytváříte zdrojové soubory. Ty se příkazem v shellu kompilují. Pokud se kompilace povede, je možno nově vytvořené funkce ihned volat a zkoušet, zda dělají, co by měly. Připomíná to např. vývoj uložených procedur na databázovém serveru.

K vývoji v shellu existují alternativy. Např. plugin do Eclipse erlIde.

Stav runtime prostředí, obsahy proměnných, běžící procesy a pod. se kompilací nemění. Je možné postupným voláním funkcí již hotové části aplikace vytvořit nějaký stav (např. otevřít TCP/IP spojení s protistranou, načíst obsahy souborů z disku, vytvořit v paměti databázi apod.) a v tomto stavu zkoušet, zda vyvíjený kód dělá, co má. Pokud se v něm udělají změny, po rekompilaci se načte nová verze kódu, ale testovací situace zůstává, jak byla. Což je velmi příjemné a snadno se na to zvyká. Erlang podporuje filozofii tzv. inkrementálního vývoje, kdy se kód vytváří v malých dávkách a během vývoje se rovnou zkouší (ladí), a to bez nutnosti si na to vytvářet nějaký speciální kód. Malé odladěné kousky se pak skládají do větších celků.

První modul

Dost řečí, je čas programovat.

Základním stavebním kamenem programů v Erlangu je tzv. modul. Modul obsahuje funkce, které spolu obvykle nějak logicky souvisí. Funkce je určena modulem, jménem a počtem parametrů. Ve výše uvedeném příkladu volání funkce

io:format("Hello world.~n")

se jedná o volání funkce format z modulu io s jedním parametrem.

Modul vzniká ze zdrojového souboru stejného jména s koncovkou .erl. Vytvořme si nějaký adresář na pokusy a v něm soubor test_one.erl.

% priklad 1
% file: test_one.erl

-module(test_one).

-export([hello/0]).

hello() ->
  print_msg().

print_msg() ->
  io:format("Hello world.~n").

Jak je vidět, komentáře se zapisují znakem % , co je za procentem, se ignoruje. Blokové komentáře (obdoba /* … */) zde nejsou. Typ konce řádů (Unix/DOS) nehraje roli. Povinné odsazování nebo syntaktické mezery tu nejsou (až na jednu úplně malou výjimku). Psát do programu jiné znaky než z první poloviny ASCII tabulky mne zatím nenapadlo (určitě to nějak jde).

Řádky začínající mínuskem (-module, -export) se nazývají atributy modulu. Je jich celá řada, některé jsou povinné, jiné ne. Mají význam v různých fázích kompilace. Atribut -module obsahuje název modulu a musí být shodný s názvem souboru (bez koncovky .erl). Taktéž musí být jako první řádek kódu (prázdné řádky či komentáře se nepočítají).

Atribut -export říká, které funkce z modulu je možno volat vně modulu. Obsahem je seznam funkcí (hranaté závorky, čárkou oddělené hodnoty). Funkce je specifikována jménem a počtem parametrů (číslice za lomítkem). V tomto případě máme dvě funkce, hello/0print_msg/0. Obě nemají parametry a funkce hello/0 je exportována.

Atributů -export může být více, ale všechny musejí být uvedeny dříve, než je zapsána první funkce.

Nyní si spustíme Erlang tak, aby jeho aktuálním adresářem byl tam, kde leží zdrojový soubor test_one.erl. Pokud tam nejsme, lze tam dodatečně doskákat příkazem shellu cd(). Dle očekávání fungují pwd()ls()

6> cd ("/home/lojza/erlang-text/priklady").
/home/lojza/erlang-text/priklady
ok
7> pwd().
/home/lojza/erlang-text/priklady/
ok
8> ls().
test_one.erl
ok
9>

Ve Windows lze používat Windows zápisy cest (c:\erlang). Lomítka, lze používat oboje, ale při použití zpětných je nutné lomítka zdvojovat (známá past zápisu cesty se zpětnými lomítky do stringu). Tedy cd(„c:\\erlang“) nebo cd(„c:/erlang“).

Modul se zkompiluje příkazem shellu c().

9> c(test_one).
{ok,test_one}
10>

Jméno modulu se píše bez uvozovek, malými písmeny, bez koncovky .erl.

Pokud se kompilace povedla, vznikne soubor .beam, což je binární forma zkompilovaného modulu.

10> ls().
test_one.beam      test_one.erl
ok
11>

Nyní již nic nebrání použít funkci hello.

11> test_one:hello().
Hello world.
ok
12>

Funkce vypsala očekávaný text a vrátila návratovou hodnotu funkce hello/0, což je návratová hodnota interní funkce print_msg/0, což je návratová hodnota funkce io:format/1, což je ok.

Kompilaci lze provést i mimo Erlang shell programem erlc.

$ erlc test_one.erl

Načtení nové verze kódu modulu v Erlang shellu se provádí příkazem l.

14> l(test_one).
{module,test_one}
15>

Datové typy a struktury

Než začneme zapisovat složitější funkce, je třeba si něco říct o datových typech, které jsou k dispozici a vůbec o tom, jak se s datovými typy v Erlangu zachází.

Čísla

Celá čísla se zapisují v desítkové soustavě. Interně se jedná o integery (velikost záleží na platformě), pokud však do této velikosti nevejdou, jsou interně zkonvertovány na nekonečné integery (tzv. bignums) a mohou být tedy libovolně veliké. Až do velikosti paměti, co je ochoten operační systém alokovat. Pro zápis integeru v jiné než desítkové soustavě se používá zápis base#number.

  0 42 2#10011 16#1F

Číslo lze zapsat znakem, který odpovídá jeho pozici v ASCII tabulce. S prefixem $

  $a  % 97
  $\n % 10

Destiná čísla se zapisují s desetinou tečkou a případným exponentem. Interně jsou uložena v 8bajtovém floatu.

  1.5  1.0 4.34E-5

Matematické operace fungují dle očekávání (+, -, *, /, rem, div, závorky …)

Atomy

Na rozdíl od čísel jsou tzv. atomy specialita Erlangu, co se mimo funkcionální programování moc nevyskytuje. Jsou to konstantní hodnoty dané svým obsahem, určené k porovnávání mezi sebou. V jazyce C se k podobným účelům pomocí #define vytvářejí konstanty zastupující nějakou číselnou hodnotu. V programu se používá makro, jehož název je volen tak, aby něco říkal (např. RETURN_OK místo 0). Atomy jsou něco podobného s tím, že programátor je nemusí nikde definovat. Prostě je použije. Interně atom znamená nějaký integer a někde existuje převodová tabulka mezi názvem atomu a jeho hodnotou, ale ta není vidět. Při programování nebo běhu systému (debugování, stack trace po výjimce a pod) se na místě atomů vyskytuje jejich název.

Atom se zapisuje jako text začínající malým písmenkem a obsahující jen znaky, čísla, @, čárku a tečku. Pokud výše uvedené pravidlo nestačí, je třeba text uzavřít do jednoduchých uvozovek.

  ok error cause22 foo@bar 'Atom with strange name?$#'

K čemu je to dobré. Atomy lze porovnávat. Jsou využívány jako návratová hodnota funkce, klíč v úložišti s hodnotami konfigurace a pod. Není nutné předávat různé informace a stavy číselným kódem jehož význam je pak třeba hledat někde v dokumentaci nebo v nějakém hlavičkovém souboru.

Atomy vznikají většinou tak, že jsou napsány (použity) někde v programu. Mohou ale vznikat konverzí z textového řetězce. Na to pozor. Paměť určená pro atomy je omezená a jednou vytvořený atom se nedá smazat. Pokud paměť dojde, zastaví se runtime prostředí. Takže není dobrý nápad vytvářet atom např. z každého id transakce nebo něco podobného. Na to nejsou určené.

Logické hodnoty

Datový typ boolean v Erlangu není. Místo něj se používají atomy true false. Porovnávací výrazy vracejí jeden z těchto atomů.

1> 1 == 3.
false

Logické operátory and, or, not, xor fungují dle očekáváni. Přičemž andor vždy vyhodnocují oba operandy. Na tzv. zkrácené vyhodnocení (např. pokud je první operátor false a spojka and, tak netřeba vyhodnocovat druhý operátor) existují operátory andalsoorelse. Někdy je (kvůli vedlejším efektům) žádoucí plné vyhodnocení, jindy zkrácené.

Porovnávací operátory (== < > =< >=) fungují opět dle očekávání. Operátor není rovno je vypadá méně obvykle (/=). Navíc existuje operátor přesná shoda (=/=).

1 == 3    % false
1 /= 3    % true
1 == 1.0  % true
1 =/= 1.0 % false

Tuples (n-tice)

Tuples (tzv. n-tice) jsou složené hodnoty. Tj. ke spojení více hodnot jedné. A to pomocí složených závorek. Hodnoty mohou být libovolného typu a n-tice lze do sebe libovolně zanořovat.

{error, 43} {1,8,12} {a, b, c, {11, d, {12, e}, f}}

Vyžívají se zejména k zápisu několika hodnot, co k sobě patří, či k přenosu více hodnot v místě, kam lze zapsat jen jednu (např. parametr funkce, návratová hodnota funkce).

List (seznam)

List slouží k zápisu/přenosu/zpracování seznamu hodnot. List není pole. Pole ve smyslu blok paměti, kde se pointerovou aritmetikou skáče na jednotlivé prvky a ty lze číst a měnit. Jako všechny ostatní hodnoty v Erlangu list lze vytvořit, číst, ale ne měnit.

Nový list se vytvoří pomocí hranatých závorek.

[a, b, c, d]  % list se 4 atomy

Ke stávajícímu listu lze přidávat prvky zleva. To je laciná operace, kdy vznikne nový list tak, že se použije původní a k němu se přidá nový prvek pomocí operátoru |

1> List1 = [a,b,c,d].
[a,b,c,d]
2> List2 = [1 | List1].
[1,a,b,c,d]

Výraz [Prvek | List] znamená List prodloužený o jeden prvek zleva. Následující výrazy mají stejný význam.

[a, b, c, d]
[a | [b, c, d]]
[a | [b | [c, d]]]
[a | [b | [c | [d]]]]
[a | [b | [c | [d | []]]]]

Zkuste si to v Erlang shellu (porovnání listu znamená porovnání prvků v něm).

3> [a,b,c,d] == [a | [b | [c | [d | []]]]].
true

Operátorem | lze list také rozložit na první prvek a zbytek.

[Head | Tail] = [a, b, c, d] % v proměnné Head je a v Tail je [b, c, d]

V následujícím příkladu se použijí příkazy shellu f() pro vymazání všech případných proměnných z předchozích pokusů a b() pro výpis hodnot proměnných.

20> f().
ok
21> List = [a, b, c, d].
[a,b,c,d]
22> [Head1 | Tail1] = List.
[a,b,c,d]
23> [Head2 | Tail2] = Tail1.
[b,c,d]
24> [Head3 | Tail3] = Tail2.
[c,d]
25> [Head4 | Tail4] = Tail3.
[d]
26> b().
Head1 = a
Head2 = b
Head3 = c
Head4 = d
List = [a,b,c,d]
Tail1 = [b,c,d]
Tail2 = [c,d]
Tail3 = [d]
Tail4 = []
ok

Zde je vidět postupné odebírání prvků v seznamu zleva, dokud v něm něco je. List je v paměti jen jeden a proměnné Tail1 až Tail4 ukazují na jeho ruzné části. Tento princip je základem rekurzivního procházení listů. Typická funkce na práci s prvky listu vypadá nějak takto.

list_action ([]) -> ok;
list_action ([Head | Tail]) ->
  do_something_with_head (Head),
  list_action (Tail).

Rekurzivní charakter sestavování a čtení listů je místo, kde se může řada lidí učící se Erlang zaseknout. Proto je dobré mu přijít na kloub hned na začátku. Až přijde řada na funkce, ještě se k tomu vrátíme.

Listy lze spojovat operátorem ++.

[a,b] ++ [c,d]

List na levé straně se postupně prvek po prvku připojí k listu na pravé straně. To je neefektivní operace. Pokud by se to mělo dělat často s nějakými dlouhými listy na levé straně, je lepší se tomu vyhnout. Ale na druhou stranu je tento zápis hezky čitelný a krátký. Vhodnost použití záleží na okolnostech.

Pro zefektivnění práce s listy (zejména jejich spojování) v situaci, kdy je list určen k tomu, aby se někam zapsal (data pro soubor, výpis do logu, socket a pod.), existují tzv. deeplisty. Deeplist je list obsahující kromě prvků další listy. U výstupních funkcí je jedno, zda dostanou list nebo deeplist. Deeplist lze převést na list funkcí list:flattern/1, což je ale drahá funkce (kopíruje jednotlivé prvky).

1> List1 = [1,2,3].
[1,2,3]
2> List2 = [a, b, c].
[a,b,c]
3> List3 = [List1, 10, 20, List2, 30].
[[1,2,3],10,20,[a,b,c],30]
6> lists:flatten(List3).
[1,2,3,10,20,a,b,c,30]

Listy a n-tice vypadají na první pohled velmi podobně. Posloupnost prvků s daným pořadím. Zde není rozdíl. Rozdíl je v tom jakými způsoby se s nimi dá pracovat.

Listy a n-tice se často do sebe různě zanořují a tím se popisují struktury dat (konfigurace, výstup z XML parseru a pod.)

[
  {foo, 1},
  {bar, 4},
  {quix, [a, b, c, d]}
]

Textové řetězce

Na textové řetězce neexistuje v Erlangu zvláštní datový typ. Zapisují se jako list integerů. Následující výrazy mají stejný význam.

widgety

"ABCD"
[65,66,67,68]
[$A,$B,$C,$D]

To není zrovna efektivní způsob, neboť každý znak zabírá 4 bajty (na 64bitové architektuře 8 bajtů). Na druhou stranu to řeší problém s UTF (není omezení pro velikost integeru reprezentující jeden znak). Je to tak proto, že autoři Erlangu práci s řetězci moc nepotřebovali (v telekomunikačních aplikacích to není příliš důležité). Pro různé výpisy do logů apod. se s tím dá žít. Pokud je třeba zpracovávat velké množství textových dat (práce s nějakým textovým síťovým protokolem – např. SOAP – HTTP + velmi ukecané XML), tak se na to používá datový typ binary (o tom někdy příště).

Z datových typů je to zatím vše. Zamlčel jsem zmíněný typ binary, typ reference a porty. Příště se podíváme na proměnné, porovnávání vzorů, funkce a rozhodovací struktury. Čímž by mělo být probráno vše, co se týče sekvenčního programování.

Našli jste v článku chybu?
Podnikatel.cz: EET pro e-shopy? Postavené na hlavu

EET pro e-shopy? Postavené na hlavu

Vitalia.cz: Dostal malý pivovar ze Slovenska do Tesca

Dostal malý pivovar ze Slovenska do Tesca

120na80.cz: Co je padesátkrát sladší než cukr?

Co je padesátkrát sladší než cukr?

Podnikatel.cz: Udělali jsme velkou chybu, napsal Čupr

Udělali jsme velkou chybu, napsal Čupr

Lupa.cz: Další Češi si nechali vložit do těla čip

Další Češi si nechali vložit do těla čip

DigiZone.cz: Funbox 4K v DVB-T2 má ostrý provoz

Funbox 4K v DVB-T2 má ostrý provoz

Vitalia.cz: Muž, který miluje příliš. Ženám neimponuje

Muž, který miluje příliš. Ženám neimponuje

DigiZone.cz: Mordparta: trochu podchlazený 87. revír

Mordparta: trochu podchlazený 87. revír

Vitalia.cz: 5 pravidel proti infekci močových cest

5 pravidel proti infekci močových cest

DigiZone.cz: Wimbledon na Nova Sport až do 2019

Wimbledon na Nova Sport až do 2019

Lupa.cz: Hackeři mají data z půlmiliardy účtů Yahoo

Hackeři mají data z půlmiliardy účtů Yahoo

Podnikatel.cz: Babišovi se nedá věřit, stěžovali si hospodští

Babišovi se nedá věřit, stěžovali si hospodští

DigiZone.cz: O2 Sport zbrojí na derby pražských S

O2 Sport zbrojí na derby pražských S

Lupa.cz: Jak se prodává firma za miliardu?

Jak se prodává firma za miliardu?

DigiZone.cz: Test: brýle pro virtuální realitu Exos Urban

Test: brýle pro virtuální realitu Exos Urban

DigiZone.cz: DVB-T2 ověřeno: seznam TV zveřejněn

DVB-T2 ověřeno: seznam TV zveřejněn

Podnikatel.cz: Byla finanční manažerka, teď cvičí jógu

Byla finanční manažerka, teď cvičí jógu

DigiZone.cz: Numan Two: rozhlasový přijímač s CD

Numan Two: rozhlasový přijímač s CD

DigiZone.cz: Parlamentní listy: kde končí PR...

Parlamentní listy: kde končí PR...

Podnikatel.cz: Instalatér, malíř a elektrikář. "Vymřou"?

Instalatér, malíř a elektrikář. "Vymřou"?