A Factor programozási nyelv igen modern lehetőségekkel rendelkezik metaprogramok írásához, fordítási időben történő kiértékeléshez és a fordító bizonyos értelemben vett kiegészítéséhez/scripteléséhez. Mondhatni ez a nyelv egyik különleges erőssége, így érdemes neki egy egész fejezetet szentelni.
Alapvetően a következő lehetőségek állnak rendelkezésünkre:
A fordítási idejű szavak, gyakorlatilag olyan szavak, melyek tokenizáláskor a fordító belsejében futtatják le a törzsükbe írt kódot. Ez gyakorlatilag a "Fordító scriptelését" jelenti tehát.
Mint azt már korábban leírtuk, egy factor program első közelítésben szóközökkel elválasztott tokenek sorozata (ahol a "..." is egy tokennek számít). Ez alapvetően tényleg igaz is, de igazából ha egy kicsit mélyebbre tekintünk a fordító működésébe, akkor látni fogjuk, hogy ettől akár teljes mértékben e is térhetünk. Ugyanez igaz a postfix alakú műveletvégzésre is, mert az itt ismertetett eszközökkel beágyazott nyelvek, infix kifejezések (lsd. az infix szótárt és az [infix szót) és hasonló érdekességek valósíthatóak meg.
Hogyan is valósítható ez meg egy olyan nyelvben, mely egymásután írt értékek és szavak együtteséből áll? Egy viszonylag elmés trükköt alkalmazva: A lényeg, hogy képesek vagyunk olyan szót deklarálni, mely nem akkor fog lefutni, amikor a program a megfelelő részhez ér a generált kódban, hanem amikor a fordító a forrásszövegben a megfelelő tokent éppen beolvasta!
Tehát gyakorlatilag a fordító szépen halad a forrás elemzésével sorról-sorra egyesével olvasgatva a tokeneket egészen odáig, amíg egy olyan tokenhez nem ér, mely fordítási idejű szónak van korábban már deklarálva. Ilyenkor ahelyett, hogy tovább építené az elemzési fát, a megfelelő kódrészletre ugrik és elkezdi végrehajtani az ott leírt kódot.
A fordítási-idejű szavainkat a SYNTAX: szóval deklarálhatjuk és a törzsét a ;-ig adhatjuk meg, akár egy hagyományos szó deklarációjkor. Nagyon fontos, hogy a fordítási idejű szavak forrását is hagyományos factor programként írhatjuk le, nem egy külön kis nyelv használható csak erre a célra. Ez azért is érdekes, mert így akár a fordítási szó deklarációjakor felhasználhatunk már meglévő, máshol definiált fordítási szavakat, tehát ha valamihez csináltunk már egy szintaktikus/szemantikus cukorkát, akkor azt akár a további ilyen deklarációinkhoz is felhasználhatjuk! Erre egyébként példát is láthatunk a mellékelt "imperative szótár"-példa implementációjának a while részében.
A fordítási idejű szavainknak ( syntaxtree -- syntaxtree' ) stack effect-el kell rendelkezniük. Ebből az is látszik, hogy az eddig elemzett szöveg valamilyen formában a jelenleg futó fordítási idejű szó rendelkezésére áll. Ez a szintaxisfa egy sorozat (vektor) formájában érhető el. Ez a vektor alapvetően szavakat és értékeket tartalmaz wrapper segítségével beágyazva (esetleg más, hasonló vektorokat) és mivel egy hagyományos sorozatról van szó, használhatjuk például a suffix! műveletet a módosítására. Ez azért hasznos, mert ezzel a módszerrel az elemzéskor futó fordítási szavunk olyan elemeket pakolhat rá a fordító által gyűjtögetett elemzőfának a végére "top level form"-ként, amik elvileg nem szerepeltek azon a helyen, így a kódban. Magyarul gyakorlatilag fordítási időben kigenerálhatunk tokeneket, melyek nincsenek eredetileg a kódban, de miután odahelyezzük őket pont a nekik megfelelő működést fogják nyújtani...
Mint azt említettük, használhatunk más fájlban definiált fordítási szót a fordítási szavaink törzsében, de fontos itt, hogy külön fájlban legyen a két dolog, különben nem fog működni. Van egy másik korlátozása is a rendszernek: bár tetszőleges értéket, szó wrappert, illetve kódidézetet helyezhetünk a veremre (ez utóbbit wrappelt szavakból álló vektorból a >quotation-al előállítva), sajnos itt már nem lehetünk olyan lazák, hogy használhassuk a saját SYNTAX:-al vagy más metaprogramozós módon deklarált dolgainkat. Ez utóbbi azért nem lehetséges, mert a fordító ekkor már pont abban a fázisban van, amikor ezeket kifejti (ugye pont ezért kezdett el futni a megfelelő fordítási szavunk) és ilyenkor már mit se számít, hogy az elemzési vektor végére ilyen szavakat akarunk irogatni - nincs aki kifejtse őket újra...
Egy viszonylag egyszerű, fordítási idejű hello world program:
A fentiek még nem sokat érnének, ha csupán annyi lenne a dolog, hogy egy szó segítségével manipulálhatnánk az eddig elvégzett elemzést és esetleg bizonyos kódot odagenerálhatnánk így a szavunk helyére. Ennek is már van bizonyos értelme, de a factor sokkal több lehetőséget nyújt a számunkra ennél. Ez a plussz előre definiált szavak egy olyan csoportja miatt létezik, melyeknek segítségével a fordítónak adhatunk utasításokat!
Az előző fejezetben említett suffix! nem tartozik közvetlenül ezek közé, az csupán egy sorozat végére helyez egy elemet. Itt olyan szavakra kell gondolni, amelyek segítségével egy tokent olvashatunk az input szövegből stringgé alakítva (scan), vagy akár több tokent stringek vektorjaként az adott stringig bezárólag (parse-tokens) és egyéb bonyolultabb szavakról, mint pl. parse-until, mely hagyományos faktor kódelemzést folytat egészen a megadott tokenig, ahol is kihasználhatjuk, hogy a parse-until más metaprogramozott szavakat is gond nélkül olvas, mert gyakorlatilag csak megkérjük a fordítot, hogy egy darabig (mondjuk egy ;-ig) csinálja azt, amit előttünk csinált volna. Ilyen és ehhez hasonló elemzési szavakból iszonyatosan sok található már meg a nyelvhez tartozó sztenderd szótárakban, így képtelenség őket itt számba venni. Mindenesetre a stringig bezárólag olvasó, stringet előállító szóval gyakorlatilag már mindent meg tudunk csinálni (persze bizonyos gyakorlati korlátok azért adódhatnak)
Az alapvető előre-olvasási műveletekről a programcsomag listenerjéből elérhető Factor handbook/The language/Parsing words alatti részekben érdemes olvasni.
Egyszerű fordítási szó, fordítási időben történő kiértékeléshez:
Könnyen látható, hogy a saját beágyazott nyelvünk képességeinek igazából semmi sem szabhat határt: adott egy turing-teljes nyelv (a factor maga), amivel a fordítót scriptelhetjük és adott egy turing-teljes nyelv (az elemzési fán gyűlő szavak listája), melyre a kódot alakítjuk. Nekünk csupán (a leglecsupaszított esetet tekintve) egy stringből kell dolgoznunk és az elemzési listába (nem kell fát építeni a szavakból) forth-stílussal a megfelelő programot megírnunk. Ha megvalósítjuk egy elemzési sóhoz az algoritmust, mely egy az elemzést záró egyértelmű szövegig tartó stringet ilyen módon elmezni képes, akkor elmondhatjuk, hogy implementáltunk egy beágyzottt DSL-t! Elvileg csak a rátermettségünkön és a kitartásunkon múlik, hogy milyen beágyazott nyelvet valósítunk meg, akár egy szigorúan típusos DSL is elképzelhető, ha kellően sokat macerálunk a dologgal!
Viszont ez a módszer nem csak az alap factor kódtól teljesen elkülönülő beágyazott nyelveket tesz lehetővé, hanem az alapnyelv elemeihez gördülékenyen illeszkdő szintaktikus és szemantikus cukorkák létrehozására is kényelmes eszköz. Ez utóbbi lehetőségre viszonylag sok példát láthatunk. Ilyen többek között a USING: ... ; is, mely a sok USE: szótár kiváltására alkalmas (ha megnézzük a kódját kiderül, hogy USE: -okra fordítja a kódunkat).
A fentieken kivul sajat felhasznalasi peldakkal is rendelkezunk is adhatunk. A factor egy olyan nyelv, amelyet nem igazan lehet, csak probalgatassal elsajatitatni, így nagyon fontos a példák adása az egyes lehetséges nyelvi eszközökhöz, különös tekintettel a fordítási szavak rendszerének a megértéséhez. Aki el akar mélyedni a factor nyelv ezen részében (vagy mert érdekli a nyelv, vagy mert esetleg ötletet szeretne meríteni más megoldásokhoz) annak erősen javasoljuk, hogy nézze meg a példaprogramok fejezet vonatkozó, metaprogramozási részét, melyben a Factor alapnyelvet kiegészítjük és felkészítjük imperatív programozási stílusra. Az említett példaprogramban megvalósítjuk az elágazást, a ciklust, a változók kezelését (értékadás+deklaráció), procedúra és függvényhívásokat. Tesszük mindezt egyrészt a factor alapnyelvbe beágyazott módon, de egy külső szótárhalmazként(az egyes elemek akár külön USE:-olhatók), másrészt olvasható szintaktikával és infix alakú kifejezésekkel.
Egy látványos példakód, mely az említett szótárainkat használja:
Amit fent látunk az mind-mind csupán a betöltött imperative.akármi szótáraknak köszönhető. A legtöbb modul viszonylag rövid (pl. az IF megvalósítása 50 körüli sor) és csak és kizárólag fordítási szavakat használ a szintaxis és a szemantika megadásához! A példaprogramként adott modulokhoz egy tesztállomány is tartozik, melyben a változók/értékadások és egyebek is bemutatásra kerülnek, így itt azokat nem emeltük ki külön példának. A példaprogram továbbá soronként és lépésenként el van látva kommentekkel, így igen hasznos forrás a fordítási szavak használatának a pontosabb megértéséhez.
Egy az előzőnél sokkal rövidebb, de szintén hasznos alkalmazás:
Ez a kis szavacska nagyon hasznos segítség lehet akkor, ha épp érteni szeretnénk mi történik és miből mi alakul ki végül ténylegesen. Továbbá az előző példából az is látszik, hogy nem csak un. "top level formákkal" használható ez a szó, mert amikor a kódidézet belselyéből hívtuk, csak az ahhoz tartozó elemzési nódus gyermekeit mutatta meg...
A funktorok gyakorlatilag sablonok melyeket elsősorban szavak és tuple-k generikus előállítására használhatunk fel. Tehát paraméterezett kódgenerálásra használható szintaktikus cukorkáról van szó, mely ilyen esetekre megóv minket mindenféle bonyolultabb fordítási szavak által történő trükközéstől. A functorok a functors szótárban találhatóak meg.
A funktorokat a FUNCTOR: szó vezeti be, melyet egy név, majd stack effekt deklaráció követ. A stack effect deklaráció a négyespont (::) -típusú szódeklarációhoz hasonlít. Itt is használhatjuk tehát a bemeneti paraméterek neveit lokális változóként majd a funktor törzsében.
A stack effect deklarációt valahány deklarációs sor követi, melyek újabb lokális változókötéseket hoznak létre. Ezeknek az alakja egy <lokális változónév><műveleti jelölő><deklarációs név> hármas. A lokális változó nevét azért kellmegadnunk, hogy a keletkező szóra már itt is hivatkozhassunk, a műveleti jelölő azt adja meg, hogy a hivatkozott szónak már léteznie kell (IS lehetőség), vagy most hozzuk létre majd (DEFINES és DEFINES-CLASS lehetőségek). A hármas legutolsó tagja egy név, ez lesz majd a keletkező szó neve. Ha ez nem lehetne paraméterezhető, akkor elég nagy bajban lennénk, ezért ez a rész egy un. interpolate-alak, melyben vegyíthetünk hagyományos szöveget lokális változókra történő hivatkozásokkal. Erre a lehetőségre egy példa: ${CLASS}-array.Tehát először megadjuk deklratív módon a keletkező szavainkat.
A fenti deklaratív leírást a a WHERE szó követi, amely után a létrehozott szavainkat írhatjuk be. Itt gyakorlatilag az implementációját adjuk meg paraméteresen annak, amit fent csak deklaráltunk...
Mint azt megszokhattuk itt sem szovegszeru behelyettesitesrol van csupan szo, ami nem is meglepo, hiszen a funktorok forditasi szavakkal vannak megvalositva...
Makrókkal kódidézeteket állíthatunk elő fordítási időben, melyek a makróhívás helyére kifejtve helyettesítődnek. A kódidézet előálltása paraméterezhető, így a makró fordítási időben ismert értékeket ehet meg a hívásakor a veremtetőről. Mindaz amit makrók segítségével megoldhatunk, az implementálható lenne kódidézetek futásidejű létrehozásával és call hívással, a makrók azonban akár jelentős sebességbeli gyorsítást eredményezhetnek a futásidejű kifejtődés miatt.
A fordítási időben történő futástól eltekintve a következő két kódsor ekvivalens:
Egy lehetséges felhasználási mód:
Egy makró akkor is kiértékelődik, ha olyan bemenetet adunk neki, amit a fordító nem tud fordítási időben kiszámítani. Ebben az utóbb említett esetben a fenti két megoldás esetén ugyanaz történne (tehát fordításkor nem ismert értékek esetén futásidőben jön létre a megfelelő kódidézet). A makrókról tehát összefoglalva elmondható, hogy gyakorlatilag olyan szavak, melyeknek a fordítási időben ismert paramétereik alapján kifejtésre kerülő kódidézet kerül a hívás helyére, míg a hagyományos, futásidejűséget megkövetelő paraméterektől függő kódrészük nem fejtődik ki.
Megj.: Makrók esetén is van a négyespontos definícióhoz (::) hasonló eszközünk. Itt ezt a MACRO:: szóval vezethetjük be, ha szeretnénk használni lokális változókat.
Jelenleg egy PEG (Parsing Expression Grammar) típusú elemző modul van a Factorhoz.
A PEG-es elemzők az LR és LL elemzőkhöz képest több erőforrást használnak fel, tehát kevésbé hatékonyak. Az egyszerű implementációk általában exponenciális futásidőt eredményeznek az elemzendő szövegen, de a Factorban egy lineáris futású PEG-elemző kapott helyet, ami viszont cserébe kifejezetten memóriaigényes. Az előbbiek ellenére a PEG-es elemző egy igen erős eszköz beágyazott nyelvek leírására. Egyedül arra kell odafigyelnünk, hogy PEG segítségével nem lehet, csak egyértelmű nyelveket elemezni, mert a hagyományos környezetfüggetlen nyelvtanokhoz képest itt számít a leírt szabályok sorrendje. Ami korábban van, hamarabb illeszkedik.
A PEG nyelvtanokról/elemzőkről itt olvashatunk egy rövid összefoglalót:
http://en.wikipedia.org/wiki/Parsing_expression_grammar
Egy különösen fontos megjegyzés a példaprogramokhoz:
A megértés érdekében a példaprogramok bő-lére eresztett kommentekkel vannak ellátva, de sajnos mivel a kommentezési lehetőségek szintén csak fordítási idejű szavakkal vannak a factorban megvalósítva, itt igazából nem működnek. Tehát ha az itt látható kódokat futtatni is akarjuk (mondjuk a listenerben), akkor egyrészt meg kell sabadítanunk a példákat a kommentektől, másrészt a listener által felajánlott szótárakat USE:-olnunk is kell majd (ezt a kód bemásolására az interpreter felajálnja így nem macerás és ez a lépés egyéb korábbi példáknál is már egyébként is szükséges).
Az EBNF: kulcsszót használhatjuk egy elemzőszó létrehozására. Az elemzőszó a vermen található stringet várja paraméterül és egy absztrakt szintaxisfát ad eredményül. Az elemzőszó neve az EBNF: után írt azonosító lesz míg a keletkező elemzési fa az EBNF; szóig bezárólag írt szabályok alapján áll elő.
Az elemzőszó megadása tehát a következő alakban írandó:
Ahol a szabályklózok a nemterminális nevéből, egy egyenlőségjelből, az illeszkedési mintából és egy opcionális akcióból állhatnak. A szintaxisfa egy Vektor adatstruktúrában épül, melybe a nemterminális pozíciók helyén további vektorok vannak beágyazva így alakítva ki a fastruktúrát.
Konkrét példák az egyszerű működésre:
Mint azt fentebb említettük, a szabályokhoz adhatunk meg opcionális akciókat is. Ezt a szabályt követő => jel után írt factor programmal fejezhetjük ki, melyet [[ és ]] közé írhatunk be. Az így megadott kód a stack-en eléri az adott node-hoz tartozó elemzési fát (a fent látott vektoros formában) és módosíthatja azt tetszőleges elemre cserélve. Ilyen kódot a szintaxisfa tetszőleges pontjához rendelhetünk, tehát a fenjebb található elemeinkben a gyerek nódusokból szintetizálódó eredményre számíthatunk és azt dolgozhatjuk tovább új eredményt előállítva.
Egy konkrét felhasználási példa:
A fent látható kódot egy kis ügyeskedéssel tovább kombinálhajuk, ha vegyítjük egy kicsit a korábban említett fordítási idejű szavakkal:
Az előző példa azért szerepel itt, hogy lássuk az ebnf-es elemzés miként segíthet pl. az elemzési szavaink esetén saját beágyazott nyelvek definiálására. Itt most viszonylag rövid teljes értékű beágyazást hajtottunk csak végre (CALCULATE: és ; között), de egy kis fantáziával akár egy [MYLANGUAGE ... MYLANGUAGE] jellegű, blokkszerű felhasználás is lehetségessé válik, amin belül egy akármilyen szemantikára forduló, tetszőleges szintaktikájú beágyazott programozási nyelvet is kidolgozhatunk!