Parse-puu läbimine Visitoriga
OOP-i eksperdid harilikult krimpsutavad nina instanceof
-i kasutamise peale. Paremaks peetakse seda, kui iga alamklassi isend (nt. puu tipp tüübiga KorrutamineJagamineContext
) leiab ise tee selle õige koodini, mis tema jaoks on mõeldud. Keerukus tuleb sellest, et me ei taha (ja tegelikult ei saagi) genereeritud Context
klassidesse oma meetodeid lisada. Probleemi lahendamiseks on välja mõeldud üsna komplitseeritud skeem, mida inglise keeles nimetakse visitor pattern.
ANTLR tuleb OOPi puristidele vastu ja pakub välja mugava võimaluse korraldada parse-puu läbimist selle skeemi põhjal. Nimelt genereerib ta soovi korral koos parseriga veel ühe liidese ja ühe klassi, mis realiseerivad selle skeemi raamistiku. AKTK puhul on nendeks AktkVisitor
ja AktkBaseVisitor
. Nende genereerimiseks on juba build.gradle faili lisatud generateGrammarSource reegli juurde vastav käsureaparameeter.
Vaatame kõigepealt liidest AktkVisitor
(võta see enda IDE-s lahti). Nagu näed, on siin meetod iga grammatika reegli ja iga sildi kohta. Meetod saab argumendiks vastava tüübiga parse-puu tipu ning peab ka midagi tagastama. Liidese päisest on näha, et tagastustüüp T
ei ole mitte mingi konkreetne tüüp, vaid tüübiparameeter, seega on võimalik liidese realiseerimisel ise öelda, mis tüüpi asju me tahame puu läbimisel tagastada. Näiteks avaldise väärtustamise juures määraksime me selleks tüübiks Integer
.
Puu "käsitsi" läbimise näite juures ilmnes, et meil ei olnud vaja käsitleda kõiki võimalikke parse-puu tipuklasse -- piisas kui tegime midagi sisulist ainult antud ülesannet puudutavate tipuklasside puhul. Vahetippude puhul lihtsalt liikusime allapoole. Liidese AktkVisitori
implementeerimisel nõuavad Java reeglid aga kõigi meetodite realiseerimist. Selleks, et ka siin programmeerijate vaeva vähendada, genereeris ANTLR koos selle liidesega ka ühe klassi, AktkBaseVisitor
, mis implementeerib antud liidese selliselt, et iga tiputüübi juures delegeeritakse töö alluvatele, kui neid on. Seega, kui me tahame teha midagi spetsiifilist vaid üksikute tiputüüpide puhul, siis ei ole meil mõtet mitte implementeerida liidest AktkVisitor
, vaid luua alamklass klassile AktkBaseVisitor
, kus vaid teatud meetodid on ülekaetud.
Kui me tahaksime eespool mainitud lihtsa avaldise väärtustamise ülesannet realiseerida visitoriga, siis sobiks meile selline klass:
import org.antlr.v4.runtime.tree.ParseTree; import week9.AktkBaseVisitor; import week9.AktkParser.*; public class AktkEvaluationVisitor extends AktkBaseVisitor<Integer> { @Override public Integer visitArvuliteraal(ArvuliteraalContext tree) { return Integer.parseInt(tree.getText()); } @Override public Integer visitLiitmineLahutamine(LiitmineLahutamineContext tree) { return arvutaTehe ( tree.getChild(0), tree.getChild(1).getText(), tree.getChild(2) ); } @Override public Integer visitKorrutamineJagamine(KorrutamineJagamineContext tree) { return arvutaTehe ( tree.getChild(0), tree.getChild(1).getText(), tree.getChild(2) ); } private Integer arvutaTehe(ParseTree leftChild, String operator, ParseTree rightChild) { // Väärtustan rekursiivselt. // visit on spets. meetod, mis organiseerib õige visit-meetodi // väljakutsumise vastavalt argumendi tüübile. int leftValue = this.visit(leftChild); int rightValue = this.visit(rightChild); // väärtustan terve operatsiooni switch (operator) { case "+": return leftValue + rightValue; case "-": return leftValue - rightValue; case "*": return leftValue * rightValue; case "/": return leftValue / rightValue; case "%": return leftValue % rightValue; default : throw new RuntimeException("Tundmatu operaator"); } } }
Nagu näha, on need juhtumid, mida me enne eristasime instanceof
kasutamise abil, on nüüd saanud endale eraldi meetodid. Kuna visitor pattern ei lubanud meil kokku võtta juhtumeid LiitmineLahutamine
ning KorrutamineJagamine
, siis on kummagi jaoks küll eraldi meetod, aga mõlemad delegeerivad põhitöö samale abimeetodile. Selles variandis ei ole enam eraldi käsitletud vahetippe, sest nende käsitlemiseks piisab ülemklassis defineeritud meetoditest.
Selle klassi kasutamiseks piisab järgmisest koodist:
private static int evaluateWithVisitor(ParseTree tree) { AktkVisitor<Integer> visitor = new AktkEvaluationVisitor(); return tree.accept(visitor); }
Võimaliku lahenduse algus
Visitoriga variant
import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; import week9.ast.*; import week9.AktkBaseVisitor; import week9.AktkParser.*; public class AktkAstCreationVisitor extends AktkBaseVisitor<AstNode> { @Override public AstNode visitFunktsiooniValjakutse(FunktsiooniValjakutseContext ctx) { // NB! siin tuleb kontrollida, kuidas näeb välja // - ilma argumentideta // - ühe argumendiga // - mitme argumendiga // funktsiooni väljakutse parse-puu String funktsiooniNimi = ctx.getChild(0).getText(); List<Expression> argumendid = new ArrayList<Expression>(); // Kui väljakutsel on 3 alluvat, siis need on funktsiooni nimi, alustav // sulg ja lõpetav sulg. if (ctx.getChildCount() > 3) { // argumentavaldised on paarisarvulise indeksiga lapsed for (int i=2; i < ctx.getChildCount(); i += 2) { argumendid.add((Expression)this.visit(ctx.getChild(i))); } } return new FunctionCall(funktsiooniNimi, argumendid); } @Override public AstNode visitOmistamine(OmistamineContext ctx) { return new Assignment(____________, _____________); } @Override public AstNode visitLauseteJada(LauseteJadaContext ctx) { // Siin on tähtis järgi uurida, kuidas näib välja mitme lausega // lauseteJada parse-puu. List<Statement> laused = new ArrayList<Statement>(); ____________________________________; ____________________________________; ____________________________________; ____________________________________; ____________________________________; ____________________________________; ____________________________________; return new Block(laused); } }