Syntaktická analýza ve funkcionálním programování

Motivační úvod

Syntaktická analýza (někdy též parsing) je proces, při němž řetězec (ASCII) znaků převádíme na datovou strukturu vhodnou k dalšímu programovému zpracování. Například máme-li program pro aritmetické výpočty používající datovou strukturu Vyraz:

data Vyraz = Cislo Int | Soucet Vyraz Vyraz | Soucin Vyraz Vyraz

chtěli bychom, syntaktickou analýzou ze vstupu:

7*3+1

odvodit výraz:

Soucet (Soucin (Cislo 7) (Cislo 3)) (Cislo 1)

Syntaktická analýza obecně není jednoduchý problém. Vše záleží na definici toho, co je jazykem vstupních výrazů určených k analýze. Tento jazyk se dá vyjádřit třeba gramatikou. Např. jazyk vstupních výrazů z našeho příkladu by mohl být zadán takto:

V	::= cislo | V + V | V * V

Slovům z gramatiky začínajícím velkým písmenem říkáme neterminály, ostatním terminály, za chvíli uvidíme proč. Zde máme jen jeden neterminál (V) a tři terminály (cislo, + a *). Význam zápisu gramatikou je intuitivní: vstupní výraz musí být možné odvodit pomocí přepisovacích pravidel z tzv. počátečního neterminálu (zde je jasné, že jediný, a tudíž i počatéční je V). Pravidla mají vlevo přepisovaný neterminál, pak oddělovač (zde ::=) a za ním řetězec terminálů a neterminálů, na který je možné původní neterminál přepsat. Ve výše uvedené gramatice jsou ve skutečnosti pravidla tři, avšak ve zkrácené formě, kde levá strana je pro všechny stejná a pravé strany jsou odděleny znaky |.

Řetězec obsahující terminály nebo neterminály se nazývá větná forma. Obsahuje-li pouze terminály, říká se mu věta a nelze z něj již podle pravidel gramatiky nic odvodit, proces odvozování tedy vždy větou končí, cizím slovem terminuje -- proto se užívá výrazu terminály.

Zkusme si nyní odvodit podle naší gramatiky výraz 7*3+1. Začneme s počátečním terminálem:

V

Vidíme, že výraz lze rozdělit na 7*3 + 1:

V ⇒ V + V

Úprava levého výrazu dále pokračuje rozdělením na 7 * 3:

VV + V ⇒ V * V + V

Nakonec se ve třech krocích přepíší neterminály na terminály. Všimněte si, že pro strukturu výrazu nejsou konkrétní hodnoty čísel v něm důležité, proto máme pro všechna čísla jediný neterminál.

VV + V ⇒ V * V + V ⇒ V * V + cislo ⇒
V * cislo + cislo ⇒ cislo * cislo + cislo

Takto jsme ověřili, že vstupní řetězec má správný tvar a měl by určovat strukturu typu Vyraz. Budování této struktury ale znamená právě hledání správného pořadí pravidel k odvození vstupního řetězce z gramatiky. Schválně si ještě jednou porovnejte definici datového typu Vyraz a jednořádkový zápis všech tří pravidel naší gramatiky. Zkuste se zamyslet, jak souvisí výraz

Soucet (Soucin (Cislo 7) (Cislo 3)) (Cislo 1)

s výše uvedeným odvozením. Označíme-li pravidla po řadě jako c, + a *, pak strom odvození vypadá takto:

strom odvození

a strom reprezentující výraz

Soucet (Soucin (Cislo 7) (Cislo 3)) (Cislo 1)

je tento:

strom výrazu typu Vyraz

Nyní je již vidět, že pořadí, ve kterém se použijí pravidla gramatiky, je důležité. Všimněte si, že při odvozování věty 7*3+1 jsme mohli prohodit pořadí použití pravidel + a *. Takové odvození by ovšem odpovídalo výrazu

Soucin (Cislo 7) (Soucet (Cislo 3) (Cislo 1))

který se od původního zřejmě liší. Navíc intuitivní význam prvního výrazu je (7*3)+1, zatímco druhého 7*(3+1) a pouze první odpovídá běžnému vnímání priority operátorů v původním 7*3+1.

Vidíme, že gramatika není jednoznačná, je ji tedy třeba upravit. To se dá zařídit buď výslovným stanovením priority operátorů, nebo elegantněji za užití stejných prostředků jako dosud -- pravidel gramatiky. Například takto:

V	::= V + V | T
T	::= cislo | T * T | (V)

Nyní již není možné provést odvození neodpovídající prioritě operátorů + a *, ale gramatika pořád není jednoznačná. Například výraz 1+2+3 v ní lze odvodit způsobem odpovídajícím (1+2)+3 nebo 1+(2+3). Vzhledem k asociativitě operátoru sčítání je to vzhledem k běžnému významu znaku + jedno, ale už pro zjednoznačnění odvozování můžeme gramatiku dále upravit takto:

V	::= V + T | T
T	::= T * F | F
F	::= cislo | (V)

což odpovídá asociování zleva, tj. odvození výrazu 1+2+3 bude odpovídat (1+2)+3.

Zbývá otázka, jak takové odvozování a vytváření výrazu odpovídajícího stromu odvození automatizovat. Pro určité typy gramatik to je možné, a to docela efektivně, jak uvidíme v následujícím textu a odkazované literatuře. Navíc nám funkcionálním programování nabízí obzvláště intuitivní způsob, jak takové odvozování naprogramovat.

Syntaktická analýza bezkontextových gramatik

V učebním textu k formálům se dozvíte, co jsou to bezkontextové gramatiky. Intuitivně jsou to gramatiky, ve kterých když můžete přepsat nějaký neterminál X na větnou formu α, tak tak můžete učinit nezávisle na okolí X (tj. kontextu). Dozvíte se také, jak se dá jednoduše syntaktická analýza CF jazyků (jazyků zadaných bexkontextovými, Context Free gramatikami) realizovat nedeterministickým zásobníkovým automatem (a zároveň se tam dozvíte i co je to zásobníkový automat :) ). Spíše až v navazujícím kurzu IA006 Vybrané kapitoly z teorie automatů se pak dozvíte, jakými algoritmy (stále vystavěnými na modelu zásobníkových automatů, ale deterministických) lze provádět analýzu gramatik s jednoznačným odvozením (u nejednoznačných se situace řeší přidáním kritérií pro výběr z možných voleb). Kromě toho se dozvíte, jak psát gramatiky tak, aby algoritmy pro jejich odvozování mohly být co nejjednodušší, neboť i mezi třídou bezkontextových gramatik lze vyčlenit podtřídy gramatik pro něž známe snazší postup syntaktické analýzy než pro ostatní. Častý postup totiž je navrhnout gramatiku tak, aby jí rozuměl člověk, a pak ji prohnat úpravami tak, aby definovala stále stejný jazyk, ale přitom šly věty podle ní odvozené lehce algoritmicky rozeznávat.

Pro jazyky C a C++ bylo generování syntaktických analyzátorů (parserů) bezkontextových gramatik a jejich podtříd zautomatizováno např. v podobě nástojů Yacc a Bison. Pro Haskell je podobným nástrojem Happy. V jeho dokumentaci najdete popis, jak jej používat i s jednoduchými příklady.

Poněkud specifičtějším přístupem pro funkcionální programování je však použití tzv. monadických parserů. Ty umožňují funkcionální popis gramatiky, který se zároveň stane jejím parserem -- kouzlo funkcionálního programování, jemuž podobné byste ve světě imperativního programování těžko hledali. Více se o tom dozvíte v článku Grahama Huttona a Erika Meijera