Typy chyb v programech v Prologu
Jak potvrdí asi každý, kdo se programováním zabýval delší dobu, většina programovacích jazyků si je hodně podobná. Existují sice odlišnosti v tom, jaké používají závorky, čím oddělují příkazy, jestli (jak) podporují objektové programování nebo jak přistupují ke správě paměti. To jsou ale jen kosmetické záležitosti a drobná vylepšení klasického “imperativního” stylu výpočtu, kdy počítač provádí jeden příkaz (instrukci) za druhým tak, jak je programátor zapsal.
To ale není jediný možný přístup k tvorbě programů. Teorie (a koneckonců i praxe) říkají, že stejné výpočetní síly jde dosáhnout také pomocí vhodně složené sady logických formulí (“Logické programování”). Nabízí se otázka, jestli – a jak – způsob programování ovlivní, jaké chyby se v programu mohou vyskytnout. Logické programování ale je spíš koncept výpočtu než určitý programovací jazyk, proto se ve zbytku textu omezím na vyprávění o jazyku Prolog, který z tohoto konceptu vychází.
Pravda ale také je, že největší inspirace k sepsání tohoto textu je cvičení z Neprocedurálního programování, které vedu, a kde se začíná právě Prologem. Seznam typů chyb tak je z větší částí soupis chyb, na které jsem narazil během opravování domácích úkolů. Některé z nich jsem čekal, jiné (hlavně ty z první kategorie) mě dost překvapily.
Překlepy, syntaktické chyby a chyby při překladu
K této kategorii chyb není moc co říkat, tady se Prolog v ničem neliší od imperativních programovacích jazyků. Prolog má, stejně jako jiné programovací jazyky, určitou syntaxi, kterou není složité porušit. Asi největší problém je zvyknout si na to, že velké/malé první písmeno identifikátoru rozhoduje o jeho typu (proměnná/konstanta). Na druhou stranu, překladač na takové chyby upozorní, takže jejich oprava je triviální.
To samé platí pro překlepy v názvech predikátů. Prolog sice není staticky kompilovaný jazyk a za běhu jde přidávat nebo odebírat pravidla a fakty (program se tak může sám modifikovat), ale pokud váš program obsahuje dotazy na neexistující predikáty, tak překladač vypisuje varování. Všechna varování sice jde vypnout, ale pokud si pod sebou někdo cíleně řeže větev…
Překlep v názvu proměnné je o trochu horší. Proměnné v Prologu jsou deklarované prostě tím, že jsou někde v programu zapsané. Situaci trochu zachraňují varování překladače v případě, že je některá z proměnných použitá jen jednou, ale ani to nemusí stačit. Nepříjemné je i to, že styl programování v Prologu vede k velkému množství pomocných proměnných a kde je hodně proměnných, tam rychle vzniká chaos.
Logické chyby
V případě logických chyb opět nejde o nic, co by v jiných jazycích nebylo. Špatný – nebo špatně zapsaný algoritmus, konstanta sem, konstanta tam, špatně zapsaná podmínka… prostě nic, co by v jiných jazycích nebylo. A stejně jako v jiných jazycích, ani v Prologu překladač s hledáním logických chyb moc nepomůže. Koneckonců, jak říká teorie, překladač může odhalit zjevné chyby, ale jinak toho moc nezmůže. A stejně jako typy chyb, i způsob boje proti nim je zcela tradiční.
Kapitola sama pro sebe jsou nekonečné cykly. I v programu, který je jinak úplně správně, není problém vyrobit nekonečný cyklus vhodným přehozením podmínek. V logickém programování jako výpočetním modelu se při vždy (nedeterministicky) vybere správný postup výpočtu. V praxi to je trochu problém, protože orákulum pro správný nedeterministický výběr stále ještě nepatří k mezi běžně dostupný hardware. Pořadí, v jakém Prolog provádí vyhodnocování proto je pevně dané a tak stačí nešťastné pořadí predikátů…
Chyby způsobené nejednoznačností
Třetí typ chyb by sice správně měl patřit pod “Logické chyby”, ale protože jde o záležitos specifickou pro Prolog a jeho způsob výpočtu, nechám je samostatně. Koneckonců, jde o článek o Prologu, ne?
Z nedeterministické povahy výpočtu v logickém programování vyplývá také další typ chyby. Volbu “správného” směru výpočtu nejde (na současných počítačích) z principu implementovat přímo, proto je v Prologu nahrazena backtrackingem. Pokud v některém kroce výpočet nemůže pokračovat dál, není to žádný problém, Prolog se vrátí o krok zpět a (opět podle deterministických pravidel) zkusí jinou větev výpočtu. Nejlépe je toto chování vidět přímo v příkazovém řádku interpretu Prologu – po nalezení odpovědi na dotaz Prolog čeká na vaši reakci a pokud odpověď zamítnete (tj. řeknete mu, že výpočet selhal), zkusí backtrackovat, aby našel další možné odpovědi.
To samo o sobě ještě není žádný problém. Naopak jde o základní vlastnost, která dělá Prolog Prologem. Problém je, že programátorům, kteří s Prologem začínají (ale nejen jim), se často stane, že na tuto vlastnost při psaní kódu a při jeho testování zapomenou. Často tak vznikne predikát, který sice dá správnou první odpověď, ale pokud ho přinutíte backtrackovat, budete se divit. Situace, které mohou nastat jsou různé. Začínají neškodným zopakování vrácené hodnoty. To sice zdrží výpočet, ale jinak žádné škody nenapáchá. Horší je, pokud takový predikát vrací správnou hodnotu nekonečně krát, nebo při backtrackování vrací nesprávné hodnoty – to už efektivně znamená buď zacyklený program, nebo celkově chybný výpočet.
V zásadě jsou dvě možnosti, jak se těmto chybám bránit. První z nich je dopsat vstupní podmínky ke každé variantě predikátu, aby pro každý vstup bylo jednoznačně určeno, ke kterém predikátu patří. Druhá možnost je vhodně umístěný operátor řezu. Jaká varianta je lepší záleží na konkrétní situaci – operátor řezu může vést k efektivnějšímu programu, na druhou stranu se tím ztrácí přehlednost a ještě víc se zvedá význam správného uspořádání pravidel. Může se stát, že nejednoznačnost v určité úloze nevadí, protože na backtracking nikdy nedojde. Ale i v takovém případě je dobré na možné problémy s nejednoznačností myslet, protože tyhle okamžiky “nikdy” nastávají až nečekaně často.
Na závěr jen krátký (učebnicový) příklad, jak nevinně může taková chyba vypada.
removeAll(_, [], []).
removeAll(X, [X|Xs], Ys) :- removeAll(X, Xs, Ys).
removeAll(X, [Y|Xs], [Y|Ys]) :- removeAll(X, Xs, Ys).
Oprava by mohla vypadat například takto:
removeAll(_, [], []).
removeAll(X, [X|Ys], Ys) :- removeAll(X, Xs, Ys).
removeAll(X, [Y|Xs], [Y|Ys]) :- X \= Y, removeAll(X, Xs, Ys).
nebo s použitím operátoru řezu takto:
removeAll(_, [], []).
removeAll(X, [X|Xs], Ys) :- !, removeAll(X, Xs, Ys).
removeAll(X, [Y|Xs], [Y|Ys]) :- removeAll(X, Xs, Ys).