Hlavní navigace

Curryfikace (currying), výjimky a vlastní operátory v jazyku ML

15. 2. 2022
Doba čtení: 25 minut

Sdílet

 Autor: Designphotos
Ve třetím článku o jazyce ML si ukážeme některé poněkud pokročilejší techniky. Seznámíme se s technikou curryfikace (curryingu), která je v ML podporována, podobně jako například v Haskellu.

Obsah

1. Curryfikace (currying) a částečně vyhodnocené funkce

2. Malá odbočka – funkce přistupující k symbolům ve svém prostředí (environment)

3. Praktické využití curryfikace

4. Skutečný počet parametrů všech funkcí v jazyce ML

5. Generování funkcí pro výpočet n-té mocniny

6. Druhý pohled na funkci vyššího řádu map

7. Proměnné a prostředí

8. Konstrukce let

9. Konstrukce let v těle funkce

10. Blok příkazů (s vedlejším efektem)

11. Koncept výjimek v jazyku ML

12. Standardní operátory

13. Zápis standardních operátorů

14. Definice vlastních operátorů

15. Změna priority a asociativity operátorů

16. Obsah navazujícího článku

17. Repositář s demonstračními příklady

18. Literatura

19. Odkazy na Internetu

1. Curryfikace (currying) a částečně vyhodnocené funkce

„Typically, developers learn new languages by applying what they know about existing languages. But learning a new paradigm is difficult – you must learn to see different solutions to familiar problems.“

V úvodní části dnešního článku si ukážeme, jakým způsobem se v programovacím jazyku ML provádí takzvaná curryfikace (anglicky currying). Pod tímto termínem se v teorii programovacích jazyků (ovšem i obecně v matematice) označuje proces, jímž se transformuje funkce, která má více než jeden parametr, do řady vložených funkcí, přičemž každá z nich má jen jediný parametr. Curryfikaci si můžeme představit jako postupnou transformaci funkce s n parametry na jinak zkonstruovanou funkci s n-1 parametry atd. až rekurzivně dojdeme k funkci s jediným parametrem:

x = f(a,b,c) →
    h = g(a)
    i = h(b)
    x = i(c)

Nebo na jediném řádku:

x = f(a,b,c) → g(a)(b)(c)
Poznámka: povšimněte si, že funkce g a h musí vracet jiné funkce.

To zní sice velmi složitě, ale v praxi je (v ML, ale i dalších jazycích) proces curryfikace realizován, jak uvidíme v dalším textu, z pohledu programátora automaticky již samotným zápisem funkce s větším množstvím parametrů. To nám umožňuje realizovat částečné vyhodnocení funkce (partial application), konkrétně zavoláním nějaké funkce (například funkce akceptující dva parametry) ve skutečnosti pouze s jediným parametrem. Jenže – co má být výsledkem volání takové funkce? Určitě ne výsledek implementované operace, protože nám chybí jeden parametr pro to, aby byl výsledek vypočten a vrácen volajícím kódu. Ovšem můžeme provést částečný výpočet dosazením (jediného) předaného parametru a výsledek – tento částečný výpočet – vrátit. Výsledkem je tedy obecně částečně aplikovaná funkce (tedy například funkce, které byly v předchozím příkladu označeny symboly g a h). Jedná se o jeden ze způsobů, jak programově (za běhu aplikace) vytvářet nové funkce.

Poznámka: curryfikace/currying se tedy ve skutečnosti poněkud liší od tvorby částečně aplikovaných funkcí (i když se mnohdy oba termíny zaměňují, nebo používají současně, což je ostatně i případ předchozího odstavce).
Poznámka2: název currying je odvozen od jména známého matematika Haskella Curryho, po kterém je ostatně pojmenován i další programovací jazyk Haskell (ten se ML v mnoha ohledech podobá, právě i v kontextu curryingu a s ním souvisejícím faktem, že funkce akceptují jeden parametr). Ve skutečnosti však Haskell tento proces nevymyslel. Za původní myšlenkou tohoto procesu stojí Moses Schönfinkel, takže se uvažovalo, že se tento proces bude nazývat „Schönfinkelisation“. To by bylo asi férovější, ovšem uznejte sami, že se nejedná o tak snadno zapamatovatelný název, jakým je currying.

2. Malá odbočka – funkce přistupující k symbolům ve svém prostředí (environment)

Zkusme si tedy ukázat, jak je currying interně implementován. Nejprve si ovšem ukážeme, že v programovacím jazyku ML je možné vytvářet funkce (ať již anonymní či pojmenované), které budou akceptovat nějaké parametry, ovšem navíc budou přistupovat k „externím“ proměnným dostupným z jejich prostředí (environment). Začneme jednoduchou anonymní funkcí, která akceptuje jediný parametr b, ovšem provádí výpočet a+b, jehož výsledek posléze vrací:

fn b => a + b;

Tuto funkci nelze zpracovat bez toho, aniž by byla v prostředí (environment, viz dále) deklarována proměnná a, na což nás překladač správně upozorní:

Elaboration failed: Unbound value identifier "a".
Poznámka: konkrétní chybové hlášení závisí na použitém dialektu jazyka ML, ovšem význam je zřejmý.

Proměnnou a však můžeme vytvořit, a to konstrukcí val. V takovém případě je již možné anonymní funkci zapsat, a to bez nahlášení chyby překladačem:

val a = 10;
 
fn b => a + b;

Výsledkem bude hodnota s typem:

val it = fn: int → int;
Poznámka: pro pochopení dalšího textu je užitečné se naučit číst typ funkce. Zde se konkrétně jedná o funkci akceptující jediný parametr typu int a vracející hodnotu typu int.

Po vyhodnocení výrazu jsme sice získali hodnotu typu funkce, ovšem tato hodnota byla pouze meziuložena do speciální proměnné it a bude brzy ztracena vyhodnocením jakéhokoli dalšího výrazu:

it(100);
val it = 110: int;

Nic nám však nebrání si výsledek, tedy funkci, uložit do proměnné, a de facto tak deklarovat pojmenovanou funkci:

val a = 10;
 
val f = fn b => a + b;

Výsledkem je nyní proměnná f s typem:

val f = fn: int → int;

Z tohoto zápisu již víme, že se jedná o funkci s jediným parametrem, kterou lze zavolat:

f(20);
val it = 30: int;

Popř. můžeme vytvořit anonymní funkci a ihned ji zavolat, to vše na jediném řádku:

val a = 10;
 
(fn b => a + b)(20);
val it = 30: int;

Nyní již víme, že funkce mohou využívat hodnoty proměnných, které jsou dostupné z jejich prostředí. Co to konkrétně znamená, se dozvíme později, ale v jednoduchosti se jedná o proměnné s hodnotou a typem známým v době tvorby funkce (může se jednat o globální proměnné, parametry funkce, v jejím rámci je nová funkce definována atd.).

3. Praktické využití curryfikace

Podívejme se nyní na následující definici funkce adder:

fun adder a = fn b => a + b;
val adder = fn: int → int → int;

Tato funkce je poměrně zajímavá, protože její návratovou hodnotou je další funkce s jediným parametrem b, která provádí výpočet a+b (viz návratový typ fn: int → int → int odvozený a následně i zobrazený překladačem).

Poznámka: operátor → má pravou asociativitu, což znamená, že zápis fn: int → int → int můžeme číst jako fn: int → (int → int). Mezi další operátory s pravou asociativitou patří operátory pro spojení seznamů a řetězců: „::“ a „@“.

Samotná funkce adder je tedy generátorem dalších funkcí, které se budou lišit tím, jaký výsledek počítají, a to na základě parametru a, jenž byl do funkce adder předán ve chvíli generování nových funkcí. Pokusme se takové funkce vytvořit (vygenerovat):

val inc = adder 1;
val inc2 = adder 2;

V předchozím příkladu jsme si nechali vygenerovat dvojici nových funkcí nazvaných inc a inc2, jejichž typy odvozené překladačem jsou:

val inc = fn: int → int;
val inc2 = fn: int → int;

Jedná se tedy o „běžné“ funkce s jediným parametrem (a pochopitelně i návratovou hodnotou), které lze běžným způsobem zavolat:

inc 10;
val it = 11: int;
 
inc2 10;
val it = 12: int;

Můžeme zde vidět, že každá z těchto funkcí provedla odlišný výpočet, a to z toho důvodu, že si každá funkce pamatuje odlišnou hodnotu a získanou při vytvoření těchto funkcí. Stále však platí, že se jedná o referenčně transparentní funkce – jejich návratové hodnoty plně závisí pouze na předaném parametru.

To však není vše a právě zde se dostáváme k curryingu. V programovacím jazyce ML je důležité, že zápis funkce adder, který jsme použili výše:

fun adder a = fn b => a + b;
val adder = fn: int → int → int;

…je možné přepsat do zjednodušené podoby:

fun adder a b = a + b;
val adder = fn: int → int → int;

Už z typů obou funkcí je zřejmé, že se jedná o totožnou sémantiku. V případě, že vytvoříme novou funkci (nezávisle na tom, zda funkci anonymní či pojmenovanou) s větším množstvím parametrů (než jeden) a současně parametry nezapíšeme formou n-tice do kulatých závorek, provede překladač currying zcela automaticky.

Poznámka: „běžná“ funkce se dvěma parametry (ve skutečnosti s jedním parametrem typu n-tice), která má sečíst dvě celočíselné hodnoty, vypadá takto:
fun adder(a, b) = a + b;
val adder = fn: int * int → int;

Už z typu funkce vypsaného překladačem je zřejmé, že se jedná o značně odlišnou funkci.

4. Skutečný počet parametrů všech funkcí v jazyce ML

Na celý problém se můžeme podívat i naopak – lze říci, že takto definované funkce s větším počtem parametrů se interně reprezentují funkcí s jediným parametrem, které vrací jinou funkci, která na sebe naváže druhý parametr (a tato funkce popř. vrací další funkci navazující třetí parametr atd.). Můžeme si to vyzkoušet na curryfikované funkci se čtyřmi parametry:

fun foobar x y z w = x + y + z + w;
val foobar = fn: int → int → int → int → int;

Jedná se o generátor generátorů funkcí:

val f1 = foobar(1);
val f1 = fn: int → int → int → int;
 
val f2 = f1(2);
val f2 = fn: int → int → int;
 
val f3 = f2(3);
val f3 = fn: int → int;
 
val f4 = f3(4);
val f4 = 10: int;

Alternativní způsob generování funkcí (různých typů) při použití jednořádkového zápisu:

fun foobar x y z w = x + y + z + w;
val foobar = fn: int → int → int → int → int;
 
val f12 = foobar(1)(2);
val f12 = fn: int → int → int;
 
val f123 = foobar(1)(2)(3);
val f123 = fn: int → int;
 
val f1234 = foobar(1)(2)(3)(4);
val f1234 = 10: int;

Ve skutečnosti ovšem můžeme závorky zcela vynechat:

fun foobar x y z w = x + y + z + w;
val foobar = fn: int → int → int → int → int;
 
val f1 = foobar 1;
val f1 = fn: int → int → int → int;
 
val f2 = f1 2;
val f2 = fn: int → int → int;
 
val f3 = f2 3;
val f3 = fn: int → int;
 
val f4 = f3 4;
val f4 = 10: int;

Ve druhém případě pak:

fun foobar x y z w = x + y + z + w;
val foobar = fn: int → int → int → int → int;
 
val f12 = foobar 1 2;
val f12 = fn: int → int → int;
 
val f123 = foobar 1 2 3;
val f123 = fn: int → int;
 
val f1234 = foobar 1 2 3 4;
val f1234 = 10: int;
 
Poznámka: bližší vysvětlení, o jaké funkce se jedná (jaké mají parametry a návratovou hodnotu) je zde zbytečné, neboť je to patrné z jejich typu odvozeného překladačem.

5. Generování funkcí pro výpočet n-té mocniny

Currying lze využít například pro generování funkcí pro výpočet zvolené n-té mocniny. Budeme přitom vycházet z rekurzivní funkce, která dokáže vypočítat mn pro dvě celočíselné hodnoty m a n. Základní podoba této funkce není nijak složitá ani překvapující:

fun pow(m, n) = if n = 0 then 1
                else m * pow(m, n-1);

Funkci lze snadno otestovat (pro jednoduchost neuvažujeme zápornou mocninu):

pow(2, 8);
val it = 256: int;
 
pow(3, 2);
val it = 9: int;

Ve druhém kroku tuto funkci upravíme. Nejprve prohodíme oba parametry, což je sice na první pohled nelogické, ovšem currying (a na něj navázané vytváření částečně vyhodnocených funkcí) vždy „odstraňuje“ první parametr, což by v našem případě měla být mocnina a nikoli základ:

fun pow(m, n) = if n = 0 then 1
                else n * pow(m-1, n)

Výsledky (se stejnými vstupními parametry) budou podle očekávání odlišné, protože nyní je prvním parametrem mocnina a druhým parametrem základ:

pow(2, 8);
val it = 64: int;
 
pow(3, 2);
val it = 8: int;

Třetí úprava spočívá v náhradě podmínky za čitelnější pattern matching:

fun pow(0, n) = 1
  | pow(m, n) = n * pow(m-1, n);
 

S výsledky:

pow(2, 8);
val it = 64: int;
 
pow(3, 2);
val it = 8: int;

A konečně můžeme funkci přepsat do podoby, v níž je automaticky proveden currying. Přepis je ryze mechanický a spočívá v odstranění závorek při volání funkce a ve vlastních vzorcích:

fun pow 0 n = 1
  | pow m n = n * pow(m-1) n;

Alternativně:

fun pow m n = if m = 0 then 1
              else n * pow(m-1) n;

Důležitý je typ této funkce:

val pow = fn: int → int → int;

Tuto funkci můžeme zavolat, a to opět bez použití závorek, protože ve skutečnosti voláme funkci s jediným parametrem; výsledkem je další funkce, které předáme druhý parametr:

pow 2 8;
val it = 64: int;
 
pow 3 2;
val it = 8: int;

Důležitější však je, že si můžeme nechat vygenerovat částečně vyhodnocené funkce. První pro výpočet druhé mocniny, druhou pro výpočet třetí mocniny:

val square = pow 2;
val square = fn: int → int;
 
val cube = pow 3;
val cube = fn: int → int;

Tyto funkce lze již bez problémů zavolat:

square 10;
val it = 100: int;
 
cube 10;
val it = 1000: int;

6. Druhý pohled na funkci vyššího řádu map

V předchozím článku jsme si kromě dalších věcí ukázali i implementaci funkce nazvané map. Jedná se o funkci vyššího řádu, jejímž prvním parametrem je jiná funkce, která je aplikována na všechny prvky seznamu. Funkce map může být realizována tak, že se využije currying, což je ostatně patrné i z jejího zápisu:

(* Implementace funkce map *)
 
fun map f [] = []
  | map f (head::tail) = (f head) :: (map f tail);

Za povšimnutí stojí typ funkce map, který ukazuje, že překladač jazyka ML skutečně korektně rozeznal typy obou parametrů (tedy že první parametr je funkce, která transformuje prvky typu a na prvky typu b) a z toho též odvodil druhý parametr (seznam prvků typu a) i návratovou hodnotu (seznam prvků typu b). Přitom to, o jaké konkrétní typy a a b se jedná, se rozhodne až při volání této funkce – funkce je tedy generická (resp. přesněji řečeno polymorfická):

val map = fn: ∀ 'a 'b . ('a → 'b) → 'a list → 'b list;

Funkci map lze použít například pro seznam číselných hodnot typu real:

fun half x = x / 2.0;
 
map half [1.0,2.0,3.0,4.0,5.0];

Možný je i zkrácený zápis s využitím anonymní funkce:

map(fn x => x/2.0, [1.0, 2.0, 3.0]);

Nic nám ovšem nebrání použít map pro vygenerování dalších funkcí pracujících nad všemi prvky nějakého vstupního seznamu, což je již poměrně silná programovací technika:

fun map f [] = []
  | map f (head::tail) = (f head) :: (map f tail);
val map = fn: ∀ 'a 'b . ('a → 'b) → 'a list → 'b list;
 
val incrementor = map (fn n => n+1);
val incrementor = fn: int list → int list;
 
incrementor [1,2,3,4];
val it = [2, 3, 4, 5]: int list;
 
val doubler = map (fn n => n*2);
val doubler = fn: int list → int list;
 
doubler [1,2,3,4];
val it = [2, 4, 6, 8]: int list;

7. Proměnné a prostředí

Již v úvodní kapitole jsme se setkali s pojmem prostředí neboli environment. Tento pojem se používá především při definici funkcí, které nějakým způsobem přistupují k již deklarovaným proměnným. V takové chvíli je zapotřebí vědět, jaká hodnota (a typ) se v dané konkrétní funkci použije a zda nebude mít změna proměnné vliv na to, jak se funkce bude vyhodnocovat.

Podívejme se na jednoduchý příklad, v němž je definována funkce nazvaná incr. Ta vrátí součet dvou hodnot, přičemž jedna hodnota je získána přímo z parametru funkce a druhá hodnota je přečtena z proměnné x, která musí být deklarována před definicí funkce a být součástí jejího prostředí:

val x = 1 : int
 
fun incr y = x + y;
 
incr 1;
 
val x = 10;
incr 1;

Po spuštění tohoto prográmku by se měly na terminál vypsat tyto řádky:

val x = 1: int;
 
val incr = fn: int → int;
 
val it = 2: int;
 
val x = 10: int;
 
val it = 2: int;

Tyto řádky vypsané jazykem ML postupně znamenají:

Řádek Význam
val x = 1: int; vytvoření proměnné x s hodnotou 1 a typem int
val incr = fn: int → int; vytvoření funkce incr, která má jediný parametr typu int a vrací int
val it = 2: int; výsledek po zavolání funkce incr s parametrem 1
val x = 10: int; změna hodnoty proměnné x
val it = 2: int; výsledek po zavolání funkce incr s parametrem 1

Vidíme, že funkce incr pro stejný vstupní parametr (1) vždy vrací stejnou hodnotu, která není závislá na změně hodnoty proměnné x po deklaraci funkce. Důležité je si uvědomit, že ve funkci incr ve skutečnosti nepracujeme přímo s původní proměnnou x. Namísto toho je v aktuálním prostředí (environment) vytvořena vazba na původní hodnotu (a typ). Výsledkem toho je fakt, že další změny proměnné x příkazem val nemají žádný vliv na to, jakou hodnotu vrátí funkce incr. Jediný vliv na návratovou hodnotu bude mít hodnota argumentu této funkce v čase jejího volání. Jinými slovy – tato vlastnost nejenom zabraňuje nepříjemným zmatkům, ale především zajišťuje referenční transparentnost funkcí, což je (nejenom) ve funkcionálním programování velmi důležitá vlastnost.

Poznámka: současně nám ovšem tato vlastnost nezabraňuje měnit hodnotu proměnné, x je tedy stále měnitelná (mutable).

8. Konstrukce let

Další vlastnost programovacího jazyka ML je s velkou pravděpodobností inspirována jazykem Scheme. Jedná se o konstrukci let, která umožňuje vytvářet bloky s lokálními symboly, s nimiž je možné v těchto blocích pracovat, ovšem vně bloku nebudou viditelné. Podívejme se na jednoduchý příklad, v němž je použit lokální symbol x:

let
    val x = 10
in
    2*x
end;

Výsledkem bude hodnota:

val it = 20: int;

Přičemž vně celého bloku nebude x viditelné:

x;
Elaboration failed: Unbound value identifier "x".

Navíc se celý blok let chápe jako výraz, takže můžeme psát:

let
    val x = 10
in
    2*x
end + 1;

Příklad složitějšího výrazu s blokem let:

val i = 10
val j : real = 10.0
val k = i
 
val lexpr =
  let val x = 1
      val y = 2
  in x + y
  end;
 
val lexpr = 3 : int
Poznámka: zde mají lokální symboly x a y pochopitelně přednost před symboly vnějšími.

V případě, že vně bloku již existuje proměnná x, je její hodnota a typ zachována:

val x = "foobar";
val x = "foobar": string;
 
x;
val it = "foobar": string;
 
let
    val x = 10
in
    2*x
end;
val it = 20: int;
 
x;
val it = "foobar": string;

Na druhou stranu proměnné definované vně bloku jsou v tomto bloku dostupné – viz použití proměnné z uvnitř bloku:

val z = 2;
 
let
    val x = 10
in
    z*x
end;
 
val it = 20: int;

9. Konstrukce let v těle funkce

S výše popsaným blokem let se velmi často setkáme při tvorbě složitějších funkcí. Například v následující funkci, která má vypočítat vzdálenost bodu [x,y] od počátku souřadné soustavy, využijeme dvou mezivýpočtů, což sice funkci prodlouží, ale pojmenování výsledků mezivýpočtů zde má (mj.) i dokumentační efekt:

fun distance(x, y) =
let
    val squaredx = x*x
    val squaredy = y*y
in
    Math.sqrt(squaredx + squaredy)
end;

Otestování základní funkcionality:

distance(1.0, 1.0);
val it = 1.4142135623730951: real;

10. Blok příkazů (s vedlejším efektem)

V programovacím jazyku ML lze vytvářet popř. volat funkce s vedlejším efektem. Nejedná se tedy o referenčně transparentní funkce, ovšem na druhou stranu jde o praktický nástroj určený pro použití v reálných programech. Takovou typickou funkcí s vedlejším efektem je funkce print:

print 42;

Při vyhodnocování takové funkce se jako vedlejší efekt vypíše hodnota 42:

Printed: 42

S funkcemi s vedlejším efektem souvisí i bloky příkazů, které u referenčně transparentních funkcí postrádají smysl. Blok příkazů je zapisován do kulatých závorek stylem:

(<expression>;<expression>;...)

Podívejme se na příklad použití – rozšíříme funkce distance o výpis mezivýsledků při jejím zavolání:

fun distance(x, y) =
let
    val squaredx = x*x
    val squaredy = y*y
in
  (
    print("Squared x=", x);
    print("Squared y=", y);
    Math.sqrt(squaredx + squaredy)
  )
end;

Funkci si vyzkoušejme zavolat:

distance(1.0, 1.0);
Printed: ("Squared x=", 1.0)
Printed: ("Squared y=", 1.0)
val it = 1.4142135623730951: real;
Poznámka: některé varianty programovacího jazyka ML nejdříve vypíšou výsledek funkce a teprve poté výsledek volání print.

11. Koncept výjimek v jazyku ML

S výjimkami jsme se krátce setkali již minule, a to konkrétně ve funkci, která měla vracet první prvek ze seznamu. V případě, že byl seznam prázdný, došlo k vyhození výjimky Empty:

(* 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"]);

Ve skutečnosti je však velmi snadné přidat si do aplikací vlastní nový typ výjimky. Postačuje použít tento zápis:

exception Nomatch;

Přitom exception je klíčové slovo a Nomatch je nový typ výjimky. Tuto výjimku můžeme použít ve funkci nazvané member, která testuje, zda v nějakém seznamu existuje prvek a. Pokud je seznam prázdný (a to po rekurzivním sestupu), vyhodí se právě výjimka Nomatch:

fun member(a, []) = raise Nomatch
 |  member(a, b::y) = if a == b then b::y
                      else member(a, y);

Zachytávání výjimek vypadá syntakticky následovně:

<epxression1> handle <exception_name> => <expression2>

To znamená, že můžeme psát:

fun check(a,x) = member(a, x) handle Nomatch =>
   (print("error!"); []);

12. Standardní operátory

V programovacím jazyce ML nalezneme relativně velké množství operátorů. Musíme si však uvědomit, že operátory jsou definovány jako běžné funkce a samotný zápis operátoru je pouhý syntaktický cukr (ovšem velmi praktický):

<výraz1> <operátor;> <výraz2>

je totožný se zápisem:

<operátor;>(<výraz1>, <výraz2>)

V následující tabulce jsou vypsány všechny standardní operátory programovacího jazyka ML. Ty se od sebe liší (pochopitelně) nejenom prováděnou operací, ale taktéž prioritou a asociativitou. Priorita určuje, v jakém pořadí se budou operátory vyhodnocovat, pokud jsou použity v jediném výrazu (například ve výrazu 1+2*3). A asociativita určuje uzávorkování operátorů se stejnou prioritou, tj. zda se provede (1+2)+3 nebo naopak 1+(2+3). Z tabulky je patrné, že většina operátorů je asociativní zleva, ovšem skupina dvou operátorů s prioritou 5 má asociativitu zprava (jedná se konkrétně o operátory určené pro spojení řetězců popř. seznamů, s nimiž jsme se seznámili minule):

Operátor Priorita Asociativita
* 7 zleva
/ 7 zleva
div 7 zleva
mod 7 zleva
+ 6 zleva
6 zleva
^ 6 zleva
:: 5 zprava
@ 5 zprava
= 4 zleva
<> 4 zleva
> 4 zleva
>= 4 zleva
< 4 zleva
<= 4 zleva
:= 3 zleva
o 3 zleva
before 0 zleva

13. Zápis standardních operátorů

Všechny výše uvedené operátory jsou interně implementovány jako funkce a dále jsou definovány slovy infix a infixr:

infix  7  * / div mod
infix  6  + - ^
infixr 5  :: @
infix  4  = <> > >= < <=
infix  3  := o
infix  0  before
Poznámka: povšimněte si, že ne všechny priority jsou ve skutečnosti obsazeny standardními operátory, což pochopitelně není náhoda.

14. Definice vlastních operátorů

V programovacím jazyku ML sice není (prakticky) možné přetěžovat standardní operátory, ovšem samotný jazyk nám nezabraňuje ve vytváření operátorů nových. U nových operátorů lze určit jejich prioritu i asociativitu, což je velmi užitečné (podle mého skromného názoru dokonce mnohem užitečnější, než pouhé přetěžování existujících operátorů).

Prvním krokem je definice funkce se dvěma parametry, která bude následně zaregistrována jako nový binární operátor. Můžeme si například vyzkoušet funkci pro umocnění mn. S touto funkcí jsme se již setkali v úvodních kapitolách, takže jen ve zkratce:

fun pow(m, 0) = 1
  | pow(m, n) = m * pow(m, n-1);

Funkci převedeme na operátor, konkrétně na operátor asociativní zleva s prioritou 8:

infix 8 pow;

Nyní je již možné nový operátor začít používat, a to se všemi výhodami, které přináší statický typový systém programovacího jazyka ML:

2 pow 3;
2 pow 3 + 1;
Poznámka: ve skutečnosti asi budete chtít pravou asociativitu operátoru pow:
infixr 8 pow;

15. Změna priority a asociativity operátorů

Podívejme se nyní na ukázky dalších uživatelsky definovaných operátorů. Užitečný může být operátor exists pro detekci, zda seznam obsahuje určitý prvek či nikoli (lepší by bylo operátor pojmenovat in, toto klíčové slovo je však již obsazeno). Vlastní implementace funkce exists je relativně snadná:

fun exists(element, []) = false
  | exists(element, head::tail) = if element = head then true
                                  else exists(element, tail);

Funkci si samozřejmě můžeme otestovat:

exists(2, [1,2,3]);
exists(0, [1,2,3]);
exists(42, [42]);
exists(42, []);

A následně ji převést na operátor, a to konkrétně operátor s druhou nejnižší prioritou:

infix 1 exists;

Nyní se bude test existence prvku v seznamu zapisovat skutečně jako operátor:

2 exists [1,2,3];
0 exists [1,2,3];
42 exists [42];
42 exists [];

Sami si vyzkoušejte, jaký vliv má druhá nejnižší priorita (1) na tyto výrazy:

root_podpora

1+1 exists [1,2,3];
3*1 exists [1,2,3];
1+1 exists [1,2] @ [3,4];

16. Obsah navazujícího článku

Prozatím jsme si do značné míry vystačili se základními datovými typy jazyka ML. Ovšem velká síla tohoto jazyka spočívá v principu deklarace nových datových typů. Podporován je algebraický typový systém, což je téma, kterému bude věnován celý čtvrtý díl seriálu o jazyku ML.

17. 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:

# Příklad Popis příkladu Cesta
1 01_currying1.ml anonymní funkce přistupující k proměnné uložené v jejím prostředí https://github.com/tisnik/ml-examples/tree/master/arti­cle03/01_currying1.ml
2 02_currying2.ml anonymní funkce přistupující k proměnné uložené v jejím prostředí https://github.com/tisnik/ml-examples/tree/master/arti­cle03/02_currying2.ml
3 03_currying3.ml předchozí příklad, ovšem zapsaný na jediném řádku https://github.com/tisnik/ml-examples/tree/master/arti­cle03/03_currying3.ml
4 04_currying4.ml částečná aplikace funkce https://github.com/tisnik/ml-examples/tree/master/arti­cle03/04_currying4.ml
5 05_pow_curry1.ml generátor funkce pro výpočet n-té mocniny https://github.com/tisnik/ml-examples/tree/master/arti­cle03/05_pow_curry1.ml
6 06_pow_curry2.ml generátor funkce pro výpočet n-té mocniny, verze s pattern matchingem https://github.com/tisnik/ml-examples/tree/master/arti­cle03/06_pow_curry2.ml
7 07_currying4_params.ml currying funkce se čtyřmi parametry, verze s neanonymní funkcí, zápis se závorkami https://github.com/tisnik/ml-examples/tree/master/arti­cle03/07_currying4_params­.ml
8 08_currying4_params.ml currying funkce se čtyřmi parametry, verze s neanonymní funkcí, zápis bez závorek https://github.com/tisnik/ml-examples/tree/master/arti­cle03/08_currying4_params­.ml
9 09_environment.ml prostředí a vazba na funkce https://github.com/tisnik/ml-examples/tree/master/arti­cle03/09_environment.ml
10 10_distance.ml výpočet vzdálenosti bodu od počátku https://github.com/tisnik/ml-examples/tree/master/arti­cle03/10_distance.ml
11 11_exceptions1.ml práce s výjimkami https://github.com/tisnik/ml-examples/tree/master/arti­cle03/11_exceptions1.ml
12 12_exceptions2.ml práce s výjimkami https://github.com/tisnik/ml-examples/tree/master/arti­cle03/12_exceptions2.ml
13 13_increment.ml zvýšení hodnoty prvků https://github.com/tisnik/ml-examples/tree/master/arti­cle03/13_increment.ml
14 14_increment.ml zvýšení hodnoty prvků https://github.com/tisnik/ml-examples/tree/master/arti­cle03/14_increment.ml
15 15_let3.ml blok let https://github.com/tisnik/ml-examples/tree/master/arti­cle03/15_let3.ml
16 16_pow_operator.ml operátor pow https://github.com/tisnik/ml-examples/tree/master/arti­cle03/16_pow_operator.ml
17 17_simple_let1.ml blok let https://github.com/tisnik/ml-examples/tree/master/arti­cle03/17_simple_let1.ml
18 18_simple_let2.ml blok let https://github.com/tisnik/ml-examples/tree/master/arti­cle03/18_simple_let2.ml
19 19_exists.ml operátor exists https://github.com/tisnik/ml-examples/tree/master/arti­cle03/19_exists.ml
20 20_pow_associativity.ml operátor pow se změnou asociativity https://github.com/tisnik/ml-examples/tree/master/arti­cle03/20_pow_associativity­.ml
21 21_exists1.ml operátor exists https://github.com/tisnik/ml-examples/tree/master/arti­cle03/21_exists1.ml
22 22_exists2.ml operátor exists https://github.com/tisnik/ml-examples/tree/master/arti­cle03/22_exists2.ml
23 23_exists3.ml operátor exists https://github.com/tisnik/ml-examples/tree/master/arti­cle03/23_exists3.ml

18. Literatura

Poznámka: v této kapitole jsou uvedeny nejenom knihy o jazyku ML resp. Standard ML, ale i knihy o programovacím jazyku OCaml, který ze Standard ML ze značné míry vychází.
  1. ML for the Working Programmer
    https://www.cl.cam.ac.uk/~lp15/MLbo­ok/pub-details.html
  2. Elements of ML Programming, 2nd Edition (ML97)
    http://infolab.stanford.e­du/~ullman/emlp.html
  3. A tour of Standard ML
    https://saityi.github.io/sml-tour/tour/welcome
  4. The History of Standard ML
    https://smlfamily.github.i­o/history/SML-history.pdf
  5. The Standard ML Basis Library
    https://smlfamily.github.io/Basis/
  6. Programming in Standard ML
    http://www.cs.cmu.edu/~rwh/is­ml/book.pdf
  7. Programming in Standard ML '97: A Tutorial Introduction
    http://www.lfcs.inf.ed.ac­.uk/reports/97/ECS-LFCS-97–364/
  8. Programming in Standard ML '97: An On-line Tutorial
    https://homepages.inf.ed.ac­.uk/stg/NOTES/
  9. The OCaml system release 4.13
    https://ocaml.org/releases/4­.13/htmlman/index.html
  10. Real World OCaml: Functional programming for the masses
    https://dev.realworldocaml.org/
  11. OCaml from the Very Beginning
    http://ocaml-book.com/
  12. OCaml from the Very Beginning: More OCaml : Algorithms, Methods & Diversions
    http://ocaml-book.com/more-ocaml-algorithms-methods-diversions/
  13. Unix system programming in OCaml
    http://ocaml.github.io/ocamlunix/
  14. OCaml for Scientists
    https://www.ffconsultancy­.com/products/ocaml_for_sci­entists/index.html
  15. Using, Understanding, and Unraveling The OCaml Language
    https://caml.inria.fr/pub/docs/u3-ocaml/
  16. Developing Applications With objective Caml
    https://caml.inria.fr/pub/docs/oreilly-book/index.html
  17. Introduction to Objective Caml
    http://courses.cms.caltech­.edu/cs134/cs134b/book.pdf
  18. How to Think Like a (Functional) Programmer
    https://greenteapress.com/thin­kocaml/index.html

19. Odkazy na Internetu

  1. Standard ML of New Jersey
    https://www.smlnj.org/
  2. Programming Languages: Standard ML – 1 (a navazující videa)
    https://www.youtube.com/wat­ch?v=2sqjUWGGzTo
  3. 6 Excellent Free Books to Learn Standard ML
    https://www.linuxlinks.com/excellent-free-books-learn-standard-ml/
  4. SOSML: The Online Interpreter for Standard ML
    https://sosml.org/
  5. ML (Computer program language)
    https://www.barnesandnoble­.com/b/books/other-programming-languages/ml-computer-program-language/_/N-29Z8q8Zvy7
  6. Strong Typing
    https://perl.plover.com/y­ak/typing/notes.html
  7. What to know before debating type systems
    http://blogs.perl.org/user­s/ovid/2010/08/what-to-know-before-debating-type-systems.html
  8. Types, and Why You Should Care (Youtube)
    https://www.youtube.com/wat­ch?v=0arFPIQatCU
  9. DynamicTyping (Martin Fowler)
    https://www.martinfowler.com/bli­ki/DynamicTyping.html
  10. DomainSpecificLanguage (Martin Fowler)
    https://www.martinfowler.com/bli­ki/DomainSpecificLanguage­.html
  11. Language Workbenches: The Killer-App for Domain Specific Languages?
    https://www.martinfowler.com/ar­ticles/languageWorkbench.html
  12. Effective ML (Youtube)
    https://www.youtube.com/watch?v=-J8YyfrSwTk
  13. Why OCaml (Youtube)
    https://www.youtube.com/wat­ch?v=v1CmGbOGb2I
  14. CSE 341: Functions and patterns
    https://courses.cs.washin­gton.edu/courses/cse341/04wi/lec­tures/03-ml-functions.html
  15. Comparing Objective Caml and Standard ML
    http://adam.chlipala.net/mlcomp/
  16. 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
  17. Cheat Sheets (pro OCaml)
    https://www.ocaml.org/doc­s/cheat_sheets.html
  18. Syllabus (FAS CS51)
    https://cs51.io/college/syllabus/
  19. Abstraction and Design In Computation
    http://book.cs51.io/
  20. Learn X in Y minutes Where X=Standard ML
    https://learnxinyminutes.com/doc­s/standard-ml/
  21. CSE307 Online – Summer 2018: Principles of Programing Languages course
    https://www3.cs.stonybrook­.edu/~pfodor/courses/summer/cse307­.html
  22. CSE307 Principles of Programming Languages course: SML part 1
    https://www.youtube.com/wat­ch?v=p1n0_PsM6hw
  23. CSE 307 – Principles of Programming Languages – SML
    https://www3.cs.stonybrook­.edu/~pfodor/courses/summer/CSE307/L01_SML­.pdf
  24. SML, Some Basic Examples
    https://cs.fit.edu/~ryan/sml/in­tro.html
  25. History of programming languages
    https://devskiller.com/history-of-programming-languages/
  26. History of programming languages (Wikipedia)
    https://en.wikipedia.org/wi­ki/History_of_programming_lan­guages
  27. 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/
  28. The Evolution Of Programming Languages
    https://www.i-programmer.info/news/98-languages/8809-the-evolution-of-programming-languages.html
  29. Evoluce programovacích jazyků
    https://ccrma.stanford.edu/cou­rses/250a-fall-2005/docs/ComputerLanguagesChart.png
  30. Poly/ML Homepage
    https://polyml.org/
  31. PolyConf 16: A brief history of F# / Rachel Reese
    https://www.youtube.com/wat­ch?v=cbDjpi727aY
  32. Programovací jazyk Clojure 18: základní techniky optimalizace aplikací
    https://www.root.cz/clanky/pro­gramovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/
  33. Moscow ML Language Overview
    https://itu.dk/people/ses­toft/mosml/mosmlref.pdf
  34. ForLoops
    http://mlton.org/ForLoops
  35. Funkcionální dobrodružství v JavaScriptu
    https://blog.kolman.cz/2015/12/fun­kcionalni-dobrodruzstvi-v-javascriptu.html
  36. Recenze knihy Functional Thinking (Paradigm over syntax)
    https://www.root.cz/clanky/recenze-knihy-functional-thinking-paradigm-over-syntax/
  37. Currying
    https://sw-samuraj.cz/2011/02/currying/
  38. Používání funkcí v F#
    https://docs.microsoft.com/cs-cz/dotnet/fsharp/tutorials/using-functions
  39. Funkce vyššího řádu
    http://naucte-se.haskell.cz/funkce-vyssiho-radu
  40. Currying (Wikipedia)
    https://en.wikipedia.org/wi­ki/Currying
  41. Currying (Haskell wiki)
    https://wiki.haskell.org/Currying
  42. Haskell Curry
    https://en.wikipedia.org/wi­ki/Haskell_Curry
  43. Moses Schönfinkel
    https://en.wikipedia.org/wi­ki/Moses_Sch%C3%B6nfinkel

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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.