Hlavní navigace

Erlang: struktury a ETS tabulky

11. 12. 2014
Doba čtení: 16 minut

Sdílet

Programovací jazyk Erlang je určený k vytváření distribuovaných systémů pro zpracování velkého množství paralelních úloh. Dnešní díl bude o strukturách, a když už je budeme mít, tak je začneme ukládat do tabulek a ukážeme si, jak napsat procesy, které takové tabulky obsluhují.

Struktury

Jednou z nejzákladnějších úloh, která se vyskytuje při programování, je spojování více hodnot do jednoho kousku. Ten pak lze jako jeden celek někam předat a posléze si z něj jednotlivé komponenty vytahovat. Z doposud poznaných datových typů by se na to hodily n-tice. A pro tyto účely se n-tice opravdu často používají. V jednoduchých případech to stačí. Problémy se začnou objevovat, pokud je třeba pojmout větší množství hodnot. Ztrácí se přehlednost, riziko chyby se zvětšuje a pokud se objeví potřeba přidat dodatečně nějaký nový atribut, je s tím práce na mnoha místech.

Struktury jsou jako v mnoha jiných jazycích složenou hodnotou, jejichž jednotlivé komponenty jsou pojmenované a pod tímto jménem se k nim přistupuje. Definice struktury se vytváří atributem -record

-record(NazevStruktury, {Pole1, Pole2, ... PoleN}).

Na názvy struktury a polí se vztahují stejná omezení jako na atomy. Tj. buď se musí jednat o alfanumerické řetězce začínající na malé písmeno, nebo to může být prakticky cokoliv uzavřené do jednoduchých uvozovek.

Struktury mohou být lokální, kdy se používají jen uvnitř jednoho modulu. Lokální struktura se například hodí pro stavová data procesu. Takové struktury se definují jako atribut ve zdrojovém souboru modulu.

Jiná situace nastává, pokud je třeba strukturu používat ve více modulech. Např. v parametrech nebo výstupních hodnotách exportovaných funkcí. Pak se definice struktur píší do společných include souborů. Příklad:

%%
%% priklad include souboru s definici struktury
%% soubor: osoba.hrl
%%

-record (osoba, {
  id,
  jmeno,
  prijmeni,
  vek
}).

V Erlang shellu se možné se strukturami pracovat. Před tím je třeba nějakým způsoben načíst definice struktur, o které jde. To se dá udělat několika způsoby. Jedním z nich je příkazem rr načíst struktury z nějakého zdrojového souboru.

1> rr("osoba.hrl").
[osoba]

Struktura se zapisuje pomocí svého názvu uvozeného # , za kterým následují složené závorky, ve kterých je pak nastavování atributů struktury.

2> Osoba1 = #osoba{jmeno = "Karel", prijmeni= "Barel"}.
#osoba{id = undefined,jmeno = "Karel",prijmeni = "Barel",
       vek = undefined}

Vznikla struktura s nastavenými atributy jmeno a prijmeni. Nenastavené atributy obsahují atom undefined.

K hodnotám atributů se lze dostat přes tečku.

3> Osoba1#osoba.jmeno.
"Karel"
4> Osoba1#osoba.prijmeni.
"Barel"

Na strukturu lze aplikovat porovnávání vzorů, kterým lze např. vytáhnout více hodnot ze struktury do proměnných v jednom příkazu.

6> #osoba{jmeno = Jmeno, prijmeni = Prijmeni} = Osoba1.
#osoba{id = undefined,jmeno = "Karel",prijmeni = "Barel",
       vek = undefined}
7> Jmeno.
"Karel"
8> Prijmeni.
"Barel"

Jako všechna ostatní data v Erlangu nelze strukturu dodatečně měnit. Lze však vytvořit novou ze stávající s tím, že se vybrané atributy změní. Což se zapisuje tak, že se napíše struktura, nastaví se v ní některé atributy a před to celé se uvede již existující proměnná. Výsledkem je struktura obsahující data z proměnné, v níž se změnily vybrané atributy. Příklad:

9> Osoba2 = Osoba1#osoba{id=1234, vek=34}.
#osoba{id = 1234,jmeno = "Karel",prijmeni = "Barel",
       vek = 34}

Se strukturami v Erlangu souvisí jedno malé „tajemství“. Struktury nejsou z hlediska runtime prostředí další datový typ, ale vnitřně jsou reprezentovány jako n-tice. Jsou něco jako makro, jež se při kompilaci interně rozvine na práci n-ticemi. Pokud vypátráte, jak dané n-tice vypadají a někde napíšete kód, který s nimi bude pracovat přímo, tak to bude fungovat, ale vězte, že děláte něc, co se nemá dělat. Tak to prosím nikdy nedělejte. Kdyby nic jiného, v nějaké budoucí verzi Erlangu se může implementace struktur změnit a váš kód přestane fungovat. S tím, jak tyto n-tice vypadají, se dá setkat např. při debugování nebo při práci v Erlang shellu, pokud nejsou načteny definice struktur. Příklad:

10> io:format("osoba: ~p~n", [Osoba2]).
osoba: {osoba,1234,"Karel","Barel",34}
ok

Jedná o tzv. tagovanou n-tici (tagged tuple). V tagované n-tici je prvním prvkem je atom určující typ (obsah). V případě struktury se jedná o její název, následují pak jednotlivé prvky v pořadí podle definice struktury.

Při práci se strukturami v Erlang shellu je dobré do něj dostat příslušné definice. To lze udělat čtením zdrojových souborů, příkazem rr , jak bylo ukázáno výše. Ne vždy jsou k dispozici zdrojové soubory modulů, se kterými se v shellu pracuje. Pak se hodí, že definice struktur daného modulu jsou obsaženy i ve zkompilovaných binárkách. Příkazu rr lze zadat modul (atom se jménem modulu), a tak lze načíst definice v něm obsažené. Příklad. Systémový modul pro práci se soubory file umí mimo jiné zjistit metadata o souboru (různé časové údaje, velikost, uid, gid …), což vrací ve struktuře #file_info. V Erlang shellu s tím lze pracovat např. takto:

1> {ok, F} = file:read_file_info("osoba.hrl").
{ok,{file_info,141,regular,read_write,
               {{2014,9,8},{23,14,53}},
               {{2014,9,6},{21,49,44}},
               {{2014,9,5},{22,24,50}},
               33206,1,3,0,0,0,0}}
2> F.
{file_info,141,regular,read_write,
           {{2014,9,8},{23,14,53}},
           {{2014,9,6},{21,49,44}},
           {{2014,9,5},{22,24,50}},
           33206,1,3,0,0,0,0}
3> rr(file).
[file_descriptor,file_info]
4> F.
#file_info{size = 141,type = regular,access = read_write,
           atime = {{2014,9,8},{23,14,53}},
           mtime = {{2014,9,6},{21,49,44}},
           ctime = {{2014,9,5},{22,24,50}},
           mode = 33206,links = 1,major_device = 3,minor_device = 0,
           inode = 0,uid = 0,gid = 0}
5> F#file_info.size.
141

Pokud existuje více definic struktury se stejným názvem, platí ta, co se načetla jako poslední. Malá odbočka ke stejnojmenným identifikátorům. V Erlangu nejsou jmenné prostory ani nic podobného. Tuto slabinu je nutné řešit používáním identifikátorů (názvů modulů a struktur) tak aby nevznikaly konflikty. U struktur stačí, aby se nepotkaly struktury stejného jména v jednom modulu. U názvů modulů je třeba mít jednoznačnost zajištěnou globálně. Což se často řeší různými „kryptickými“ předponami pro moduly, které spolu nějak souvisí.

ETS tabulky

Telekomunikační aplikace pro které byl Erlang navržen, potřebují rychlá úložiště pro větší množství dat. Tyto databáze nebývají složité a na dnešní poměry nejsou ani moc rozsáhlé (vejdou se do paměti). V Erlangu na to existují tzv. ETS tabulky (Erlang term storage) pro ukládání dat do paměti a DETS (disk Erlang term storage) tabulky pro ukládání dat do souborů. Jedná se o úložiště typu klíč hodnota. Něco podobného jako asociativní pole v různých jazycích. S různými nuancemi, které lze ovlivňovat konfiguračními parametry. Mohou být v následujících variantách:

  • set – klíč je jedinečný. Pokud se pod existujícím klíčem uloží jiná hodnota, původní se přepíše
  • ordered set – klíč je opět jedinečný, při procházení ETS tabulky jsou klíče seřazeny dle velikosti (porovnávání operátorem <)
  • bag – klíč nemusí být jedinečný, lze pod ním uložit více hodnot, ale dvojice klíč/hodnota se vyskytuje jen jednou.
  • duplicated bag – klíč nemusí být jedinečný, lze pod ním uložit více hodnot a ty mohou být stejné.

Ukládanými hodnotami jsou n-tice a klíčem je hodnota na předem dané pozici v ukládaných datech (defaultně první, dá se to nastavit při vytváření tabulky).

Funkce pro práci s ETS tabulkami jsou v modulu ets (a DETS v modulu dets). Základní operace (vytvoření, ukládání dat, čtení a uvolnění tabulky) může vypadat např. takto:

1> ets:new(my_table, [named_table, set, {keypos, 1}]).
my_table

2> ets:info(my_table).
[{compressed,false},
 {memory,314},
 {owner,<0.32.0>},
 {heir,none},
 {name,my_table},
 {size,0},
 {node,nonode@nohost},
 {named_table,true},
 {type,set},
 {keypos,1},
 {protection,protected}]
3> ets:insert(my_table, {1, foo}).
true
4> ets:insert(my_table, {2, bar}).
true
5> ets:lookup(my_table, 1).
[{1,foo}]
6> ets:lookup(my_table, 3).
[]
7> ets:delete(my_table).
true

Vlastnosti tabulky jsou dány druhým parametrem funkce ets:new/1. Což je seznam vlastností (options), kde se logické příznaky dávají najevo výskytem nějakého atomu a příznaky s hodnotu n-ticí {atom, hodnota}. Tento přístup se často používá na mnoha místech.

Jak je vidět, tabulka se vytvoří voláním funkce ets:new. V tomto případě se vytvořila tabulky typu set, která je pojmenovaná (option named_table v parametrech). Což znamenám že se funkce s ní pracující na tabulku odkazují jejím jménem (atom my_table v prvním parametru. Lze vytvořit nepojmenovanou tabulku (option named_table se neuvede), pak funkce ets:new vrátí handler na tabulku, který si je potřeba uschovat a k tabulce přistupovat přes něj. Parametr keypos říká, na které pozici v ukládaných n-ticích se nachází klíč.

S daty je manipulováno ve funkcích ets:insertets:lookup. Funkce ets:delete smaže obsah tabulku i s jejím obsahem.

Tabulka přísluší k procesu, který ji vytvořil. Je jejím vlastníkem. Pokud proces skončí (např. zhavaruje), tabulka zanikne. S vlastníkem souvisí provádění změn v tabulky. Není-li řečeno jinak, změny může provádět jen proces, který tabulku vlastní, a číst data může kterýkoliv proces. Většinou to tak vyhovuje. Čte, kdokoliv má potřebu, a změny dělá jen jeden proces, a tomu se požadavky na update zasílají zprávami. Požadavky na zápis jsou serializovány (dokud se nedodělá jeden, ostatní čekají ve frontě), což většinou stačí k tomu, aby byla data v tabulce konzistentní. Přístup procesů k datům tabulky se ovlivňuje parametrem protection. Kromě popsaného chování (mód protected), existuje možnost private (i čtení je dovoleno jen vlastníkovi) nebo public (číst i zapisovat může každý proces). V případě potřeby lze vlastnictví tabulky předat z jednoho procesu na jiný (viz. dokumentace).

Data v tabulce pochopitelně nejsou uvolňována, takže je třeba myslet na odmazávání neaktuálních dat.

Příklad – databáze přenesených čísel

Jak pracovat s ETS a DETS tabulkami si ukážeme na následujícím příkladu s databází přenesených čísel (jen mobilních).

Pokud si někdo v ČR přeje změnit operátora, má možnost přejít k jinému a vzít si sebou svoje telefonní číslo. S tímto procesem souvisí jistá organizační činnost, kdy se zákazník a operátoři musejí dohodnout, že číslo přechází a od kdy. To ponechme stranou. Výsledkem je seznam všech čísel, která jsou provozována u jiného operátora, než ke kterému patří číselný rozsah, do kterého náleží. Tato databáze se v podobě textového souboru pravidelně distribuuje (resp. jeho inkrementální změny) k jednotlivým operátorům a ti si jej nahrávají do svých systémů. Zhruba řečeno.

Úkolem je určit, zda je zadané číslo přenesené, a pokud ano, tak ke kterému operátorovi. V tuto chvíli se budeme zaobírat jen mobilními čísly, které se mohou přenášet jen po jednom. Čísla pevných linek se mohou přenášet jako rozsahy (dvě hodnoty – hranice od a do).

%%
%% struktura pro databazi prenesenych cisel
%% soubor: npdb.hrl
%%

-record (np_mob, {
  msisdn,
  oper_id
}).

Telefonnímu číslu se říká MSISDN (Mobile Subscriber ISDN Number) a v databázi bude uloženo jako celé číslo (integery jsou nekonečné). Oper Id je také integer.

Data je vhodné uložit do ETS tabulky a jejich kopie se bude zálohovat do DETS tabulky v souboru na disku. Změny se budou zapisovat jak na disk, tak do paměti a při startu se ETS tabulka v paměti naplní obsahem DETS dat z disku.

Následující příklad bude rozkouskovaný do více okomentovaných fragmentů kódu, přičemž na konci bude odkaz na stažení všech souborů, které jsou třeba.

Bývá zvykem vytvořit modul na manipulaci s daty tabulky mimo modul pro proces, který pak tabulku vlastní. Nejprve je třeba mít funkce pro otevírání a zavírání tabulek.

% vytvoreni tabulek
create_tables (FileName) ->
  % parametr keypos urcuje na kterem miste v ukladane n-tici je klic
  % konstrukce #struktura.prvek se rozexpanduje na index prvku ve strukture,
  % coz je "nahodou" zrovna to co je potreba (v tomto pripade 1)
  ets:new(npdb_mob_ram, [named_table, set, {keypos, #np_mob.msisdn}]),
  dets:open_file(npdb_mob_disk, [{file, FileName}, {keypos, #np_mob.msisdn}]),
  ok.

% uzavreni tabulek
close_tables () ->
  ets:delete (npdb_mob_ram),
  dets:close (npdb_mob_disk).

V tomto případě jsou vytvořené tabulky pojmenované. Tj. pro práci s nimi se používají jejich jména. Kdyby tomu tak nebylo, pro přístup k nim je třeba identifikátor tabulky (TabId), který vrací funkce ets:new (resp. dets:open_file).

Pro manipulaci s daty se používají funkce insertdelete. Přičemž insert buď vytvoří nový záznam (pokud se klíč v tabulce nevyskytuje), nebo jej přepíše (pokud se klíč v tabulce již vyskytuje).

% nastaveni operatora k cislu
% zapis se provadi jak do pameti tak na disk
update (Msisdn, OperId) ->
  NpRec = #np_mob{msisdn=Msisdn, oper_id=OperId},
  ets:insert(npdb_mob_ram, NpRec),
  dets:insert(npdb_mob_disk, NpRec).

% smazani zaznamu (cislo se vratilo k puvodnimu operatorovi a jiz neni prenesene)
% zapis se provadi jak do pameti tak na disk
delete (Msisdn) ->
  ets:delete(npdb_mob_ram, Msisdn),
  dets:delete(npdb_mob_disk, Msisdn).

Asi nejčastěji používanou funkcí je vyhledávání. To se dělá jen nad ETS tabulkou v pamětí.

% dohledani operatora k cislu - hleda se jen v pameti
lookup(Msisdn) ->
  case ets:lookup(npdb_mob_ram, Msisdn) of
    % vysledkem je pole o jednom prvku (tabulka je typu set, klic je zde maximalne jednou)
    [#np_mob{oper_id = OperId}] -> {ok, OperId};
    [] -> {error, instance} % nenaslo se
  end.

Povšimněte si, že struktura #np_mob (definice řádku v tabulce) se používá jen interně a není součástí vstupních ani výstupních dat.

Aby disková DETS tabulka k něčemu byla, je třeba mít funkci na její nakopírování do ETS tabulky. Na to se hodí funkce ets:from_dets.

% po startu je treba nacist data z disku do pameti
restore_backup() ->
  ets:from_dets (npdb_mob_ram, npdb_mob_disk).

Modul s uvedenými funkcemi a s funkcemi pro naplnění tabulky testovacími daty je v souboru npdb_mob_db.erl v archivu zdrojových kodů příkladů tohoto článku.

Po kompilaci a vytvoření tabulek lze vyzkoušet, zda vše pracuje, jak má

1> c(npdb_mob_db).
{ok,npdb_mob_db}
2> npdb_mob_db:create_tables("data.db").
ok
3> npdb_mob_db:update(790003366, 103).
ok
4> npdb_mob_db:lookup(790003366).
{ok,103}
5> npdb_mob_db:delete(790003366).
ok
6> npdb_mob_db:lookup(790003366).
{error,instance}

Dále je možno do tabulky naplnit testovacími daty. Např. 2 miliony záznamů.

7> npdb_mob_db:fill_random(2000000).
generating 2000000 records
cleaning npdb_mob_ram
cleaning npdb_mob_disk
filling npdb_mob_ram
filling npdb_mob_disk
ok
8> ets:info(npdb_mob_ram).
[{compressed,false},
 {memory,20191104},
 {owner,<0.32.0>},
 {heir,none},
 {name,npdb_mob_ram},
 {size,1990101},
 {node,nonode@nohost},
 {named_table,true},
 {type,set},
 {keypos,2},
 {protection,protected}]
9> dets:info(npdb_mob_disk).
[{type,set},
 {keypos,2},
 {size,1990101},
 {file_size,67334984},
 {filename,"data.db"}]

Záznamů v tabulkách nejsou rovné 2 miliony, neboť při generování náhodných čísel sem tam dochází ke kolizím. Laskavý čtenář promine.

Obsah tabulek lze procházet pomocí nástroje tv (table viewer). Spouští se příkazem tv:start() a pokud má Erlang kam zobrazit grafické okno, objeví se okno se seznamem tabulek, které pak v dalším kroku zobrazit.

1> tv:start().

Alternativně se dá v programu observer (observer:start()) použít záložka Table viewer (o něco modernější vzhled).

Jak bylo řečeno výše, tabulka vždy patří k nějakému procesu. Myšlenka obvykle bývá taková, že vlastník tabulky (nebo soustavy více tabulek) provádí změny. Požadavky na změny proces dostává synchronními zprávami. Ostatní procesy v tabulkách čtou, a to tak, že zavolají čtecí funkce, jež se vykonají přímo v nich. To ve většině případů stačí. V případě, že je třeba zajistit nějakou konzistenci dat (nějaká zapisovací transakce mění více řádků a pokud by během toho někdo jiný četl, vznikla by chyba), je třeba serializovat synchronními zprávami i čtení. Nebo pokud lze poznat, že přečtená data jsou chybná, tak je zahodit a zkusit je přečíst znovu.

Řídící proces pro naši databázi je proces typu server (gen_server) a vypadá takto:

%%
%% Ridici proces pro databazi prenesenych mobilnich cisel
%% soubor: npdb_mob.erl
%%
-module(npdb_mob).
-behaviour(gen_server).

% O & M funlce + uzivatelske API
-export([start_link/1, stop/0, update/2, delete/1, lookup/1, fill_random/1]).

% callbacky pro gen_server
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%%
%% O & M funkce
%% funkce pro vypinani a zapinani procesu
%%

start_link(FileName) ->
  gen_server:start_link({local, ?MODULE}, ?MODULE, [FileName], []).

stop() ->
  gen_server:cast (?MODULE, stop).

%%
%% uzivatelske API
%%

% vyhledavaci funkce kouka rovnou do tabulek
lookup (Msisdn) ->
  npdb_mob_db:lookup(Msisdn).

% updatovaci funkce zasilaji zpravu procesu a cekaji na odpoved
update(Msisdn, OperId) ->
  gen_server:call (?MODULE, {update, Msisdn, OperId}).

delete(Msisdn) ->
  gen_server:call (?MODULE, {delete, Msisdn}).

fill_random (N) ->
  % je treba trochu delsi timeout nez defaultnich 5 sekund
  gen_server:call (?MODULE, {fill_random, N}, 25000).

%%
%% gen_server callbacky
%%

% inicializace - vytvoreni/otevreni tabulek a nacteni dat z disku do pameti
init ([FileName]) ->
  npdb_mob_db:create_tables(FileName),
  npdb_mob_db:restore_backup(),
  State = undefined,  % stavova data nejsou potreba, mohly by zde byt nejake
                      % statistiky
  {ok, State}.

% pri uklizeni se zavrou tabulky
terminate (_Reason, _State) ->
  npdb_mob_db:close_tables().

% zastaveni procesu
handle_cast (stop, State) ->
  {stop, normal, State}.

% obsluha synchronnich volani
% shodou okolnosti se vsude jedna jen o prenos parametru do
% prislusnych funkci pro praci s tabulkou
handle_call ({update, Msisdn, OperId}, _From, State) ->
  Reply = npdb_mob_db:update (Msisdn, OperId),
  {reply, Reply, State};

handle_call ({delete, Msisdn}, _From, State) ->
  Reply = npdb_mob_db:delete (Msisdn),
  {reply, Reply, State};

handle_call ({fill_random, N}, _From, State) ->
  Reply = npdb_mob_db:fill_random (N),
  {reply, Reply, State}.

% reakce na zpravu "mimo protokol" ve fronte
handle_info (Msg, State) ->
  io:format ("unexpected message: ~w~n", [Msg]),
  {noreply, State}.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

Je vidět, že funkce na změnu dat (update, delete, fill_random) zasílají zprávu procesu a teprve v něm se provede požadovaná operace. Naopak funkce na čtení (lookup) pracuje s tabulkou přímo v procesu, který si ji zavolal.

Po zkompilování funguje opět vše, jak má.

1> c(npdb_mob).
{ok,npdb_mob}
2> npdb_mob:start_link("data.db").
{ok,<0.40.0>}
3> npdb_mob:update(790003366, 104).
ok
4> npdb_mob:lookup(790003366).
{ok,104}
5> npdb_mob:delete(790003366).
ok
6> npdb_mob:lookup(790003366).
{error,instance}

Databáze Mnesia

Součástí distribuce Erlangu je databáze Mnesia. K ETS a DETS tabulkám přidává transakční vrstvu (více zápisů, které se buď provedou všechny, nebo neprovede žádný, pokud dojde k chybě) a další zajímavé vlastnosti. Např. automatická replikace, nebo indexování přes více sloupců.

Nehodí se moc na dnešní typy databází, ve kterých se nikdy nic nemaže a neustále rostou. Spíše může plnit úlohu úložiště pro aktuální data aplikace. Data se v případě potřeby (uživatel se nalogoval) načtou z velké SQL databáze, aplikace s nimi pracuje, změny se ukládají (pokud nejsou kritické, tak stačí asynchronně) zpět do SQL databáze a až přestanou být potřeba (uživatel se odlogoval nebo timeout), vymažou se.

Proti jednoduchým úložištím typu Redis či Memcache má Mnesia výhodu v tom, že může být distribuovaná, má transakce, může obsahovat více dat, než se vejde (chcete dát) do paměti a může mít dodatečné indexy. Navíc data mohou být uložena ve stejném runtime jako běží aplikace, takže odpadá režie s komunikací mezi aplikací a úložištěm (obvykle přes sockety).

Databáze Mnesia může nabídnou řešení častého dilematu, jak daleko umístit data od aplikace. Pokud jsou příliš centralizována, projeví se to v době přístupu, pokud jsou příliš decentralizována, je třeba řešit replikace. Mnesia za nutnost replikací umí dostat data přímo do aplikace. Což zejména u jednoduchých úloh, kde se hodně čte a málo zapisuje, může být zajímavé.

Příklad: zde popsaná databáze čísel má při použití jednoduché ETS tabulky dobu přístupu na čtení (jak dlouho trvá vykonání uživatelské vyhledávací funkce) řádově v jednotkách mikrosekund. Při použití Mnesia databáze jsou to desítky mikrosekund, což je ale stále méně než cokoliv, co komunikuje přes TCP/IP (stovky mikrosekund).

I když se databázi Mnesia říká databáze, je to stále jen vylepšené úložiště typu klíč/hodnota. Pokud se vaše aplikace neobejde bez soustavy do sebe zanořených SQL dotazů přes několik desítek řádek, není dobrý nápad ji zkoušet používat.

Povídání o databázi Mnesia je téma na samostatný seriál. Pro bližší seznámení doporučuji oficiální dokumentaci, která obsahuje i podrobného průvodce (Mnesia User's Guide).

Příště si ukážeme, jak k dnešnímu příkladu vytvořit dohlížecí proces (tzv. supervisor). A jak to celé zabalit do tzv. aplikace a nechat spustit při startu.

Byl pro vás článek přínosný?

Autor článku