Hlavní navigace

Mercury: Programování s pomocí instalatéra

26. 2. 2004
Doba čtení: 9 minut

Sdílet

Existují programovací jazyky, kde platí: co je kompilovatelné, to nemůže havarovat. Debugger jsem nepoužil už dva roky. Takový zázrak můžete prožít, jen pokud vaše programátorské kroky prověří pečlivý instalatér. Žádná roura ve vašem programu nesmí vycházet odnikud (tj. SEGFAULT) a žádná nesmí téct (tj. memory leak). Jazyk Mercury, který se vám v krátké sérii článků pokusím představit, má i další přednosti.

Mercury zatím není na krátký běh

Na úvod „nevýhody“. Jazyk Mercury má v současné době podle mých informací jedinou implementaci dostupnou rozhodně pro Linux, zřejmě pro Windows (nemám, nepoužívám) a odnedávna pro MacOS X. To je v přímé souvislosti s malou publicitou věnovanou tomuto jazyku. Nový příchozí není tedy zachycen měkkým polštářem uživatelské komunity, ale musí své otázky klást přímo velkým bohům. Na druhou stranu tvůrci Mercury již deset let na tomhle jazyce čile pracují, a vzhledem ke geografické poloze Univerzity v Melbourne lze skutečně slíbit odpovědi přes noc.

Nevýhoda malé uživatelské komunity se projevuje i v poměrně malé knihovně funkcí. Nemůžete očekávat to, co s sebou přináší např. Java. Jako protiváhu lze uvést skutečnost, že kompilátor Mercury překládá přímo do běžného C (o jehož další překlad se stará standardní gcc či jiný kompilátor), takže není problém propojovat kód v Mercury s libovolnými knihovnami v C/C++. A pokud bych mluvil nejen o finálních verzích, ale i o alpha a beta vydáních, umožňuje Mercury přímý překlad do .NET a překlad do Javy je ve vývoji. Propojení pak bude přímé a bez vlastnoručního kutění.

S ohledem na popsaný současný stav je Mercury stále ještě programovacím jazykem, který vítězí až na mírně delší běh. Pokud potřebujete ubastlit řešení nějakého problému, rychle jej odevzdat a nikdy se k němu nevracet, asi po Mercury nesáhnete. I když ušetřit hodiny ladění perlového skriptu také nemusí být k zahození…

Co je kompilovatelné, nehavaruje

Mercury je jazyk, v němž není možné napsat havarující program. Samosebou je možné napsat chybný program, který provede něco jiného, než jste si přáli, stejně jako je možné napsat program, který vyčerpá všechny systémové zdroje a je na základě toho násilně ukončen. Jiný důvod pádu není možný.

Naprostá nemožnost pádu je zaručena bezchybně propojenými „rourami“, tj. způsobem práce s proměnnými. Na rozdíl od běžných (procedurálních) jazyků, kde programem popisujete, co se má udělat napřed a co se má udělat potom, v deklarativních jazycích (jako je Prolog, LISP… a také Mercury) říkáte, jak se má co na co změnit. Pořadí dílčích operací určuje až kompilátor, a to tak, aby napřed bylo vyrobeno to, co se má popsaným způsobem zpracovat.

Pokud budete chtít zpracovat něco, co nebylo nikdy vyrobeno – například přičíst jedničku k „nealokované“ proměnné – kompilátor to pozná a takový chybný program vůbec nepustí do světa. Podobně, pokud budete chtít nakládat s něčím nedovoleným způsobem – například přičíst jedničku ke slovu „osel“ – kompilátor to pozná a přinutí vás, abyste jasně řekli, co si pod tím představujete. Mohli jste například chtít za slovo osel připojit znak „1“, což se samosebou smí, ale musí se to tak vyjádřit. (Tahle vlastnost jazyka se nazývá „statická typovanost“; o všech proměnných je staticky, v době kompilace, známo, jakých hodnot mohou nabývat, tj. jakého jsou typu.)

Podobně, pokud budete tvrdit, že nějaká funkce vždy vrátí platný výsledek, kompilátor to prověří a případně ukáže prstem na místo, kde mu omylem či záměrně lžete. Například vaše funkce volá jinou funkci, která může selhat. Nebo je ve vašem kódu uvedena podmínka, která nemusí být za všech okolností splněna.

V čistém jazyce, jakým je např. právě Mercury, píšete holý popis algoritmu. Nemusíte (vlastně nesmíte) řešit alokaci paměti, a navíc za vás kompilátor zkontroluje, že váš popis algoritmu nemá nikde trhlinku ani vnitřní nesrovnalost.

Mezi matematickou logikou a reálným světem

Prvním příkladem musí být program, který vypíše text „Ahoj, světe.“ Jak ale vlastně Mercury řeší tradiční rozpor mezi reálným světem (stavem vašeho systému, volnou pamětí, barvou pracovní plochy a tisíci dalšími proměnlivými okolnostmi) a čistou matematickou logikou popsaných algoritmů? Šalamounsky, opět dobře propojenými rourami.

:- pred main(io__state::di, io__state::uo) is det.
main(StavSvetaPred, StavSvetaPo)
:- io__write_string("Ahoj, světe.", StavSvetaPred, StavSvetaPo). 

(Pokud vám syntaxe připomíná Prolog, nemýlíte se. U pouhé syntaktické podobnosti to ale také zůstane.)

Spustitelný program musí obsahovat „predikát“ (rozuměj proceduru) s názvem „main“, který má dva parametry, jeden vstupní a jeden výstupní. Prvním parametrem do predikátu „přiteče“ celý váš svět (uloží se do proměnné StavSvetaPred), druhým parametrem po skončení vašeho predikátu celý svět zase odteče (StavSvetaPo). Světy před a po samozřejmě nejsou identické (to by nebylo třeba vůbec do vašeho predikátu vstupovat a kompilátor by ze spořivosti takový predikát opravdu vůbec nezavolal). Jaký je tedy vztah mezi světem před a světem po? No svět po má přece obsahovat navíc hlášku „Ahoj, světe“ vypsanou na konzoli. A o to se postará knihovní predikát io__write_string. Jak se dočtete v dokumentaci, má tento predikát tři parametry:

:- pred io__write_string(string, io__state, io__state).
:- mode io__write_string(in, di, uo) is det. 

Prvním parametrem je řetězec (tj. nějaká hodnota typu string), druhým a třetím parametrem je stav světa (tj. hodnota typu io__state). Nedílnou součástí popisu vnějších vlastností predikátu je ovšem tzv. mód, tj. způsob použití: první parametr musí být vstupní (in), druhý parametr musí být vstupní nenávratně zničitelný (di, destroyable input) a třetí musí být unikátní výstupní (uo, unique output).

Obyčejný vstupní parametr vás asi nepolekal, ale co je to nenávratně zničitelný vstupní parametr? Přesně, jak je řečeno, je to hodnota, kterou jednou vložíte do predikátu a už nikdy k ní nebudete mít přístup. Nezapomeňte, že v naší proměnné StavSvetaPred je opravdu uložen úplný popis světa. Těžko můžete chtít po autorech knihovní funkce io__write_string, aby vám dovolili vytisknout něco na konzoli (na papír, vytetovat na rameno), a přesto vám nechali možnost vrátit se ke světu bez tetování. Když vložíte celý tenhle svět do tetovacího stroje, nedostanete nikdy původní svět zpět. Dostanete ovšem (unikátní) popis nového světa, světa s vytetovaným pozdravem. (A dostanete ho v proměnné StavSvetaPo.)

Popisy módů, determinismus

Bdělý čtenář mohl vytušit, že naše deklarace predikátu main/2 (tj. predikátu o dvou parametrech) byla zhuštěná a popisovala současně typy parametrů a „směr“ použití, tj. mód. Ekvivalentně bylo možné napsat:

:- pred main(io__state, io__state).
:- mode main(di, uo) is det.

Povšimněte si nyní slůvek „is det“. Ta říkají, že v uvedeném případě použití se predikát chová deterministicky. Tzn. že vždy uspěje (nemůže selhat, nemůže nevrátit žádné řešení) a vždy vrátí právě jedno řešení.

Podívejme se na další možnosti chování predikátu: například knihovní predikát string__append/3, jehož prvním parametrem je první polovina řetězce, druhým parametrem je druhá polovina a třetím parametrem je spojení první a druhé poloviny, má několik módů použití:

:- pred string__append(string, string, string).
:- mode string__append(in, in, in) is semidet.
:- mode string__append(in, in, out) is det.
:- mode string__append(out, out, in) is multi.

Pokud na vstupu zaplníte všechny tři parametry, predikát ověří, že třetí parametr je skutečně shodný s tím, když vezmete první dva a spojíte je za sebe. Tento způsob použití je tedy „semideterminis­tický“, protože predikát může selhat a tím vám oznámit, že hodnota třetího parametru nevznikla spojením hodnot prvních dvou.

Pokud zadáte první dva parametry, predikát je spojí a vrátí výstup. To je možné udělat právě jedním způsobem a nelze při tom neuspět, takže je tato varianta deterministická.

Pokud zadáte jen třetí parametr, predikát zadaný řetězec rozdělí na dvě části, první vrátí v prvním parametru a druhou ve druhém parametru. Rozdělení např. slova „osel“ je ovšem možné provést více způsoby (""-„osel“, „o“-„sel“, „os“-„el“, „ose“-„l“ a „osel“-""), takže predikát postupně nabídne více řešení (multi). Predikát však nemůže neuspět, protože i prázdný řetězec lze získat zřetězením dvou prázdných řetězců, takže predikát vždy alespoň jedno řešení najde. To je rozdíl od plně nedeterministického módu, kdy nemůžete s jistotou ani říct, že alespoň jedno řešení najdete. Jako příklad plně nedeterministického módu lze uvést:

:- pred list__member(T, list(T)).
:- mode list__member(in, in) is semidet.
:- mode list__member(out, in) is nondet.

Predikát list__member má dva parametry: prvním je proměnná nějakého, naprosto libovolného, typu T a druhým je proměnná typu seznam proměnných typu T. (Jediná implementace predikátu pro hledání výskytu prvku v seznamu je tedy použitelná pro seznamy čísel, seznamy řetězců, seznamy matic, seznamy jaderných reaktorů. Kompilátor však nedovolí nechat hledat číslo v seznamu řetězců nebo jaderný reaktor v seznamu matic. Více o typovém polymorfismu najdete v příštím dílu.)

Deterministické použití nás nepřekvapí: zadáme-li hledaný prvek a seznam, predikát uspěje, pokud hledaný prvek v seznamu je, a selže, pokud není.

Nedeterministické použití je také jasné: zadáme seznam a budeme se ptát na různé možné „hledané“ prvky. Predikát bude postupně vracet více různých prvků. Predikát může ovšem také selhat, pokud mu zadáme prázdný seznam. Neexistuje prvek, který by byl členem prázdného seznamu, proto predikát není „multi“, ale „nondet“.

Kompilátor ukáže prstem

Po předchozím vysvětlení je vám jasné, proč je tenhle program nesprávný:

:- pred main(io::di, io::uo) is det.
main(VstupniSvet, VystupniSvet)
:- list__member(A, [1,2,3]), io__write_string("V seznamu je prvek: ", VstupniSvet, PracovniSvet), io__write(A, PracovniSvet, VystupniSvet). 

V implementaci predikátu main je volán predikát member, který ze zadaného seznamu vrací jeho členy. Tenhle predikát však může selhat (kdybychom náhodou zadali prázdný seznam), nebo může uspět vícekrát (mohli jsme zadat seznam víceprvkový, což jsme také udělali).

Chybný program by mohl havarovat, protože predikát io__write by chtěl vypsat hodnotu proměnné A, ovšem z prázdného seznamu by nemohla tahle proměnná být žádnou hodnotou naplněna. A naopak: tvrdíme, že náš predikát main je deterministický, tj. že vrací právě jedno řešení. Z víceprvkového seznamu však mohly být vybrány různé prvky, a tak by bylo teoreticky možné vrátit tři různé světy: v jednom jsme na konzoli nechali vypsat 1, ve druhém 2 a ve třetím 3.

Z těchto dvou důvodů kompilátor program odmítne:

 hello.m:001: In `main(di, uo)':
 hello.m:001: error: determinism declaration not satisfied.
 hello.m:001: Declared `det', inferred `nondet'.
 hello.m:004: call to `member(out, in)' can fail.
 hello.m:004: call to `member(out, in)' can succeed more than once. 

Čísla řádků vám přesně řeknou, kde jste se zmýlili.

Cukřík závěrem

Aby vás nutnost řetězit stavy světa neodradila, hned vám prozradím syntaktický cukřík jazyka Mercury. Místo dvojice proměnných VstupniSvet, VystupniSvet (a zvlášť v případech, kde potřebujete řetězit více stavů v průběhu výpočtu VstupniSvet->PracovniSvet->VystupniSvet) je možné použít zvláštní syntaxi tzv. „stavových proměnných“ (state variable):

main(!Svet)
:- io__write_string("Ahoj, ", !Svet), io__write_string("světe.", !Svet). 

„Proměnná“ !Svet jsou v původním smyslu proměnné dvě, jedna vstupní a jedna výstupní. A jsou propojeny tak, jak za sebou následují řádky kódu. (Povšimněte si, že v předešlých příkladech se proházením řádek kódu nic nezmění: aby mohl predikát vrátit VystupniSvet, musí napřed vyrobit PracovniSvet, a než má k dispozici PracovniSvet, musí napřed zpracovat VstupniSvet, bez ohledu na to, jestli napíšete napřed řádku, jak se z pracovního světa dělá výstupní, nebo jak se ze vstupního dělá pracovní. Skutečně kompilátor vaše řádky prohází tak, jak je potřeba pro daný mód predikátu; pro každý mód bude pravděpodobně pořadí jiné.)

root_podpora

Příště se podíváme na systém modulů a slibovaný typový polymorfismus.

A za domácí úkol můžete promyslet otázku, jestli, když jednou do proměnné přiřadíte nějakou hodnotu, můžete tu hodnotu ještě změnit.

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