Jak bylo již zmíněno, jazyky přijímané parsery v alternativní
kompozici obecně mohou být disjunktní. V těchto případech dochází
k neefektivitě. Celý problém si ukážeme na velmi jednoduchém případu
kombinátoru letter
, v němž jsme v jeho původní definici použili
právě kombinátor <:>
:
letter(W):- W :-> lower <:> upper.Zamyslíme-li se nad tím, jak
letter
funguje například pro vstup
"a"
, tak zjišťujeme, že je nejdříve aplikován parser
lower
. Až potud je vše v pořádku. Potom je však použit parser
upper
, který neuspěje a dojde k navracení. Protože jazyky
přijímané parsery lower
a upper
jsou disjunktní, s naprostou jistotou víme, že kdykoli lower
uspěje, tak již nemá
smysl parser upper
aplikovat -- ten v takovém případě
vždy selhává.
V tomto jednoduchém případě je ztráta efektivity ještě malá, ale můžeme si představit mnohem komplikovanější alternativní kompozice množství parserů, jejichž aplikace již bude z hlediska časové složitosti nezanedbatelná. Kupříkladu:
prologProgram(W):- W :-> (prologDirective <:> prologClause <:> prologComment) <**> .Pokud uspěje parser
prologDirective
je jisté, že všechny zbylé
alternativy selžou. To platí rovněž v případě
prologClause
. Výpočetní složitost aplikace jednotlivých parserů
v alternativní kompozici je v tomto případě již nezanedbatelná. Ve
většině iterací se tak plýtvá.
Nabízí se tedy otázka, zda by nebylo možné takovou přídavnou informaci nějakým způsobem využít v implementaci parserů. Umožní nám to následující dvě varianty kombinátoru alternativní kompozice:
<:(P1,P2,I+L):- I+L_ :-> P1, (L_=[] -> (I+L :-> P2) ; L=L_).Kombinátor
<:
má charakter zkráceného vyhodnocení zleva. Pokud
uspěje parser tj. vrátí neprázdný seznam úspěšných rozkladů,
stává se tento seznam rovněž výsledkem kompozice. Parser se
použije pouze v případě selhání .:>(P1,P2,I+L):- I+L_ :-> P2, (L_=[] -> (I+L :-> P1) ; L=L_).Kombinátor
:>
je symetrickou variantou <:
. Nejdříve tedy
aplikuje parser a teprve v případě jeho neúspěchu .
Nyní definujme efektivnější variantu parseru letter
vytvořenou
pomocí jednoho z právě zavedených kombinátorů:
letter(W):- W :-> lower <: upper.Výše uvedený parser vydává identické výsledky jako původní
letter
,
navíc však pracuje efektivněji.
Na základě právě vytvořených kombinátorů můžeme definovat nejen množství lépe fungujících parserů, ale také samotných konstruktorů.
V jejich vytváření pokračujme nyní parserem binárního čísla. Jeho vstupem je posloupnost nul a jedniček. Jako výsledek vydává odpovídající decimální hodnotu:
bin(W):- W :-> (symbolA("0") <: symbolA("1"))<<*>> <@ foldR(evalBin,0>0) => alter(Val>_,Val). evalBin(I,Val>Pow,Value>Power):- Power is Pow+1, Value is Val+(2^Pow)*I.Variantu nově definovaného kombinátoru alternativní kompozice jsme použili v parseru
bin
ve stejné situaci jako v letter
.
Půjdeme ještě dál a představíme si zástupce nové rodiny iterátorů:
<*>(P,W):- W :-> (P <&> P<*>) <: epsilon.V původně použitém mutátoru
<<*>>
je v každé iteraci aplikováno
primitivum epsilon
jen proto, aby byl jím vydaný výsledek
(kromě případu poslední iterace) oříznut v seznam úspěšných rozkladů
pomocí diamantu.
Iterátor <*>
řeší ořezávání struktury LOS elegantnějším
způsobem -- zahazuje nepotřebné výsledky za běhu a postupuje nejpřímější cestou k maximální derivaci. V průběhu dalších
fází výpočtu se tak v případě zřetězení parserů nemusíme zdržovat
vytvářením výsledků o nichž dopředu víme, že je nebudeme
potřebovat. Druhá alternativa -- tedy epsilon
, se uplatní
pouze jednou a to za poslední číslicí přijímaného čísla:
bin(W):- W :-> (symbolA("0") <: symbolA("1"))<*> <@ foldR(evalBin,0>0) => alter(Val>_,Val).V knihovně jsou připraveny i varianty zbylých iterátorů -- jako
<+>
, <?>
a jejich varianty.
Důležitým přínosem nových konstruktorů je nejen výrazná úspora paměti, ale také větší efektivita výpočtu. Stejně jako s pomocí diamantových verzí lze s jejich pomocí udržovat velikost struktury LOS v přijatelných mezích. Navíc se nezajímavé výsledky ani nevytvářejí, jak jsme si vysvětlili výše.
Zároveň je na místě velká opatrnost při použití společně s parsery přijímajícími jazyky, jejichž průnik je neprázdný.
Jindy může být eliminace derivací žádoucí. Příkladem takové situace jsou identifikátory a klíčová slova programovacích jazyků.
Vezměme si například jazyk Java. Klíčová slova jsou rezervovanými
názvy identifikátorů (například new
či for
). V analyzátoru se pak použije konstrukce typu:
Pokud uspěje parser klíčových slov , zbytečně se již neztrácí čas s parserem identifikátorů. Ten by sice uspěl, ale jeho výsledek by nebyl přípustný.W :->
...
<:
...
Jindy použití nových verzí kombinátorů v případech parserů s neprázdným průnikem sice může zrychlit výpočet a ušetřit paměť, zároveň nás však může připravit o důležité výsledky. Je tedy nutné věnovat dostatečnou pozornost návrhu parserů.
Až dosud jsme vytvořili několik rodin mutátorů pro iterativní aplikaci
parserů. Rodina iterátorů představená v této části je poslední a v mnoha ohledech nejlepší z nich. Počet definovaných iterátorů opět
narostl, proto jsou všechny přehledně shrnuty v tabulce .
dvorka 2013-12-31