Obsah
1. Referenčně transparentní funkce a jejich význam při optimalizaci aplikací
2. Definice běžných funkcí, volání funkcí s předáváním parametrů
8. Pattern matching pro více variant
13. Spojení řetězců předaných v seznamu
15. Odvození typu parametru z typu návratové hodnoty
17. Příloha: funkce pro operaci nad seznamy
18. Repositář s demonstračními příklady
1. Referenčně transparentní funkce a jejich význam při optimalizaci aplikací
Již v úvodním článku o programovacím jazyce ML jsme si řekli, že tento jazyk patří, společně s klasickým LISPem, Scheme, Haskellem či Erlangem do skupiny (ne vždy nutně čistě) funkcionálních jazyků, tj. programovacích jazyků vycházejících z teorie takzvaného λ-kalkulu, jehož autorem je Alonzo Church (na první návrhy LISPu se dokonce můžeme dívat jako na jeden z formalizovaných způsobů zápisu λ-kalkulu, pro nějž jen tak mimochodem existuje mechanismus vyhodnocování jednotlivých λ výrazů; taktéž se tím například vysvětluje přítomnost znaku lambda v logu jazyka Clojure nebo Racketu). Ve skutečnosti sice ML není čistě funkcionálním jazykem, ovšem v případě, že vývojář bude při tvorbě svých aplikací dodržovat zásady funkcionálního programování, bude pro něj mnohem snadnější vytvářet skutečně výkonné aplikace (navíc bezpečné z hlediska souběhu).
Připomeňme si taktéž, že v programovacím jazyce ML jsou funkce považovány za plnohodnotné datové typy, což znamená, že funkce lze navázat na libovolný symbol (a tím vlastně původně anonymní funkci pojmenovat), funkce lze předávat jako parametry do jiných funkcí a funkce mohou být taktéž návratovou hodnotou jiných funkcí – funkce tedy může vytvořit a vrátit jinou funkci. ML taktéž podporuje práci s uzávěry (closure(s)), tj. funkcí svázaných s nějakým symbolem (symboly) vytvořenými vně funkce. Podpora uzávěrů umožňuje například tvorbu funkcí sdílejících společný kontext (GUI), líné vyhodnocování atd. Taktéž lze ovšem vytvářet funkce s vedlejším efektem, které například zapisují data do souborů, mění hodnotu navázanou na globální symboly atd.
Vývojáři by však neměli tyto možnosti nabízené programovacím jazykem ML zneužívat, protože tím znemožňují využití některých optimalizačních technik a v neposlední řadě si taktéž komplikují možnost testování takto vytvořených funkcí. Namísto toho se ukazuje být velmi výhodné vytvářet takzvané referenčně transparentní funkce, což jsou funkce, které nepřistupují k žádným globálním symbolům, nemají žádný vedlejší efekt ani si nepamatují žádný vnitřní stav (příkladem „funkce“ s vnitřním stavem je například funkce random). Referenčně transparentní funkci jsou při jejím volání předány parametry a funkce pouze na základě hodnot předaných parametrů vrátí nějaký výsledek. Tato (pochopitelná) vlastnost má jeden důležitý důsledek – chování referenčně transparentní funkce je nezávislé na stavu aplikace a je taktéž zcela nezávislé na tom, kdy je funkce zavolána.
2. Definice běžných funkcí, volání funkcí s předáváním parametrů
Připomeňme si, jakým způsobem se v programovacím jazyku ML definují funkce. V případě, že se má jednat o pojmenovanou (tedy neanonymní) funkci, používá se pro definici takové funkce snadno zapamatovatelné slovo fun, za nímž následuje jméno funkce, seznam formálních parametrů, znak = a tělo funkce (už z tohoto zápisu je patrné, že se počítá s tím, že se jedná o referenčně transparentní funkce).
Příklad definice funkce s jediným parametrem:
(* Definice funkce s jedním parametrem *) fun inc n = n + 1;
Takto definovanou funkci lze zavolat dvěma způsoby – bez kulatých závorek popř. naopak s využitím závorek:
inc 1; inc(1);
Velmi podobně lze definovat funkci se dvěma parametry:
(* Definice funkce se dvěma parametry *) fun add (x, y) = x + y;
V tomto případě však bude volání vyžadovat použití kulatých závorek:
add(3,4);
V dalším textu se budeme zabývat datovými typy, takže se podívejme, jakého typu jsou obě výše definované funkce. Typ funkce je v ML odvozen nejenom od počtu parametrů, ale i od typů těchto parametrů i typu návratové hodnoty. V případě, že tyto typy nejsou přímo určeny programátorem (a to v našem případě nejsou), bude se provádět odvozování. V těchto konkrétních případech se typ odvodí z výrazů n + 1 a x + y následovně:
fun inc n = n + 1; > val inc = fn: int → int; fun add (x, y) = x + y; > val add = fn: int * int → int;
3. Rekurzivní funkce
Rekurze, neboli volání nějaké funkce v těle té samé funkce (přímá rekurze) nebo prostřednictvím funkce jiné (takzvaná nepřímá rekurze), představuje jednu ze základních programátorských technik, na kterých je ML postaven, podobně jako další funkcionální jazyky. Můžeme zde spatřovat velkou inspiraci Lispem, ve kterém se rekurze také velmi často používá; ostatně samotné Lispovské příkazy, například apply, map či forall jsou definovány rekurzivně, podobně jako v dalším Lispovsky orientovaném jazyce – ve Scheme. Samotná myšlenka rekurze je však starší než všechny programovací jazyky, protože je hluboce zakořeněna jak v podstatě některých přírodních i umělých jevů či objektů, tak i v matematice, v například v definicích různých algebraických a geometrických struktur.
Definice přímé rekurzivní funkce v jazyce ML je zcela bezproblémová – nejsou zapotřebí žádné dopředné („forward“) deklarace atd. A pochopitelně jsou stále odvozovány popř. hlídány datové typy předávaných parametrů i návratových hodnot:
(* Naivní implementace funkce length *) fun length(x) = if null(x) then 0 else 1 + length(tl(x));
Typ této funkce je zajímavý – funkce bude akceptovat seznam libovolného typu, tj. jedná se o generickou funkci ('a zde značí „any“):
val length = fn: ∀ 'a . 'a list → int;
Otestování je snadné:
length([]); > val it = 0: int; length([1]); > val it = 1: int; length([1,2,3,4]); > val it = 4: int;
4. Pattern matching
Výše uvedená implementace funkce length není pro jazyk ML idiomatická – spíše se podobá přímému přepisu z LISPu nebo ze Scheme. Častěji se setkáme s použitím pattern matchingu, kterým se (v tomto případě) určují těla funkce pro různé vstupní podmínky. Díky tomu lze velmi snadno a především přehledně vyjmenovat například všechny mezní podmínky. Pro funkci length je zde jediná mezní podmínka a tou je předání prázdného seznamu (další možnost je jen jedna – předal se neprázdný seznam; jiná možnost díky typovému systému není povolena):
(* Implementace funkce length založená na pattern matchingu *) fun length([]) = 0 | length(lst) = 1 + length(tl(lst));
Vzhledem k tomu, že v hlavičce funkce je jediný parametr (v obou případech), můžeme vynechat kulaté závorky:
(* Implementace funkce length založená na pattern matchingu *) fun length [] = 0 | length lst = 1 + length(tl(lst));
Předchozí použití pattern matchingu ovšem ani zdaleka neukazuje všechny možnosti programovacího jazyka ML v této oblasti. Díky existenci operátoru :: (zápis připojení prvku k seznamu, obdoba cons z LISPu/Clojure) můžeme přímo ve druhé větvi specifikovat, že pokud se na vstupu objeví seznam s hlavičkou a tělem (tedy má alespoň jeden prvek), má se provést tato větev a funkce length se má rekurzivně volat s tělem seznamu (které už může být prázdné):
(* Implementace funkce length založená na pattern matchingu *) fun length([]) = 0 | length(head::tail) = 1 + length(tail);
Vzhledem k tomu, že se hodnota hlavičky seznamu (jeho prvního prvku) nikde nepoužívá, lze zápis ještě více zjednodušit náhradou identifikátoru za podtržítko:
(* Implementace funkce length založená na pattern matchingu *) fun length([]) = 0 | length(_::tail) = 1 + length(tail);
5. Generická funkce append
Další rekurzivní funkcí, kterou je možné v jazyku ML velmi snadno implementovat, je funkce, která k existujícímu seznamu připojí druhý seznam (již jsme se s ní seznámili minule). Pro tuto operaci sice existuje specializovaný operátor @, ale zajímavější bude se pokusit funkci implementovat vlastními silami:
(* Naivní implementace funkce append *) fun append(x, y) = if null(x) then y else hd(x) :: append(tl(x), y);
Povšimněte si, že ML automaticky odvodil jak typ parametrů této funkce, tak i typ návratové hodnoty. Pro tuto operaci musel analyzovat interně volané funkce, tedy null, hd a tl:
val append = fn: ∀ 'a . 'a list * 'a list → 'a list;
Funkci append si můžeme otestovat, a to včetně mezních případů – jeden ze seznamů může být prázdný:
append([], [1, 2, 3]); > val it = [1, 2, 3]: int list; append([1, 2, 3], []); > val it = [1, 2, 3]: int list; append([1, 2, 3], [4, 5]); > val it = [1, 2, 3, 4, 5]: int list; append([], []); > val it = []: '~A list, '~A free;
Vzhledem k tomu, že se opět jedná o generickou funkci, můžeme jí předat seznamy jiného typu:
append(["foo", "bar"], ["baz"]); > val it = ["foo", "bar", "baz"]: string list;
Nebo taktéž:
append([[1,2], [3,4,5]], [[6,7,8], [9]]); val it = [[1, 2], [3, 4, 5], [6, 7, 8], [9]]: int list list;
Idiomatičtější je opět použití pattern matchingu, které může v případě funkce append vypadat následovně:
(* Implementace funkce append založená na pattern matchingu *) fun append([], y) = y | append(head::tail, y) = head :: append(tail, y);
6. Přetěžování operátorů?
Tvůrci jazyka ML se při jeho návrhu snažili o to, aby nebylo nutné příliš často explicitně specifikovat datové typy parametrů funkcí, lokálních proměnných či výrazů. Zastavme se na chvíli právě u výrazů. Pro relativně velké množství zcela odlišných datových typů existuje operace typu „spoj“ či „sečti“. Týká se to například celých čísel, reálných čísel, seznamů, ale i řetězců. V některých programovacích jazycích se tato operace aplikovaná na různé datové typy reprezentuje totožným operátorem +, který je v tomto kontextu přetížený.
V programovacím jazyku ML je sice operátor + zdánlivě taktéž přetížen, protože ho lze použít jak pro typ int, tak i pro typ real. Ve skutečnosti tomu tak není, protože tento operátor je definován pro typ num, což je typ získaný sloučením (union) typů int a real (někdy též word). Ke sloučeným datovým typům se dostaneme později a mimochodem se jedná o jednu z nejsilnějších vlastností jazyka ML. Ovšem vraťme se k operátoru +, jehož typový popis vypadá následovně:
val + : num * num -> num
kde:
num = int union real
nebo:
num = word union int union real
Další podobné datové typy pro variantu jazyka ML podporující word a word8:
Sjednocení | Základní datové typy |
---|---|
realint | int, real |
wordint | int, word, word8 |
num | int, real, word, word8 |
numtxt | int, real, word, word8, char, string |
To ovšem znamená, že pro spojování řetězců nebo seznamů se musí použít jiné funkce nebo jiné operátory. Skutečně tomu tak je; existuje totiž speciální operátor určený pouze pro spojení dvou řetězců a jiný operátor určený pro spojení dvou seznamů. Bez zdlouhavého vysvětlování jejich funkce se podívejme na jejich typový popis, z něhož už by mělo být vše zřejmé:
val @ : ('a list * 'a list) -> 'a list val ^ : string * string -> string
7. Použití operátoru @
Operátor @ určený pro spojování seznamů je možné použít i pro takové operace, pro které není striktně určen. Můžeme například vytvořit funkci reverse, která celý seznam otočí a to tak, že postupně bude odebírat prvky z jednoho konce seznamu (ve fázi navíjení) a poté je připojovat za druhý konec seznamu (ve fází odvíjení). A operátor @ se použije z toho důvodu, že :: lze použít pouze pro připojení prvku na začátek seznamu, nikoli na jeho konec. Ve výsledku získáme funkci se složitostí O(n2):
(* Naivní implementace funkce reverse *) fun reverse(x) = if null(x) then x else reverse(tl(x)) @ [hd(x)];
Tato funkce je typu:
val reverse = fn: ∀ 'a . 'a list → 'a list;
Opět se tedy jedná o generickou funkci, která není závislá na typu prvků seznamu (ovšem výsledný seznam bude stejného typu).
Otestovat funkci reverse můžeme i s využitím prázdného seznamu na vstupu:
reverse([]); reverse([1,2]); reverse([1,2,3,4]);
Idiomatický zápis používající pattern matching:
(* Implementace funkce reverse pattern matchingem *) fun reverse([]) = [] | reverse(lst) = reverse(tl(lst)) @ [hd(lst)];
Ještě lepší je zápis, v němž můžeme vynechat volání tl a hd (to se stejně provede, ale skrytě):
(* Implementace funkce reverse pattern matchingem *) fun reverse([]) = [] | reverse(head::tail) = reverse(tail) @ [head];
8. Pattern matching pro více variant
Prozatím jsme si ukazovali funkce, v nichž se pattern matching používal pro rozhodnutí mezi dvěma variantami, takže by se mohlo zdát, že se jedná o jakousi formu rozhodovací konstrukce if-then-else. Ve skutečnosti může variant existovat větší množství, takže se spíše jedná o konstrukci typu switch (ovšem bez přidružených problémů, které tato konstrukce do některých jazyků přinesla). Podívejme se nejprve na to, jak by se v jazyku ML mohla definovat funkce pro výpočet n-tého členu slavné Fibonacciho posloupnosti. Jedno z možných řešení přímo vychází z jedné varianty matematické definice této posloupnosti (existuje ještě varianta s F0=0):
F1 = 1 F2 = 1 Fn = Fn-1 + Fn-2
Přepis tohoto předpisu bez použití pattern matchingu bude vypadat takto:
(* Naivní implementace výpočtu Fibonacciho posloupnosti *) fun fib n = if n = 0 then 0 else if n = 1 then 1 else fib (n - 1) + fib (n - 2);
Otestování:
fib 0; fib 1; fib 10;
Ve skutečnosti ovšem můžeme vzít matematický předpis a prakticky doslova ho přepsat do ML:
(* Implementace výpočtu Fibonacciho posloupnosti s využitím pattern matchingu *) fun fib 0 = 0 | fib 1 = 1 | fib n = fib (n - 1) + fib (n - 2);
9. Koncová rekurze
Vraťme se ještě k funkci length, kterou jsme zapsali takto:
(* Implementace funkce length založená na pattern matchingu *) fun length([]) = 0 | length(_::tail) = 1 + length(tail);
Z paměťového i časového hlediska se ovšem nejedná o nejlepší zápis algoritmu, protože se zde používá skutečná rekurze. Lepší bude funkci přepsat tak, aby bylo možné použít koncovou rekurzi. Ta je založena na tom, že pokud se na konci funkce rekurzivně volá ta samá funkce a s výsledkem se již žádným způsobem nemanipuluje (třeba se k němu nic nepřičítá), lze interně rekurzi převést na smyčku. Jedno z možných řešení může vypadat takto:
fun accumulate ([], a) = a | accumulate ((_::tail), a) = accumulate(tail, (1 + a)); fun length lst = accumulate(lst, 0);
Vytvořili jsme zde pomocnou funkci accumulate, která je sice rekurzivní, ale využívá se zde koncové rekurze – funkce sice volá sama sebe, ale takovým způsobem, že není nutné používat zásobník. Tuto pomocnou funkci pak zavoláme s tím, že akumulátor a je inicializován na nulu.
fun length lst = let fun accumulate [] a = a | accumulate (_::tail) a = accumulate tail (1 + a) in accumulate lst 0 end
10. Funkce vyššího řádu
Ve funkcionálních programovacích jazycích mají funkce stejně plnohodnotný význam, jako jakékoli jiné datové typy. Výjimkou není, jak již ostatně víme, ani programovací jazyk ML, v němž je typ funkce odvozen od typů parametrů a návratové hodnoty. Pokud je ovšem funkce plnohodnotným datovým typem, znamená to, že může být předána jako parametr do jiné funkce popř. vrácena jako návratová hodnota (jiné) funkce. Funkcím, které jako svůj parametr akceptují jinou funkci popř. které vrací funkci, se říká funkce vyššího řádu. Takové funkce jsou v jazyku ML plně podporovány a nalezneme je i ve standardní knihovně tohoto jazyka. Podívejme se na asi nejtypičtější prakticky použitelnou funkci vyššího řádu. Jedná se o funkci map, která aplikuje (jinou) funkci na jednotlivé prvky seznamu, přičemž výsledkem bude nový seznam. Funkci map lze realizovat rekurzivně:
(* Implementace funkce map *) fun map f [] = [] | map f (head::tail) = (f head) :: (map f tail);
Pokud ve vstupním seznamu existuje alespoň jeden prvek (head), je na něj aplikována funkce f předaná jako parametr a rekurzivně se zavolá map pro zbytek seznamu. Zajímavý je typ funkce map získaný automatickým odvozením:
val map = fn: ∀ 'a 'b . ('a → 'b) → 'a list → 'b list;
11. Otestování funkce map
Funkci map si můžeme snadno otestovat, například tak, že na prvky seznamu budeme aplikovat funkci inc:
fun inc x = x + 1; map inc [1,2,3];
S výsledkem:
val it = [2, 3, 4]: int list;
Funkci map ovšem můžeme použít i pro výpočty nad vektory reálných čísel:
fun half x = x / 2.0; map half [1.0,2.0,3.0,4.0,5.0];
Tentokrát s výsledkem:
val it = [0.5, 1.0, 1.5, 2.0, 2.5]: real list;
Ovšem stále je zaručena typová korektnost:
map half [1, 2, 3, 4, 5]; Elaboration failed: Type clash. Functions of type "real list → real list" cannot take an argument of type "int list": Cannot merge "real" and "int".
12. Explicitní určení typů
Vraťme se opět k funkci sum, kterou jsme definovali následovně:
(* Výpočet součtu prvků v seznamu *) fun sum([]) = 0 | sum(x::y) = x + sum y;
U takto definované funkce je odvozen datový typ:
val sum = fn: int list → int;
Což znamená, že je funkce použitelná pouze pro seznamy celých čísel.
Alternativně je možné náhradou 0 za 0.0 dosáhnout toho, že funkce bude použitelná pro seznamy reálných čísel (ovšem již ne čísel celých):
(* Výpočet součtu prvků v seznamu *) fun sum([]) = 0.0 | sum(x::y) = x + sum y; sum([1.1, 2.2, 3.3]);
Typ můžeme specifikovat i explicitně, například u parametru (a klidně i v jediné větvi):
(* Výpočet součtu prvků v seznamu *) fun sum([] : real list) = 0.0 | sum(x::y) = x + sum y; sum([1.1, 2.2, 3.3]);
Podobně je tomu u funkce add:
(* Definice funkce se dvěma parametry *) fun add (x, y) = x + y; add(3,4);
Explicitní specifikace typu jednoho z parametrů (ovlivní i + a tím i druhý parametr):
(* Definice funkce se dvěma parametry typu real *) fun add(x:real,y) = x+y; add(1.2, 3.4);
13. Spojení řetězců předaných v seznamu
Spojení dvou řetězců lze provést operátorem ^, takže je snadné napsat rekurzivní podobu funkce, která spojí všechny řetězce předané v seznamu. Stále přitom používáme stejný základ – pattern matching, rozdělení seznamu na hlavu a tělo a rekurzi:
(* Spojení řetězců předaných v seznamu *) fun join([] : string list) = "" | join(x::y) = x ^ join y; join(["foo", " ", "bar", " ", "baz"]);
14. Vyvolání výjimky
Již na konci předchozího článku jsme si ukázali funkci, která vrátí první prvek seznamu. Pokud první prvek neexistuje, vyvolá se výjimka:
(* Vrácení prvního prvku ze seznamu *) fun car([]) = raise Empty | car(x::y) = x; car([]); car([1]); car([1,2]); car([1,2,3]); car(["foo", "bar"]);
val car = fn: ∀ 'a . 'a list → 'a;
15. Odvození typu parametru z typu návratové hodnoty
Kouzlo typového systému programovacího jazyka ML spočívá v tom, že typ parametru lze odvodit z typu návratové hodnoty (či naopak). Pokud je tedy typ návratové hodnoty určen explicitně, bude to bráno v úvahu v typovém odvozování. A to i tehdy, pokud je typ návratové hodnoty určen v jediné větvi pattern matchingu (viz podtrženou část kódu):
(* Vrácení prvního prvku ze seznamu *) fun car([]) = raise Empty | car(x::y) = x : int;
Nyní bude typ funkce car odvozen do:
val car = fn: int list → int;
zatímco předchozí typ byl:
val car = fn: ∀ 'a . 'a list → 'a;
16. Typ option
Na závěr se musíme zmínit o velmi užitečném datovém typu nazvaném option (ten jsme si již popsali v seriálu o Rustu). Tento datový typ se používá například ve chvílích, kdy je zapotřebí reprezentovat neznámou hodnotu, chybovou hodnotu (ovšem bez vyvolání výjimky), vytvořit funkci s volitelnými parametry či vytvořit typově bezpečnou obdobu odkazu typu null (null samo o sobě je v IT zlo :-). Typ option si můžeme představit jako unii se dvěma hodnotami NONE a SOME, přičemž hodnota SOME je kontejnerem pro jinou hodnotu, například výsledek výpočtu.
Poměrně typickým příkladem je funkce pro dělení, která ve chvíli, kdy je dělitel nulový, vrátí hodnotu NONE a při nenulovém děliteli pak hodnotu SOME, která obaluje vypočtený podíl:
(* Použití typu option *) fun divide(x, 0) = NONE | divide(x, y) = SOME(x div y); divide(10, 5); divide(10, 0);
Typ funkce je:
val divide = fn: int * int → int option;
A vypočtené výsledky:
val it = SOME 2: int option; val it = NONE: int option;
17. Příloha: funkce pro operaci nad seznamy
V příloze jsou vypsány standardní funkce určené pro provádění operací nad seznamy. U funkcí schválně neuvádím bližší popis, ale pouze jejich typový popis, z něhož je (při kombinaci s názvem funkce) mnohdy přímo patrné jaká operace se provádí. Jedná se o další z předností silného typového systému:
Funkce | Typový popis |
---|---|
null | 'a list → bool |
length | 'a list → int |
@ | 'a list * 'a list → 'a list |
hd | 'a list → 'a |
tl | 'a list → 'a list |
last | 'a list → 'a |
getItem | 'a list → ('a * 'a list) option |
nth | 'a list * int → 'a |
take | 'a list * int → 'a list |
drop | 'a list * int → 'a list |
rev | 'a list → 'a list |
concat | 'a list list → 'a list |
revAppend | 'a list * 'a list → 'a list |
app | ('a → unit) → 'a list → unit |
map | ('a → 'b) → 'a list → 'b list |
mapPartial | ('a → 'b option) → 'a list → 'b list |
find | ('a → bool) → 'a list → 'a option |
filter | ('a → bool) → 'a list → 'a list |
partition | ('a → bool) → 'a list → 'a list * 'a list |
foldl | ('a * 'b → 'b) → 'b → 'a list → 'b |
foldr | ('a * 'b → 'b) → 'b → 'a list → 'b |
exists | ('a → bool) → 'a list → bool |
all | ('a → bool) → 'a list → bool |
tabulate | int * (int → 'a) → 'a list |
collate | ('a * 'a → order) → 'a list * 'a list → order |
18. Repositář s demonstračními příklady
Všechny výše popsané demonstrační příklady byly uloženy do repositáře dostupného na adrese https://github.com/tisnik/ml-examples/. V tabulce umístěné pod tímto odstavcem jsou uvedeny odkazy na tyto příklady:
19. Literatura
- ML for the Working Programmer
https://www.cl.cam.ac.uk/~lp15/MLbook/pub-details.html - Elements of ML Programming, 2nd Edition (ML97)
http://infolab.stanford.edu/~ullman/emlp.html - A tour of Standard ML
https://saityi.github.io/sml-tour/tour/welcome - The History of Standard ML
https://smlfamily.github.io/history/SML-history.pdf - The Standard ML Basis Library
https://smlfamily.github.io/Basis/ - Programming in Standard ML
http://www.cs.cmu.edu/~rwh/isml/book.pdf - Programming in Standard ML '97: A Tutorial Introduction
http://www.lfcs.inf.ed.ac.uk/reports/97/ECS-LFCS-97–364/ - Programming in Standard ML '97: An On-line Tutorial
https://homepages.inf.ed.ac.uk/stg/NOTES/ - The OCaml system release 4.13
https://ocaml.org/releases/4.13/htmlman/index.html - Real World OCaml: Functional programming for the masses
https://dev.realworldocaml.org/ - OCaml from the Very Beginning
http://ocaml-book.com/ - OCaml from the Very Beginning: More OCaml : Algorithms, Methods & Diversions
http://ocaml-book.com/more-ocaml-algorithms-methods-diversions/ - Unix system programming in OCaml
http://ocaml.github.io/ocamlunix/ - OCaml for Scientists
https://www.ffconsultancy.com/products/ocaml_for_scientists/index.html - Using, Understanding, and Unraveling The OCaml Language
https://caml.inria.fr/pub/docs/u3-ocaml/ - Developing Applications With objective Caml
https://caml.inria.fr/pub/docs/oreilly-book/index.html - Introduction to Objective Caml
http://courses.cms.caltech.edu/cs134/cs134b/book.pdf - How to Think Like a (Functional) Programmer
https://greenteapress.com/thinkocaml/index.html
20. Odkazy na Internetu
- Standard ML of New Jersey
https://www.smlnj.org/ - Programming Languages: Standard ML – 1 (a navazující videa)
https://www.youtube.com/watch?v=2sqjUWGGzTo - 6 Excellent Free Books to Learn Standard ML
https://www.linuxlinks.com/excellent-free-books-learn-standard-ml/ - SOSML: The Online Interpreter for Standard ML
https://sosml.org/ - ML (Computer program language)
https://www.barnesandnoble.com/b/books/other-programming-languages/ml-computer-program-language/_/N-29Z8q8Zvy7 - Strong Typing
https://perl.plover.com/yak/typing/notes.html - What to know before debating type systems
http://blogs.perl.org/users/ovid/2010/08/what-to-know-before-debating-type-systems.html - Types, and Why You Should Care (Youtube)
https://www.youtube.com/watch?v=0arFPIQatCU - DynamicTyping (Martin Fowler)
https://www.martinfowler.com/bliki/DynamicTyping.html - DomainSpecificLanguage (Martin Fowler)
https://www.martinfowler.com/bliki/DomainSpecificLanguage.html - Language Workbenches: The Killer-App for Domain Specific Languages?
https://www.martinfowler.com/articles/languageWorkbench.html - Effective ML (Youtube)
https://www.youtube.com/watch?v=-J8YyfrSwTk - Why OCaml (Youtube)
https://www.youtube.com/watch?v=v1CmGbOGb2I - CSE 341: Functions and patterns
https://courses.cs.washington.edu/courses/cse341/04wi/lectures/03-ml-functions.html - Comparing Objective Caml and Standard ML
http://adam.chlipala.net/mlcomp/ - What are the key differences between Standard ML and OCaml?
https://www.quora.com/What-are-the-key-differences-between-Standard-ML-and-OCaml?share=1 - Cheat Sheets (pro OCaml)
https://www.ocaml.org/docs/cheat_sheets.html - Syllabus (FAS CS51)
https://cs51.io/college/syllabus/ - Abstraction and Design In Computation
http://book.cs51.io/ - Learn X in Y minutes Where X=Standard ML
https://learnxinyminutes.com/docs/standard-ml/ - CSE307 Online – Summer 2018: Principles of Programing Languages course
https://www3.cs.stonybrook.edu/~pfodor/courses/summer/cse307.html - CSE307 Principles of Programming Languages course: SML part 1
https://www.youtube.com/watch?v=p1n0_PsM6hw - CSE 307 – Principles of Programming Languages – SML
https://www3.cs.stonybrook.edu/~pfodor/courses/summer/CSE307/L01_SML.pdf - SML, Some Basic Examples
https://cs.fit.edu/~ryan/sml/intro.html - History of programming languages
https://devskiller.com/history-of-programming-languages/ - History of programming languages (Wikipedia)
https://en.wikipedia.org/wiki/History_of_programming_languages - Jemný úvod do rozsáhlého světa jazyků LISP a Scheme
https://www.root.cz/clanky/jemny-uvod-do-rozsahleho-sveta-jazyku-lisp-a-scheme/ - The Evolution Of Programming Languages
https://www.i-programmer.info/news/98-languages/8809-the-evolution-of-programming-languages.html - Evoluce programovacích jazyků
https://ccrma.stanford.edu/courses/250a-fall-2005/docs/ComputerLanguagesChart.png - Poly/ML Homepage
https://polyml.org/ - PolyConf 16: A brief history of F# / Rachel Reese
https://www.youtube.com/watch?v=cbDjpi727aY - Programovací jazyk Clojure 18: základní techniky optimalizace aplikací
https://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Moscow ML Language Overview
https://itu.dk/people/sestoft/mosml/mosmlref.pdf