Súčtové typy

Úvod

Pri návrhu údajových typov potrebujeme vyjadriť nielen to, z akých zložiek sa údaj skladá, ale aj to, ktorú z viacerých podôb môže mať. V štvrtej prednáške sme prvú situáciu opísali súčinovými typmi: hodnota typu $T_1\times T_2$ obsahuje súčasne obe zložky. Teraz sa zameriame na alternatívy, pri ktorých je prítomná práve jedna z prípustných možností.

Takéto rozlíšenie potrebujeme napríklad pri modelovaní výsledkov operácií. Vyhľadávanie môže nájsť záznam alebo oznámiť jeho neprítomnosť; geometrický objekt môže byť bodom, kruhom alebo obdĺžnikom. Každá alternatíva pritom môže niesť iné údaje. Typ výsledku má tieto možnosti vyjadriť tak, aby bolo možné určiť, ktorá nastala a aké údaje sú v nej dostupné.

Príklad

Uvažujme funkciu, ktorá podľa identifikátora tovaru vyhľadá počet kusov na sklade. Pri existujúcom zázname má výsledok obsahovať prirodzené číslo; ak záznam neexistuje, potrebuje oznámiť neprítomnosť údaja. Číslo $0$ nemôžeme použiť ako jednoznačné označenie nenájdeného záznamu, pretože je aj platným počtom kusov.

Tieto dva výsledky môžeme reprezentovať súčtovým typom $\mathsf{Nat}+\mathsf{Unit}$. Jedna alternatíva obsahuje nájdený počet kusov, druhá oznamuje neprítomnosť záznamu. Už zavedený typ $\mathsf{Unit}$ má jedinú hodnotu, takže v druhej alternatíve nepotrebujeme prenášať ďalší údaj. Rozhodujúce je označenie alternatívy: prítomná nula a neprítomný záznam sú odlišné hodnoty výsledného typu.

Súčtový typ tak zachytáva štruktúru alternatív priamo v type údaja. Hodnotu vytvárame výberom a označením jednej možnosti; spracúvame ju rozborom prípadov. Typové pravidlá pri tomto rozbore overia, že sú pokryté všetky alternatívy a že údaje v každej vetve používame podľa ich typu. Tento princíp využijeme aj pri variantoch s pomenovanými alternatívami.

V tejto prednáške budú predstavené:

  • binárne súčtové typy, ich vzťah k súčinovým typom a možnosti reprezentácie údajov;
  • injekcie a selektor case spolu s príslušnými typovacími a vyhodnocovacími pravidlami;
  • úloha typových anotácií, variantové typy a ich využitie pri enumeračných typoch, zabalenie hodnôt a voliteľné hodnoty.

Nadväzujeme na notáciu tretej a štvrtej prednášky:

  • $\Gamma\vdash t:T$ je typovací úsudok.
  • $t\to t'$ jeden vyhodnocovací krok.
  • $t\to^*t'$ nula alebo viac krokov.
  • Zápis $[x\mapsto s]t$ označuje substitúciu bez zachytenia.
  • Budeme používať redukčnú stratégiu volanie hodnotou.
  • Číslice $1,2,\ldots$ sú skratky numerických hodnôt, napríklad $1=\mathsf{succ}\ 0$.

Poznámka

V neskorších príkladoch budeme na ilustračné účely používať aj typy ako $\mathsf{Real}$, $\mathsf{Float}$ či $\mathsf{String}$ a príslušné operácie, hoci sme ich formálne nezaviedli. V danom príklade ich budeme považovať za predpokladané rozšírenie vstavaných typov; nové pravidlá pre súčty a varianty od tejto voľby nezávisia.

Binárny súčtový typ

Nech $T_1$ a $T_2$ sú typy. Binárny súčtový typ $T_1+T_2$ vytvoríme typovým konštruktorom $+$. Jeho hodnoty obsahujú buď hodnotu typu $T_1$ označenú ako ľavá alternatíva, alebo hodnotu typu $T_2$ označenú ako pravá alternatíva.

Samotný term $t_1:T_1$ ešte nie je termom typu $T_1+T_2$. Do súčtu ho musíme vložiť injekciou. Označenie alternatívy je podstatné aj vtedy, keď $T_1=T_2$: ľavé a pravé vloženie tej istej hodnoty zostávajú rozlíšiteľné.

Injekcie binárneho súčtového typu

Pre súčtový typ $T_1+T_2$ zavedieme dva termové konštruktory, nazývané injekcie:

  • $\mathsf{inl}$ (left injection) vloží term typu $T_1$ do ľavej alternatívy;
  • $\mathsf{inr}$ (right injection) vloží term typu $T_2$ do pravej alternatívy.

Ich úlohu pre pevne zvolené $T_1,T_2$ môžeme schematicky vyjadriť takto:

$$ \begin{aligned} \mathsf{inl}&:T_1\to(T_1+T_2),\\ \mathsf{inr}&:T_2\to(T_1+T_2). \end{aligned} $$

Tento zápis opisuje úlohu konštruktorov; v gramatike nižšie sú $\mathsf{inl}\ t$ a $\mathsf{inr}\ t$ samostatné tvary termov, nie aplikácie voľných premenných inl a inr.

Nie každý term súčtového typu je syntakticky injekciou: môže ním byť aj premenná, aplikácia funkcie alebo výraz case. Kanonické hodnoty súčtového typu však majú tvar ľavej alebo pravej injekcie s hodnotou vo vnútri. Injekcia s ešte nevyhodnoteným argumentom hodnotou nie je.

Poznámka

Typový konštruktor $+$ vytvára typ z typov. Termové konštruktory $\mathsf{inl}$ a $\mathsf{inr}$ vytvárajú term z termu a označujú zvolenú alternatívu. Pri pomenovaných variantoch sa názvy alternatív bežne nazývajú konštruktory; návestie samo však nie je premennou ani typovým konštruktorom.

Selektor $\mathsf{case}$

Hodnotu súčtového typu spracujeme rozborom prípadov (case analysis). Jeho eliminátor, nazývaný aj selektor, má tvar:

$$ \begin{array}{rll} &\mathsf{case}\ t\ \mathsf{of}&\\ &\mathsf{inl}\ x_1&\Rightarrow t_1\\ \mid&\mathsf{inr}\ x_2&\Rightarrow t_2. \end{array} $$

Term $t$ má typ $T_1+T_2$. Premenná $x_1:T_1$ je viazaná iba v tele $t_1$ a premenná $x_2:T_2$ iba v tele $t_2$; neviažu výskyty v $t$ ani v druhej vetve. Obe vetvy musia mať rovnaký výsledný typ.

Pri vyhodnocovaní najprv vyhodnotíme analyzovaný term $t$. Podľa injekcie vyberieme vetvu a do jej tela dosadíme vnútornú hodnotu za voľné výskyty príslušnej premennej. Substitúcia zabraňuje zachyteniu, podľa potreby premenovaním viazaných premenných. Nezvolená vetva sa nevyhodnocuje.

Príklad

Nech $\Gamma\vdash t_1:\mathsf{Nat}$ a $\Gamma\vdash t_2:\mathsf{Bool}$. Potom môžeme odvodiť:

$$ \Gamma\vdash\mathsf{inl}\ t_1:\mathsf{Nat}+\mathsf{Bool}, \qquad \Gamma\vdash\mathsf{inr}\ t_2:\mathsf{Nat}+\mathsf{Bool}. $$

Ide o termy súčtového typu, nie o samotné termy typov $\mathsf{Nat}$ a $\mathsf{Bool}$. Zatiaľ používame neanotované injekcie; problém určenia druhej zložky typu vysvetlíme nižšie.

Súčinové a súčtové typy sú duálne v spôsobe konštruovania a eliminácie:

  • Súčin vytvoríme poskytnutím oboch zložiek; údaje vyberáme projekciami.
  • Súčet vytvoríme výberom jednej alternatívy; údaje spracujeme rozborom oboch prípadov.

Projekcie zo súčinu a injekcie do súčtu

V logickej interpretácii zodpovedá súčin konjunkcii a súčet disjunkcii. Na vytvorenie dvojice potrebujeme oba údaje; na vytvorenie súčtovej hodnoty postačuje jeden z nich spolu s označením alternatívy.

Rozšírenie syntaxe jednoducho typovaného $\lambda$-kalkulu o binárny súčtový typ

Najprv pridáme neanotované injekcie. Pôvodnú gramatiku rozšírime o nové tvary termov $t$, hodnôt $v$ a typov $T$:

$$ \begin{aligned} t::={}&\ldots\mid\mathsf{inl}\ t\mid\mathsf{inr}\ t\mid\mathsf{case}\ t\ \mathsf{of}\ \mathsf{inl}\ x_1\Rightarrow t_1\ \texttt{'|'}\ \mathsf{inr}\ x_2\Rightarrow t_2,\\ v::={}&\ldots\mid\mathsf{inl}\ v\mid\mathsf{inr}\ v,\\ T::={}&\ldots\mid T+T. \end{aligned} $$

Symbol $\mid$ v BNF oddeľuje syntaktické možnosti. Zápis $\texttt{'|'}$ v úvodzovkách označuje terminálny symbol jazyka, ktorý oddeľuje dve vetvy jedného výrazu case; úvodzovky do samotného termu nepíšeme.

Tri bodky zastupujú všetky doteraz zavedené tvary vrátane $n$-tíc a záznamov. Konštruktor $+$ pridáva nový spôsob tvorby zložených typov. Význam množín $\mathcal{A}$ a $\mathcal{B}$ sa nemení.

Priorita a asociativita typových konštruktorov

Menšie číslo znamená vyššiu prioritu:

Typový operátor Priorita Konvencia zápisu
$\times$ 1 Vnorené binárne súčiny zátvorkujeme.
$+$ 2 Vnorené binárne súčty zátvorkujeme.
$\to$ 3 Viaže sa doprava.

Asociativita zápisu určuje, kam doplníme zátvorky. Neznamená rovnosť rôzne vnorených typov. V našom jazyku $A\times(B\times C)$ a $(A\times B)\times C$ nie sú syntakticky ani definične rovnaké typy; ich hodnoty majú inú štruktúru dvojíc a iné projekcie. Sú však izomorfné, čo označujeme $\cong$: údaje možno prevádzať oboma smermi a tieto prevody sa na hodnotách navzájom rušia.

Analogicky platí $A+(B+C)\cong(A+B)+C$, ale príslušné injekcie sú inak vnorené. Pri binárnych súčtoch preto vždy uvádzame zátvorky. Zápis $A\times B\times C$ zo štvrtej prednášky označuje priamy trojzložkový súčin, ktorého hodnotou je trojica $\langle a,b,c\rangle$. Ani ten automaticky nestotožňujeme s vnorenou dvojicou.

Príklad

Zátvorky doplníme podľa priority a pravej asociativity šípky. Znak $=$ tu porovnáva dva zápisy toho istého syntaktického typu:

$$ \begin{aligned} A\to B\to C&=A\to(B\to C),\\ A\times B+C&=(A\times B)+C,\\ A\times B+C\to D&=((A\times B)+C)\to D,\\ A\times B\to C+D&=(A\times B)\to(C+D). \end{aligned} $$

Naproti tomu pri zmene vnorenia píšeme iba izomorfizmus:

$$ A\times(B\times C)\cong(A\times B)\times C, \qquad A+(B+C)\cong(A+B)+C. $$

Napríklad prevod

$$\lambda p:A\times(B\times C).\langle\langle p.1,(p.2).1\rangle,(p.2).2\rangle$$

zmení pravé vnorenie dvojíc na ľavé. Zátvorky teda menia štruktúru typu, hoci množstvo prenášaných údajov zostáva rovnaké.

Sumy a jednoznačnosť typov

V STLC znamená jednoznačnosť typov, že pre pevné $\Gamma$ a $t$ z úsudkov $\Gamma\vdash t:T$ a $\Gamma\vdash t:S$ vyplýva $T=S$. Term má v danom kontexte najviac jeden typ.

V doterajšom rozšírení s neanotovanými injekciami táto vlastnosť neplatí. Ich prirodzené typovacie pravidlá povoľujú ľubovoľnú druhú zložku súčtu:

$$ \frac{\Gamma\vdash t:T_1}{\Gamma\vdash\mathsf{inl}\ t:T_1+T_2} \qquad \frac{\Gamma\vdash t:T_2}{\Gamma\vdash\mathsf{inr}\ t:T_1+T_2}. $$

Ak $\Gamma\vdash t:\mathsf{Nat}$, potom $\mathsf{inl}\ t$ môže mať typ $\mathsf{Nat}+\mathsf{Bool}$ aj $\mathsf{Nat}+\mathsf{Unit}$. Pri $\mathsf{inr}\ t$ zostáva neurčená ľavá zložka. Z typu vloženého termu teda nedokážeme jednoznačne syntetizovať výsledný typ injekcie.

Algoritmus už nemôže iba čítať pravidlá zdola nahor a určovať jediný typ každého podtermu ako doteraz. To však neznamená nemožnosť typovej kontroly: ak už poznáme očakávaný typ $T_1+T_2$, vieme ľavú injekciu skontrolovať voči nemu overením argumentu proti $T_1$.

Metódy kontroly typov pre súčtové typy

Zachováme tri možné prístupy:

  • Odvodenie alebo rekonštrukcia typov. Chýbajúcu zložku nahradíme neznámou a zbierame obmedzenia z okolitého programu. Unifikácia ju môže určiť; ak informácia chýba, nemusí vzniknúť jediný konkrétny typ.
  • Rozšírenie typového systému. Vhodne navrhnuté podtypovanie s najmenším typom môže napríklad umožniť najprv priradiť ľavej injekcii typ $T_1+\mathsf{Bot}$ a potom ho rozšíriť na zamýšľaný súčet. Vyžaduje to nové typy a pravidlá; pôvodná syntax tento problém nerieši. Pri podtypovaní môže mať term viac typov, takže cieľom je algoritmicky určiť vhodný najmenší typ, nie zachovať pôvodnú vetu o jednoznačnosti. Tento prístup tu nezavádzame.
  • Explicitné typové anotácie. Programátor uvedie zamýšľaný súčtový typ, a tým aj chýbajúcu zložku. V praktických jazykoch môže túto informáciu poskytovať aj deklarácia typu alebo okolitého výrazu.

Poznámka

V ďalšom výklade zvolíme tretí prístup a budeme vyžadovať explicitnú anotáciu celého výsledného typu injekcie.

Na pripísanie typu nadviažeme zápisom $\mathsf{inl}\ t\ \mathsf{as}\ (T_1+T_2)$ a obdobne pre $\mathsf{inr}$. Rozlišujeme však dve konštrukcie:

  • Všeobecné pripísanie $t\ \mathsf{as}\ T$ má pravidlo T-Ascribe z predchádzajúcej prednášky. Overuje, že $t$ už má typ $T$, a pri vyhodnocovaní sa odstráni.
  • Anotovaná injekcia má anotáciu ako vlastnú súčasť syntaxe. Typ určujú pravidlá pre injekcie uvedené nižšie. Nejde o mechanické použitie T-Ascribe na neanotovanú injekciu.

V pôvodnej gramatike nahradíme neanotované tvary $\mathsf{inl}\ t$, $\mathsf{inr}\ t$ a zodpovedajúce hodnotové formy nasledujúcimi anotovanými tvarmi. Staré a nové tvary teda neponechávame súčasne. Konštrukcia case zostáva bez zmeny:

$$ \begin{aligned} t::={}&\ldots\mid\mathsf{inl}\ t\ \mathsf{as}\ T \mid\mathsf{inr}\ t\ \mathsf{as}\ T,\\ v::={}&\ldots\mid\mathsf{inl}\ v\ \mathsf{as}\ T \mid\mathsf{inr}\ v\ \mathsf{as}\ T. \end{aligned} $$

V gramatike zastupuje $T$ ľubovoľný typ; pravidlá typovania vyžadujú, aby anotácia injekcie bola súčtovým typom. Zápis $\mathsf{inl}\ t\ \mathsf{as}\ (T_1+T_2)$ čítame ako jednu anotovanú injekciu. Zložitý argument zátvorkujeme, napríklad $\mathsf{inl}\;(f\ x)\ \mathsf{as}\ (T_1+T_2)$. Pri aplikácii zátvorkujeme celú injekciu: $g\;(\mathsf{inl}\ v\ \mathsf{as}\ (T_1+T_2))$. Telo abstrakcie a každá vetva case siahajú doprava; vnorený rozbor podľa potreby ohraničíme zátvorkami.

Zavedením explicitných typových anotácií odstránime nejednoznačnosť výsledného typu injekcií. V takto rozšírenom jazyku môžeme opäť zachovať vlastnosť jednoznačnosti typov. Od tejto chvíle používame anotovaný jazyk.

Typovacie pravidlá

$$ \frac{\Gamma\vdash t:T_1} {\Gamma\vdash\mathsf{inl}\ t\ \mathsf{as}\ (T_1+T_2):T_1+T_2} \quad{\scriptstyle(\text{T-Inl})} $$ $$ \frac{\Gamma\vdash t:T_2} {\Gamma\vdash\mathsf{inr}\ t\ \mathsf{as}\ (T_1+T_2):T_1+T_2} \quad{\scriptstyle(\text{T-Inr})} $$ $$ \frac{\Gamma\vdash t_0:T_1+T_2\qquad \Gamma,x_1:T_1\vdash t_1:T\qquad\Gamma,x_2:T_2\vdash t_2:T} {\Gamma\vdash\mathsf{case}\ t_0\ \mathsf{of}\ \mathsf{inl}\ x_1\Rightarrow t_1\mid\mathsf{inr}\ x_2\Rightarrow t_2:T} \quad{\scriptstyle(\text{T-Case})} $$

Pravidlá injekcií overia typ argumentu proti príslušnej zložke anotácie. Typ celej injekcie je presne anotovaný súčet. Pravidlo pre case kontroluje analyzovaný term a obe vetvy v osobitne rozšírených kontextoch. Vetvy musia mať rovnaký výsledný typ $T$. Viazané premenné volíme čerstvé vzhľadom na $\Gamma$, prípadne ich premenujeme.

Vyhodnocovacie pravidlá

Pre stručný zápis nech $\mathit{br}$ označuje nezmenený zoznam vetiev $\mathsf{inl}\ x_1\Rightarrow t_1\mid\mathsf{inr}\ x_2\Rightarrow t_2$. Je to iba metajazyková skratka, nie nový term.

$$ \frac{t\to t'} {\mathsf{inl}\ t\ \mathsf{as}\ T\to\mathsf{inl}\ t'\ \mathsf{as}\ T} \quad{\scriptstyle(\text{E-Inl})} $$ $$ \frac{t\to t'} {\mathsf{inr}\ t\ \mathsf{as}\ T\to\mathsf{inr}\ t'\ \mathsf{as}\ T} \quad{\scriptstyle(\text{E-Inr})} $$ $$ \frac{t\to t'} {\mathsf{case}\ t\ \mathsf{of}\ \mathit{br}\to \mathsf{case}\ t'\ \mathsf{of}\ \mathit{br}} \quad{\scriptstyle(\text{E-Case})} $$ $$ \frac{}{\mathsf{case}\ (\mathsf{inl}\ v\ \mathsf{as}\ T)\ \mathsf{of}\ \mathit{br} \to[x_1\mapsto v]t_1} \quad{\scriptstyle(\text{E-CaseInl})} $$ $$ \frac{}{\mathsf{case}\ (\mathsf{inr}\ v\ \mathsf{as}\ T)\ \mathsf{of}\ \mathit{br} \to[x_2\mapsto v]t_2} \quad{\scriptstyle(\text{E-CaseInr})} $$

Najprv sa vyhodnocuje argument injekcie. Až keď je hodnotou, môže case vybrať príslušnú vetvu a vykonať substitúciu. Anotácia zostáva súčasťou injekčnej hodnoty; neodstraňujeme ju samostatným krokom E-Ascribe. Pravidlá pre case nevyhodnocujú telá vetiev pred ich výberom.

Príklad

Funkcia $\mathsf{getnat}:(\mathsf{Nat}+\mathsf{Bool})\to\mathsf{Nat}$ rozlišuje dve alternatívy vstupu. Pri ľavej injekcii vráti uložené číslo; pri pravej vráti $1$ pre $\mathsf{true}$ a $0$ pre $\mathsf{false}$.

Overte typovú korektnosť a vyhodnoťte aplikáciu

$$\mathsf{getnat}\;(\mathsf{inr}\ \mathsf{true}\ \mathsf{as}\ (\mathsf{Nat}+\mathsf{Bool})), $$

kde

$$ \begin{array}{rll} \mathsf{getnat}=\lambda x:\mathsf{Nat}+\mathsf{Bool}. &\mathsf{case}\ x\ \mathsf{of}&\\ &\mathsf{inl}\ n&\Rightarrow n\\ \mid&\mathsf{inr}\ b&\Rightarrow\mathsf{if}\ b\ \mathsf{then}\ 1\ \mathsf{else}\ 0. \end{array} $$

Riešenie – typová kontrola:

Typová kontrola aplikácie funkcie getnat
Obr. 1: Typová kontrola aplikácie funkcie getnat

Vyhodnotenie. Argument je už hodnotou, preto najprv dosadíme za $x$, potom vyberieme pravú vetvu a dosadíme $\mathsf{true}$ za $b$:

$$ \begin{aligned} &\mathsf{getnat}\;(\mathsf{inr}\ \mathsf{true}\ \mathsf{as}\ (\mathsf{Nat}+\mathsf{Bool}))\\ &\to\mathsf{case}\ (\mathsf{inr}\ \mathsf{true}\ \mathsf{as}\ (\mathsf{Nat}+\mathsf{Bool}))\ \mathsf{of}\\ &\qquad\mathsf{inl}\ n\Rightarrow n \mid\mathsf{inr}\ b\Rightarrow\mathsf{if}\ b\ \mathsf{then}\ 1\ \mathsf{else}\ 0 &&{\scriptstyle(\text{E-AppAbs})}\\ &\to\mathsf{if}\ \mathsf{true}\ \mathsf{then}\ 1\ \mathsf{else}\ 0 &&{\scriptstyle(\text{E-CaseInr})}\\ &\to1&&{\scriptstyle(\text{E-IfTrue})}. \end{aligned} $$

V každom kroku zostáva typ $\mathsf{Nat}$. Anotácia je súčasťou injekčnej hodnoty; pri výbere vetvy sa spotrebuje celá injekcia.

V OCamli použijeme konštruktory Inl, Inr a výraz match:

type nat_or_bool = Inl of int | Inr of bool

let getnat (x : nat_or_bool) : int =
  match x with
  | Inl n -> n
  | Inr b -> if b then 1 else 0
# getnat (Inr true);;
- : int = 1
# getnat (Inr false);;
- : int = 0
# getnat (Inl 5);;
- : int = 5

Typ int nie je doslovne formálny typ $\mathsf{Nat}$: pripúšťa aj záporné čísla a má obmedzený rozsah. Pri tomto príklade ho používame ako praktickú reprezentáciu nezáporných čísel. OCaml navyše poskytuje vlastné pravidlá deklarácií a odvodzovania typov; explicitné as z nášho kalkulu preto neprepisujeme do jeho syntaxe.

Varianty

Binárny súčet zovšeobecníme na súčet $n$ typov, podobne ako sme dvojice zovšeobecnili na $n$-tice a záznamy. Takto získame variantové typy s návestiami. Ich hodnota obsahuje práve jednu z alternatív.

Namiesto $T_1+T_2$ môžeme použiť pomenované alternatívy:

$$[lab_1:T_1,lab_2:T_2].$$

Návestia $lab_1,lab_2$ označujú alternatívy, nie nové typy. Ľavému a pravému vloženiu zodpovedajú konštrukcie

$$ [lab_1=t]\ \mathsf{as}\ [lab_1:T_1,lab_2:T_2], \qquad [lab_2=t]\ \mathsf{as}\ [lab_1:T_1,lab_2:T_2]. $$

Poznámka

Návestia patria do množiny $\mathcal{L}$ zavedenej pri záznamoch. V jednom variantovom type sú navzájom rôzne. Nie sú premennými a substitúcia ich nemení. Ako pri záznamoch zo štvrtej prednášky zachovávame poradie položiek v type; ich ľubovoľné preusporiadanie tu nie je definičnou rovnosťou typov.

Rozdiel medzi záznamom a variantom nie je iba v zátvorkách:

  • Záznam $\langle lab_1=t_1,lab_2=t_2\rangle$ obsahuje obe položky a používa zátvorky $\langle\ \rangle$.
  • Variant $[lab_j=t]\ \mathsf{as}\ T$ obsahuje iba zvolenú položku a používa hranaté zátvorky $[\ ]$.

Syntax jednoducho typovaného $\lambda$-kalkulu rozšírená o variantové typy

Rozšírime tri kategórie: termy $t$, hodnoty $v$ a typy $T$:

$$ \begin{aligned} t::={}&\ldots\mid[lab=t]\ \mathsf{as}\ T\mid\mathsf{case}\ t\ \mathsf{of}\ ([lab_i=x_i]\Rightarrow t_i)^{i\in1..n},\\ v::={}&\ldots\mid[lab=v]\ \mathsf{as}\ T,\\ T::={}&\ldots\mid[lab_i:T_i~^{i\in1..n}]. \end{aligned} $$

Používame $n\ge1$ a $lab_i\in\mathcal{L}$, pričom $lab_i\ne lab_j$ pre $i\ne j$. Horný index $i\in1..n$ znamená zoznam všetkých položiek alebo vetiev, nie umocňovanie. V samotnom terme oddeľujeme vetvy case symbolom $\mid$; pri explicitnom zápise terminálu v BNF ho uvádzame ako $\texttt{'|'}$. Premenná $x_i$ je viazaná iba v príslušnom tele $t_i$.

Príklad

Pre $n=3$ a term $t:[a:T_1,b:T_2,c:T_3]$ sa indexová schéma rozvinie na tri vetvy:

$$ \begin{alignedat}{2} &\mathsf{case}\ t\ \mathsf{of}\\ &\quad\phantom{\mid}\;[a=x]&\;&\Rightarrow t_1\\ &\quad\mathord{\mid}\;[b=y]&\;&\Rightarrow t_2\\ &\quad\mathord{\mid}\;[c=z]&\;&\Rightarrow t_3. \end{alignedat} $$

Ide o konkrétnu inštanciu všeobecnej schémy, nie o nový druh selektora. V príslušných vetvách majú viazané premenné typy $x:T_1$, $y:T_2$ a $z:T_3$; všetky tri telá musia mať rovnaký výsledný typ.

Anotácia je súčasťou variantového konštruktora a určuje celý výsledný variantový typ. Hodnotou je až $[lab=v]\ \mathsf{as}\ T$, teda variant s vyhodnoteným obsahom. Ani tu sa anotácia neodstraňuje pravidlom všeobecného pripísania.

Typovacie pravidlá

Pre stručnosť označme $V=[lab_i:T_i~^{i\in1..n}]$. Ide o metajazykovú skratku za konkrétny variantový typ.

$$ \frac{\Gamma\vdash t:T_j\qquad1\le j\le n} {\Gamma\vdash[lab_j=t]\ \mathsf{as}\ V:V} \quad{\scriptstyle(\text{T-Variant})} $$ $$ \frac{\Gamma\vdash t_0:V\qquad \Gamma,x_i:T_i\vdash t_i:T\quad(\text{pre každé }i\in1..n)} {\Gamma\vdash\mathsf{case}\ t_0\ \mathsf{of}\ ([lab_i=x_i]\Rightarrow t_i)^{i\in1..n}:T} \quad{\scriptstyle(\text{T-Case})} $$

Pravidlá vyžadujú, aby:

  • zvolené návestie patrilo do anotovaného typu a vložený term mal typ príslušnej položky;
  • každé návestie typu $V$ malo práve jednu zodpovedajúcu vetvu case, bez chýbajúcich či dodatočných návestí;
  • v každej vetve mala premenná $x_i$ typ $T_i$ a bola čerstvá vzhľadom na $\Gamma$;
  • všetky telá vetiev mali ten istý výsledný typ $T$.

V indexovom zápise uvádzame vetvy v poradí položiek typu. Jedinečnosť návestí a vyčerpávajúci rozbor zaručujú, že pre každú variantovú hodnotu nájdeme jedinú vetvu. To je podstatné pre progres: správne typovaný uzavretý rozbor sa po vyhodnotení analyzovaného termu nezasekne pre chýbajúci prípad. Zhoda typov vetiev spolu so substitučnou lemou zabezpečuje zachovanie výsledného typu po výbere vetvy.

Vyhodnocovacie pravidlá

$$ \frac{1\le j\le n} {\mathsf{case}\ ([lab_j=v_j]\ \mathsf{as}\ T)\ \mathsf{of}\ ([lab_i=x_i]\Rightarrow t_i)^{i\in1..n}\to[x_j\mapsto v_j]t_j} \quad{\scriptstyle(\text{E-CaseVariant})} $$ $$ \frac{t_0\to t_0'} {\mathsf{case}\ t_0\ \mathsf{of}\ ([lab_i=x_i]\Rightarrow t_i)^{i\in1..n} \to\mathsf{case}\ t_0'\ \mathsf{of}\ ([lab_i=x_i]\Rightarrow t_i)^{i\in1..n}} \quad{\scriptstyle(\text{E-Case})} $$ $$ \frac{t\to t'}{[lab=t]\ \mathsf{as}\ T\to[lab=t']\ \mathsf{as}\ T} \quad{\scriptstyle(\text{E-Variant})} $$

Prvé pravidlo používa $j\in1..n$ a dosadí vnútornú hodnotu $v_j$ za voľné výskyty $x_j$ v tele $t_j$. Druhé pravidlo vyhodnocuje analyzovaný term a tretie obsah variantu. Poradie je teda rovnaké ako pri binárnom súčte: obsah variantu, výber vetvy, substitúcia bez zachytenia, potom vyhodnocovanie vybranej vetvy. Ostatné telá sa nevyhodnocujú.

Príklad

Variantový typ použijeme na reprezentáciu geometrických tvarov: bodu, kruhu a obdĺžnika. Pre tento príklad predpokladáme rozšírenie $\mathcal{B}$ o vstavaný typ $\mathsf{Float}$, jeho číselné hodnoty, porovnávanie a aritmetické operácie. Čísla zapisujeme s desatinnou bodkou. Symbol $\pi_{\mathsf{Float}}$ označuje aproximáciu $\pi$ v tomto type, zodpovedajúcu Float.pi v OCamli.

Typové skratky sú:

$$ \begin{aligned} \mathsf{Suradnica}&=\mathsf{Float}\times\mathsf{Float},\\ \mathsf{Tvar}&=\begin{alignedat}[t]{2} &[\mathsf{bod}&\;&:\mathsf{Suradnica},\\ &\phantom{[}\mathsf{kruh}&\;&:\mathsf{Suradnica}\times\mathsf{Float},\\ &\phantom{[}\mathsf{obdl}&\;&:\mathsf{Suradnica}\times\mathsf{Suradnica}]. \end{alignedat} \end{aligned} $$

Bod nesie súradnice. Kruh nesie dvojicu stredu a nezáporného polomeru. Obdĺžnik je určený dvojicou protiľahlých rohov; obvykle uvádzame dolný ľavý a horný pravý roh. Samotný typ podmienku nezáporného polomeru ani poradie rohov nevynucuje.

Zložky sú vnorené binárne súčiny. Pre $o:\mathsf{Suradnica}\times\mathsf{Suradnica}$ sú $o.1$ a $o.2$ súradnice rohov; $(o.1).1$ je prvá súradnica prvého rohu. Pre $k:\mathsf{Suradnica}\times\mathsf{Float}$ je $k.1$ stred a $k.2$ polomer.

Funkcia pre výpočet obsahu. Najprv definujeme absolútnu hodnotu:

$$ \begin{aligned} \mathsf{abs}&:\mathsf{Float}\to\mathsf{Float},\\ \mathsf{abs}&=\lambda x:\mathsf{Float}.\ \mathsf{if}\ x<0.0\ \mathsf{then}\ 0.0-x\ \mathsf{else}\ x. \end{aligned} $$

Potom definujeme obsah pomocou úplného rozboru troch alternatív:

$$ \begin{aligned} \mathsf{obsah}&:\mathsf{Tvar}\to\mathsf{Float},\\ \mathsf{obsah}&=\lambda x:\mathsf{Tvar}.\mathsf{case}\ x\ \mathsf{of}\\ &\quad\begin{alignedat}[t]{2} &\phantom{\mid}\;[\mathsf{bod}=b]&\;&\Rightarrow0.0\\ &\mathord{\mid}\;[\mathsf{kruh}=k]&\;&\Rightarrow\pi_{\mathsf{Float}}\cdot k.2\cdot k.2\\ &\mathord{\mid}\;[\mathsf{obdl}=o]&\;&\Rightarrow \begin{aligned}[t] &\mathsf{abs}\;((o.1).1-(o.2).1)\\ &\quad\cdot\mathsf{abs}\;((o.1).2-(o.2).2). \end{aligned} \end{alignedat} \end{aligned} $$

Bod má nulový obsah, kruh obsah $\pi r^2$ a obdĺžnik súčin dĺžok strán. Všetky tri vetvy majú typ $\mathsf{Float}$. Absolútne hodnoty pri obdĺžniku umožňujú zadať protiľahlé rohy aj v opačnom poradí.

Funkcia pre výpočet stredu:

$$ \begin{aligned} \mathsf{stred}&:\mathsf{Tvar}\to\mathsf{Suradnica},\\ \mathsf{stred}&=\lambda x:\mathsf{Tvar}.\mathsf{case}\ x\ \mathsf{of}\\ &\quad\begin{alignedat}[t]{2} &\phantom{\mid}\;[\mathsf{bod}=b]&\;&\Rightarrow b\\ &\mathord{\mid}\;[\mathsf{kruh}=k]&\;&\Rightarrow k.1\\ &\mathord{\mid}\;[\mathsf{obdl}=o]&\;&\Rightarrow \left\langle\frac{(o.1).1+(o.2).1}{2.0}, \frac{(o.1).2+(o.2).2}{2.0}\right\rangle. \end{alignedat} \end{aligned} $$

Pre bod vraciame jeho súradnice, pre kruh uložený stred. Stred obdĺžnika získame priemerom zodpovedajúcich súradníc rohov. Každá vetva má typ $\mathsf{Suradnica}$. Znamienko $=$ zavádza definíciu funkcie, zatiaľ čo dvojbodka uvádza jej typ.

OCaml implementácia používa rovnaké alternatívy a vzorce:

type suradnica = float * float
type tvar =
  | Bod of suradnica
  | Kruh of suradnica * float
  | Obdl of suradnica * suradnica

let obsah : tvar -> float = function
  | Bod _ -> 0.0
  | Kruh (_, r) -> Float.pi *. r *. r
  | Obdl ((x1, y1), (x2, y2)) ->
      let w = abs_float (x2 -. x1) in
      let h = abs_float (y2 -. y1) in
      w *. h

let stred : tvar -> suradnica = function
  | Bod p -> p
  | Kruh (p, _) -> p
  | Obdl ((x1, y1), (x2, y2)) ->
      ((x1 +. x2) /. 2.0, (y1 +. y2) /. 2.0)
# obsah (Kruh ((0.0, 0.0), 2.0));;
- : float = 12.5663706143591725
# obsah (Obdl ((0.0, 0.0), (3.0, 4.0)));;
- : float = 12.
# stred (Obdl ((0.0, 0.0), (3.0, 4.0)));;
- : suradnica = (1.5, 2.)

Číselný výstup pri kruhu je aproximácia. V OCamli používame operátory *., -., +. a /. pre float; vo formálnom príklade im zodpovedajú predpokladané operácie na $\mathsf{Float}$. Varianty umožňujú spojiť rôzne reprezentácie tvarov do jedného typu a pre každú alternatívu bezpečne sprístupniť jej údaje.

Špeciálne variantové typy

Varianty umožňujú vyjadriť viacero užitočných spôsobov organizácie údajov:

  • Enumeračné typy vymenúvajú pevne danú množinu alternatív bez ďalšieho údaja.
  • Jednopoložkové varianty obaľujú hodnotu a odlišujú jej význam od vnútornej reprezentácie.
  • Voliteľné hodnoty rozlišujú prítomnosť a neprítomnosť údaja.

V našom kalkule ide o konkrétne jednoduché typy zostavené už zavedenými konštrukciami. Samostatné nové pravidlá pre tieto tri prípady nepotrebujeme.

Enumeračné typy

Enumeračné typy sú variantové typy, kde každá položka má:

  • návestie lab, a
  • typ $\mathsf{Unit}$.

Príklad

Typ reprezentujúci dni v týždni je možné definovať ako enumeračný typ:

$$ \begin{aligned} \mathsf{Week}=[\;&monday:\mathsf{Unit},\\ &tuesday:\mathsf{Unit},\\ &wednesday:\mathsf{Unit},\\ &thursday:\mathsf{Unit},\\ &friday:\mathsf{Unit},\\ &saturday:\mathsf{Unit},\\ &sunday:\mathsf{Unit}\;]. \end{aligned} $$

Napríklad pondelok reprezentuje hodnota:

$$ [ monday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} $$

Keďže typ $\mathsf{Unit}$ má jedinú hodnotu unit, typ $\mathsf{Week}$ má presne 7 hodnôt, zodpovedajúcich dňom v týždni.

Obsahom každej alternatívy je jediná hodnota $\mathsf{unit}$; jednotlivé dni rozlišuje návestie.

K termom enumeračného typu $\mathsf{Week}$ je možné pristupovať prostredníctvom selektora case.

Majme funkciu, ktorá pre argument $ w : \mathsf{Week} $ vráti nasledujúci pracovný deň v týždni:

$$ \begin{aligned} \mathsf{nextWorkDay}&:\mathsf{Week}\to\mathsf{Week},\\ \mathsf{nextWorkDay} &= \lambda w: \mathsf{Week}. \\ &\quad \mathsf{case}~w~\mathsf{of} \\ &\quad\quad ~~~[monday = x] \Rightarrow [tuesday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[tuesday = x] \Rightarrow [wednesday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[wednesday = x] \Rightarrow [thursday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[thursday = x] \Rightarrow [friday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[friday = x] \Rightarrow [monday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[saturday = x] \Rightarrow [monday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \\ &\quad\quad \mid~[sunday = x] \Rightarrow [monday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} \end{aligned} $$

Príklad použitia:

$$ \mathsf{nextWorkDay}\;([saturday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week}) \to^* [monday = \mathsf{unit}]~\mathsf{as}\ \mathsf{Week} $$

V každej vetve má $x$ typ $\mathsf{Unit}$ a nepoužije sa. Rozbor pokrýva všetkých sedem dní a všetky vetvy vracajú $\mathsf{Week}$.

Predchádzajúci príklad v jazyku OCaml:

(* Definícia typu Week *)
type week =
  | Monday
  | Tuesday
  | Wednesday
  | Thursday
  | Friday
  | Saturday
  | Sunday

(* Funkcia, ktorá vracia nasledujúci pracovný deň *)
let next_work_day w =
  match w with
  | Monday -> Tuesday
  | Tuesday -> Wednesday
  | Wednesday -> Thursday
  | Thursday -> Friday
  | Friday -> Monday
  | Saturday -> Monday
  | Sunday -> Monday

OCaml má pre alternatívy bez ďalšieho údaja priamo bezargumentové konštruktory Monday, …, Sunday. Na rozdiel od formálneho modelu preto nepíšeme explicitné unit.

# next_work_day Saturday;;
- : week = Monday

Variantové typy s jednou položkou

Jednopoložkový variant má práve jedno návestie:

$$V=[lab:T].$$

Term $t:T$ doň vložíme ako $[lab=t]\ \mathsf{as}\ V$. Hodnoty majú tvar $[lab=v]\ \mathsf{as}\ V$ s vnútornou hodnotou $v:T$. Aj tu môže byť všeobecným termom typu $V$ premenná či aplikácia, ktorá takú hodnotu ešte len vypočíta.

Obal odlišuje význam údaja od jeho reprezentácie. Operáciu očakávajúcu $T$ nemožno priamo použiť na hodnotu typu $V$; najprv ju rozbalíme selektorom case. Typy $T$ a $[lab:T]$ teda automaticky nestotožňujeme. Jednopoložkový variant navyše nemusí mať jedinú hodnotu: počet možností závisí od vnútorného typu $T$.

Príklad

Pri konverzii medzi dolármi a eurami chceme zabrániť zámene dvoch súm s rovnakou číselnou reprezentáciou. Pre tento príklad predpokladáme ilustračné rozšírenie $\mathcal{B}$ o $\mathsf{Real}$ s číselnými hodnotami a aritmetikou. Operácia

$$\mathsf{times}:\mathsf{Real}\to\mathsf{Real}\to\mathsf{Real}$$

je tu primitívne násobenie reálnych čísel, nie Churchovo násobenie z druhej prednášky. Použijeme pevné ilustračné kurzy $0.8$ a $1.25$, ktoré sú navzájom prevrátené.

Najprv bez obalových typov. Ak obe meny reprezentujeme priamo typom $\mathsf{Real}$, môžeme napísať:

$$ \begin{aligned} \mathsf{dollartoeuro}&:\mathsf{Real}\to\mathsf{Real},\\ \mathsf{dollartoeuro}&=\lambda d:\mathsf{Real}.\mathsf{times}\ d\ 0.8,\\ \mathsf{eurotodollar}&:\mathsf{Real}\to\mathsf{Real},\\ \mathsf{eurotodollar}&=\lambda e:\mathsf{Real}.\mathsf{times}\ e\ 1.25,\\ \mathsf{mybalance}&=39.50. \end{aligned} $$

Po dosadení týchto definícií platí:

$$ \begin{aligned} \mathsf{eurotodollar}\;(\mathsf{dollartoeuro}\ \mathsf{mybalance})&\to^*39.50,\\ \mathsf{dollartoeuro}\;(\mathsf{dollartoeuro}\ \mathsf{mybalance})&\to^*25.28. \end{aligned} $$

Aj druhý výraz je dobre typovaný ako $\mathsf{Real}$, hoci omylom konvertuje už prepočítanú sumu znova tým istým smerom. Samotná číselná reprezentácia nerozlišuje meny.

Bezpečná verzia s obalmi. Predchádzajúce definície nahradíme funkciami pracujúcimi s dvoma rôznymi variantovými typmi:

$$ \begin{aligned} \mathsf{DollarAmount}&=[dollars:\mathsf{Real}],\\ \mathsf{EuroAmount}&=[euros:\mathsf{Real}]. \end{aligned} $$

$\mathsf{Real}$ je vnútorný číselný typ; $\mathsf{DollarAmount}$ a $\mathsf{EuroAmount}$ sú odlišné obalové typy. Ich odlišnosť v našom štrukturálnom modeli vyplýva z rôznych návestí, nie iba z rôznych názvov typových skratiek.

Bezpečné konverzie hodnotu rozbalia, prepočítajú a opäť zabalia do správnej alternatívy:

$$ \begin{aligned} \mathsf{dollartoeuro}&=\lambda d:\mathsf{DollarAmount}.\ \mathsf{case}\ d\ \mathsf{of}\\ &\qquad[dollars=x]\Rightarrow [euros=\mathsf{times}\ x\ 0.8]\ \mathsf{as}\ \mathsf{EuroAmount},\\ \mathsf{eurotodollar}&=\lambda e:\mathsf{EuroAmount}.\ \mathsf{case}\ e\ \mathsf{of}\\ &\qquad[euros=x]\Rightarrow [dollars=\mathsf{times}\ x\ 1.25]\ \mathsf{as}\ \mathsf{DollarAmount}. \end{aligned} $$

Majú typy:

$$ \begin{aligned} \mathsf{dollartoeuro}&:\mathsf{DollarAmount}\to\mathsf{EuroAmount},\\ \mathsf{eurotodollar}&:\mathsf{EuroAmount}\to\mathsf{DollarAmount}. \end{aligned} $$

V oboch rozboroch má $x$ typ $\mathsf{Real}$. Injekcia výsledku určuje príslušný obalový typ. Pre

$$\mathsf{mybalance}=[dollars=39.50]\ \mathsf{as}\ \mathsf{DollarAmount}$$

dostaneme tieto výpočty:

$$ \begin{aligned} \mathsf{dollartoeuro}\ \mathsf{mybalance} &\to^*[euros=31.60]\ \mathsf{as}\ \mathsf{EuroAmount},\\ \mathsf{eurotodollar}\;(\mathsf{dollartoeuro}\ \mathsf{mybalance}) &\to^*[dollars=39.50]\ \mathsf{as}\ \mathsf{DollarAmount}. \end{aligned} $$

Výsledok spätnej konverzie má typ $\mathsf{DollarAmount}$ a zostáva zabalený, nie je neobaleným číslom typu $\mathsf{Real}$. Pri volaní hodnotou sa po výbere vetvy najprv vypočíta násobenie v obsahu variantu; až potom vznikne variantová hodnota.

Výraz

$$\mathsf{dollartoeuro}\;(\mathsf{dollartoeuro}\ \mathsf{mybalance})$$

neprejde typovou kontrolou. Vnútorná aplikácia má typ $\mathsf{EuroAmount}$, ale vonkajšia očakáva $\mathsf{DollarAmount}$, takže nemožno použiť T-App. Typová chyba je neúspech statickej kontroly; nepíšeme ju ako výsledok vyhodnocovacej relácie $\to$.

OCaml implementácia:

type dollar_amount = Dollar of float
type euro_amount = Euro of float

let times x y = x *. y

let dollartoeuro (Dollar d) =
  Euro (times d 0.8)

let eurotodollar (Euro e) =
  Dollar (times e 1.25)

let mybalance = Dollar 39.50
# dollartoeuro mybalance;;
- : euro_amount = Euro 31.6
# eurotodollar (dollartoeuro mybalance);;
- : dollar_amount = Dollar 39.5
# dollartoeuro (dollartoeuro mybalance);;
Error: This expression has type euro_amount
       but an expression was expected of type dollar_amount

OCaml rozlišuje deklarované typy dollar_amount a euro_amount a odmietne rovnakú zámenu. Typ float je iba konečná aproximácia formálneho $\mathsf{Real}$; presnosť finančných výpočtov je samostatná otázka. Obaly tu ilustrujú ochranu pred zámenou mien, nie spôsob reprezentácie presnej peňažnej aritmetiky.

Voliteľné hodnoty

Variantové typy umožňujú reprezentovať voliteľné hodnoty (optional values): údaj môže byť prítomný alebo môže chýbať. Pre každý pevne zvolený typ $T$ môžeme použiť schému

$$\mathsf{Option}(T)=[none:\mathsf{Unit},some:T].$$

Ide o metajazykovú schému, nie o nový polymorfný typový konštruktor vo formálnom jazyku. Každá konkrétna voľba $T$ označuje konkrétny variantový typ. Jeho hodnoty sú:

  • $[none=\mathsf{unit}]\ \mathsf{as}\ \mathsf{Option}(T)$ — neprítomnosť údaja;
  • $[some=v]\ \mathsf{as}\ \mathsf{Option}(T)$ pre $v:T$ — prítomná hodnota.

V ďalšom texte budeme používať konkrétnu typovú skratku

$$\mathsf{OptionNat}=[none:\mathsf{Unit},some:\mathsf{Nat}].$$

Konštrukcie v bežných programovacích jazykoch vyjadrujú podobnú myšlienku, hoci majú vlastné typové systémy:

Jazyk Typ / konštrukcia Príklad použitia
OCaml 'a option Some 5 alebo None
Haskell Maybe a Just 5 alebo Nothing
Rust Option<T> Some(5) alebo None
Java Optional<T> Optional.of(5) alebo Optional.empty()

Príklad

Ak chceme v OCamli verne reprezentovať prirodzené čísla pomocou nuly a nasledovníka, môžeme použiť tieto deklarácie:

type nat = Zero | S of nat
type natoption = None | Some of nat

Typ natoption zodpovedá konkrétnemu $\mathsf{OptionNat}$. Some (S Zero) obsahuje prirodzenú jednotku, None označuje neprítomnosť údaja. Ide o OCaml reprezentáciu; deklaráciou rekurzívneho typu nat nemeníme formálnu syntax nášho kalkulu. V bežnom OCaml programe môžeme namiesto vlastného natoption použiť vstavaný typ nat option.

Čiastočné a úplné funkcie. Matematická čiastočná funkcia $f:\mathbb{N}\rightharpoonup\mathbb{N}$ nemusí mať pre každý vstup definovanú hodnotu. Môžeme ju reprezentovať úplnou matematickou funkciou $\widehat f:\mathbb{N}\to(\mathbb{N}\cup\{\mathsf{none}\})$, kde $\mathsf{none}\notin\mathbb{N}$ je osobitný symbol neprítomnosti. Ak je $f(k)$ definované, položíme $\widehat f(k)=f(k)$; inak $\widehat f(k)=\mathsf{none}$.

V našom formálnom jazyku tejto myšlienke zodpovedá typ $\mathsf{Nat}\to\mathsf{OptionNat}$. Množinu prirodzených čísel $\mathbb{N}$ odlišujeme od syntaktického typu $\mathsf{Nat}$: číslo $n\in\mathbb{N}$ reprezentujeme numerickou hodnotou nášho jazyka. Prítomný výsledok vložíme do $[some=n]\ \mathsf{as}\ \mathsf{OptionNat}$, neprítomnosť vyjadríme hodnotou $[none=\mathsf{unit}]\ \mathsf{as}\ \mathsf{OptionNat}$.

none je riadny výsledok výpočtu, nie chýbajúce vyhodnotenie ani výnimka. Samotné pridanie typu však nevytvorí algoritmus, ktorý rozhodne nedefinovanosť ľubovoľnej čiastočne vypočítateľnej funkcie. V našich príkladoch vieme chýbajúci záznam zistiť a explicitne ho oznámiť.

Pri spracovaní výsledku pomocou case musí rozbor obsahovať obe alternatívy. Typový systém však nenúti každý výsledok rozbaliť: môžeme ho aj priamo vrátiť či odovzdať inej funkcii.

Príklad

Tabuľku s číselnými indexmi môžeme reprezentovať funkciou:

$$\mathsf{Table}=\mathsf{Nat}\to\mathsf{OptionNat}.$$

Je definovaná pre každý index. Výsledok má jednu z foriem

$$ [some=n]\ \mathsf{as}\ \mathsf{OptionNat} \quad\text{alebo}\quad [none=\mathsf{unit}]\ \mathsf{as}\ \mathsf{OptionNat}. $$

Anotácia patrí typu výsledku $\mathsf{OptionNat}$, nie funkčnému typu $\mathsf{Table}$. Z typu $\mathsf{Table}$ nevyplýva konečný počet prítomných záznamov: napríklad $\lambda k:\mathsf{Nat}.[some=k]\ \mathsf{as}\ \mathsf{OptionNat}$ má hodnotu na každom indexe.

Ak chceme modelovať konečnú tabuľku, požadujeme konečnú podporu, teda iba konečne veľa indexov s výsledkom some. Konkrétna tabuľka s jediným záznamom $0\mapsto7$ je:

$$ \begin{aligned} \mathsf{table}&:\mathsf{Table},\\ \mathsf{table}&=\lambda k:\mathsf{Nat}.\ \mathsf{if}\ \mathsf{iszero}\ k\\ &\quad\mathsf{then}\ [some=7]\ \mathsf{as}\ \mathsf{OptionNat}\\ &\quad\mathsf{else}\ [none=\mathsf{unit}]\ \mathsf{as}\ \mathsf{OptionNat}. \end{aligned} $$

Vyhľadávanie je iba aplikáciou tabuľky na index:

$$ \begin{aligned} \mathsf{lookup}&:\mathsf{Nat}\to\mathsf{Table}\to\mathsf{OptionNat},\\ \mathsf{lookup}&=\lambda k:\mathsf{Nat}.\lambda t:\mathsf{Table}.t\ k. \end{aligned} $$

V tele platí podľa T-App:

$$k:\mathsf{Nat},t:\mathsf{Nat}\to\mathsf{OptionNat}\vdash t\ k:\mathsf{OptionNat}.$$

Dve použitia T-Abs dajú uvedený typ. Po rozvinutí skratiek a vyhodnotení podmienky dostaneme:

$$ \begin{aligned} \mathsf{lookup}\ 0\ \mathsf{table} &\to^*[some=7]\ \mathsf{as}\ \mathsf{OptionNat},\\ \mathsf{lookup}\ 1\ \mathsf{table} &\to^*[none=\mathsf{unit}]\ \mathsf{as}\ \mathsf{OptionNat}. \end{aligned} $$

Prítomnosť údaja môžeme rozlíšiť pomocou case; tu pri chýbajúcom zázname zvolíme náhradnú hodnotu $0$:

$$ \begin{aligned} &\mathsf{case}\ (\mathsf{lookup}\ 0\ \mathsf{table})\ \mathsf{of}\\ &\qquad[none=u]\Rightarrow0\mid[some=n]\Rightarrow n\\ &\to^*\mathsf{case}\ ([some=7]\ \mathsf{as}\ \mathsf{OptionNat})\ \mathsf{of}\\ &\qquad[none=u]\Rightarrow0\mid[some=n]\Rightarrow n\\ &\to7. \end{aligned} $$

Pre index $1$ analogicky vyberieme vetvu none a dostaneme $0$. V každom rozbore má $u$ typ $\mathsf{Unit}$, $n$ typ $\mathsf{Nat}$ a obe telá typ $\mathsf{Nat}$.

V nasledujúcej OCaml verzii použijeme pre stručnosť int a vstavaný int option. Indexy a uložené čísla budeme používať iba nezáporné; obmedzenie na prirodzené čísla samotný int nevynucuje.

type table = int -> int option

let table : table = fun k ->
  if k = 0 then Some 7 else None

let lookup (k : int) (t : table) : int option = t k

let value_or_zero (result : int option) : int =
  match result with
  | None -> 0
  | Some n -> n
# lookup 0 table;;
- : int option = Some 7
# lookup 1 table;;
- : int option = None
# value_or_zero (lookup 0 table);;
- : int = 7
# value_or_zero (lookup 1 table);;
- : int = 0

Výsledok Some 0 by bol prítomným záznamom s hodnotou nula; None je chýbajúci záznam. Až funkcia value_or_zero ich zámerne prevedie na rovnakú náhradnú číselnú hodnotu. Samotný typ option tieto prípady rozlišuje.

Zhrnutie

Súčin spája viac údajov naraz, súčet označuje jednu z alternatív. Injekcie sú konštruktory termov a selektor case údaje spracuje rozborom prípadov. Explicitná anotácia určuje celý výsledný súčtový typ, čím odstraňuje nejednoznačnosť neanotovaných injekcií. Pri volaní hodnotou sa pred výberom vetvy vyhodnotí aj obsah injekcie.

Variantové typy zovšeobecňujú binárny súčet na pomenované alternatívy. Vyčerpávajúci rozbor s jednotným výsledným typom umožňuje ich bezpečné spracovanie. Alternatívy s $\mathsf{Unit}$ tvoria enumeračné typy; jednopoložkové varianty odlišujú význam hodnôt s rovnakou reprezentáciou. Typ $\mathsf{OptionNat}$ pridáva explicitnú neprítomnosť údaja a umožňuje modelovať tabuľku ako úplnú funkciu vracajúcu voliteľný výsledok.

Zdroje

  1. Pierce, Benjamin C.: Types and Programming Languages. MIT Press, Cambridge, MA, 2002. ISBN 978-0-262-16209-8. Oddiely 11.9 Sums a 11.10 Variants, s. 132–141. Ruský preklad: Бенджамин Пирс: Типы в языках программирования. КДУ, Добросвет, 2012. ISBN 978-5-7913-0082-9.
  2. Sampson, Adrian: CS 4110/5110: Programming Languages and Logics. Cornell University, jar 2024. Prednáškové materiály.
  3. Pfenning, Frank: 15-814: Types and Programming Languages. Carnegie Mellon University, jeseň 2021. Prednáškové materiály.
  4. Pierce, Benjamin C. a kol.: Software Foundations: More on the Simply Typed Lambda-Calculus. Časť Sums: anotácie, typovanie a vyhodnocovanie binárnych súčtov.
  5. The OCaml Manual, verzia 5.4: Type and exception definitions. Deklarácie variantových typov a ich konštruktorov.