Vlastnosti operátorové notace
Už jsme si zvykli na to, že při definicích parserů lze používat
namísto standardního zápisu predikátů notaci operátorovou. Stejně jako
u všech ostatních námi definovaných operátorů, i v případě operátoru
:->
bylo hlavní motivací pro jeho zavedení umožnit
pohodlnější a přehlednější zapisování parserů. Operátorová notace
tohoto predikátu nám však navíc umožní práci parserů zefektivnit.
V parserech, které jsme dosud vytvořili, vystupoval :->
vždy
jako predikát v operátorové notaci, jenž v rámci svého volání svazuje
parser s vstupně/výstupním termem a provádí jeho
aplikaci. Cílem této části je ukázat, že po vhodném rozšíření knihovny
nebude volání predikátu :->
v určitých případech vůbec nutné.
Myšlenkou výše zmíněného zlepšení je návrh použít operátor :->
nikoli jako cíl, ale pouze jako funktor vytvářející term a to vše při
zachování možnosti operátorového zápisu konstruktorů.
Způsob, jakým této úpravy dosáhnout, si demonstrujme na příkladu
mutátoru <**>
pro iterovanou aplikaci daného parseru:
<**>(P,W):-
W :->
((P <&> P<**>)
<:>
epsilon).
Přepis těla mutátoru vzhledem k definicím priorit použitých operátorů
by vypadal takto:
:->( W,
<:>( <&>( P,
<**>(P)),
epsilon))
Pokud odstraníme uzávorkování parseru z těla definice mutátoru -- bude
interpretován odlišně:
W :->
(P <&> P<**>)
<:>
epsilon
|
<:>( :->( W,
<&>( P,
<**>(P))),
epsilon)
|
Operátor :->
zde vystupuje jako funktor struktury, který
svazuje vstupně/výstupní term s prvním parserem v alternativní
kompozici. Přestává tedy být cílem a ukazuje se, že by mohl být schopen
za určitých okolností plnit svou úlohu staticky. Tento odlišný způsob
interpretace nás přivádí k myšlence
zavedení další varianty kombinátoru alternativní kompozice:
% I+L :-> P1<:>P2
<:>(I+L :-> P1,P2) :-
I+L1 :-> P1,
I+L2 :-> P2,
append(L1,L2,L).
Co jsme výše uvedeným postupem vlastně získali? Odpoví nám na to
inovovaná definice mutátoru:
<**>(P,W):-
W :->
(P <&> P<**>)
<:>
epsilon.
Z definice je vidět, že při zachování původního způsobu zápisu nyní
v každé iteraci ubylo jedno volání predikátu :->
. Volá se totiž
místo něj a kombinátoru <:>/3
pouze nová verze <:>/2
.
Díky tomu dojde k nezanedbatelnému zrychlení.
Stejným způsobem, jakým jsme upravili mutátor pro iterovanou aplikaci
parseru <**>
, lze upravit samozřejmě nejen ostatní iterátory,
ale i konstruktory parserů. Aby to bylo možné, musíme definovat další
varianty konstruktorů -- stejně jako v případě kombinátoru
<:>
. O tom, jaká z verzí toho kterého konstruktoru se použije
rozhoduje operátorové okolí :->
. Je asi zbytečné zmiňovat, že
definování alternativních variant má smysl pouze v případech
konstruktorů, jenž jsou definovány rovněž jako operátory. V příkladu
další alternativní varianty zůstaňme u našeho mutátoru:
<**>(W :-> P):-
W :->
(P <&> P<**>)
<:>
epsilon.
Jak si lze povšimnout, arita nově definovaných variant konstruktorů je
menší než arita jejich původních definic. Držíme se konvence v níž
vstupně/výstupní term již není samostatným argumentem, ale je
vždy připojen k prvnímu parametru daného konstruktoru operátorem
:->
. Díky tomuto jednoznačnému odlišení je volána vždy správná
varianta predikátu přímo -- tj. verze s aritou o 1 menší.
V knihovně konstruktorů parserů jsou připraveny alternativní verze
všech nejběžnějších kombinátorů a mutátorů, jenž jsou definovány jako
operátory.
Vnitřně se v jejím jádře využívá takřka výhradně zápis představený v této
části, aby bylo dosaženo větší efektivity. Cenou za dosažené zrychlení
je nutnost definování alternativních verzí konstruktorů.
Původní verze jsou v knihovně zachovány, aby se programátor nemusel
detailně zabývat precedenčními vztahy
mezi jednotlivými operátory, je nově zavedená notace navržena tak,
aby původní zápis, kdy je parser explicitně uzávorkován, bylo možné
použít vždy.
Pokud však máme přehled o prioritách operátorů (viz příloha ), můžeme upravit výraz
tak, aby se volala efektivnější varianta daného konstruktoru. Nový
způsob zápisu parserů ale není možné použít za všech okolností přímo --
někdy je nutné provést explicitní uzávorkování. Rovněž v případech
složitějších parserů musí být programátor pozorný, aby byl parser
interpretován přesně tak, jak zamýšlel. Ukažme si proto nyní ještě
jeden příklad, v němž poukážeme na záludnosti, které použití
operátorové notace může skrývat. Zatímco definice klasickým zápisem:
:->( W,
<@( <&>( <**>(P),
Q)
F))
|
bude v pořádku. Pokud použijeme novou notaci, bude parser
interpretován nesprávně:
<@( <&>(W :-> P<**>,
Q),
F)
|
Aby byla dodržena konvence, je nutné provést explicitní uzávorkování
(podobně jako jsme to učinili v mutátoru <**>
v úvodu této
části):
<@( W :-> (P<**> <&> Q),
F)
|
Tato verze je již korektní. Využívá alternativní variantu mutátoru
<@
, volání :->
již neobsahuje a je tedy efektivnější.
Rozšíření knihovny, které jsme si představili v této části, je spíše
technického rázu. Programátor využívající knihovnu nemusí tento způsob
zápisu používat a zabývat se precedenčními vztahy operátorů
a uzávorkováním, aby jeho
parsery byly korektní. Může zůstat u běžné konvence, v níž parser
stojí uzávorkován napravo od operátoru :->
-- tak jako tomu
bylo v předchozích kapitolách. I když totiž definuje parser
obvyklým způsobem a je zavolána standardní verze konstruktoru, z jejího těla se již volání typicky dostává do kódu jádra knihovny a setrvá v něm. Zde se využívají efektivnější verze (jak se lze přesvědčit i na
příkladu mutátoru <**>
) a ztráta je tedy minimální.
Záleží tedy pouze na programátorovi, zda půjde standardní cestou nebo
využije efektivnější, ale záludnosti skrývající notaci.
Pozorný čtenář si jistě také povšiml, že pokud by byla tato úprava dotažena
skutečně do krajnosti, mohli by jsme se úplně obejít bez programování
vyššího řádu. To by však vyžadovalo změnu celkového návrhu knihovny,
nárůst jejího zdrojového kódu a nutnost definovat
množství dalších operátorů. Proto se v této práci budeme i nadále
držet programování vyššího řádu.
dvorka
2013-12-31