Domain Specifikus Nyelvek

A különböző nyelvek által biztosított lehetőségek a belső DSL-ek fejlesztéséhez

Java

Bizonyos lehetőségeket elveszítünk azáltal, hogy a Java nyelv nem támogatja az operátorok használatát. Így például egy matematikai DSL-ben a mátrixok közötti szorzást csak m1.product(m2) alakban tudjuk megadni, nem a természetesebb m1 * m2 vagy m1 x m2 alakban.

Továbbá a Java nyelv meglehetősen bőbeszédű. Emiatt nem igazán lehet tömör DSL kódot írni, ha a DSL nyelvünk Javaba van beágyazva.

Jó példa a Java-ban készített DSL-re a JMock könyvtár, amelynek segítségével olyan objektumok alkothatóak meg, amikkel más objektumok viselkedését lehet tesztelni. Az objektum készítésekor rögzíteni lehet, hogy milyen műveletek fognak meghívódni az objektumon, és milyen paramétereket fog kapni.

Egy példa a JMock használatára:

import org.jmock.Mockery; import org.jmock.Expectations; public class PublisherTest extends TestCase { Mockery context = new Mockery(); // objektum gyár public void testOneSubscriberReceivesAMessage() { final Subscriber subscriber = context.mock(Subscriber.class); // Objektum elkészítése a Subscriber // helyettesítésére. Publisher publisher = new Publisher(); // A vizsgált objektumok publisher.add(subscriber); // felkészítése. final String message = "message"; context.checking(new Expectations() {{ // elvárások megadása oneOf (subscriber).receive(message); // A subscriber objektumon meg fog // hívódni a receive függvény // a „message” paraméterrel. }}); publisher.publish(message); // végrehajtás context.assertIsSatisfied(); // ellenőrzés } }

Látható, hogy az osztállyal szembeni elvárásokat könnyedén és természetes módon írhatjuk le. Egy kissé összetettebb példa az elvárásokra:

allowing (calculator).add(2,2); will(returnValue(5)); allowing (bank).withdraw(with(any(Money.class))); will(throwException(new WithdrawalLimitReachedException()); oneOf (logger).error(with(aStringStartingWith("FATAL")));

Ahhoz, hogy ilyen egyszerűen lehessen ezt az eszközt használni, a jMock tervezői több hasznos felülettervezési technikát is kihasználtak.

Folytonos felület

Az egyik ilyen technika a folytonos felület (fluent interface). Ennek lényege, hogy egy osztály legtöbb függvénye arra az objektumra ad vissza referenciát, amin meghívták. Így lehet egymás után, szintaktikus zaj nélkül beállítani egy objektumot.
mock.expects(once()) .method(“m”) .withNoArguments() .after(“n”) .will(returnValue(20))

Kiolvasva: A mock objektum elvárja (pontosan) egyszer az „m” függvény meghívását paraméterek nélkül az „n” függvény meghívása után, és erre 20-at ad vissza.

Típusozott folytonos felület

A folytonos felület továbbfejlesztése. Lehetővé teszi, hogy korlátozzuk az objektumon használt beállító függvényeket. Úgy valósítható meg, hogy a beállító függvények immár nem az objektumra adnak vissza referenciát, amelyen meghívták őket, hanem előállítják a következő beállítási fázis objektumát.

Például így:

interface MethodNameSyntax { WithSyntax method(String name); } interface WithSyntax { OrderSyntax withAnyArguments(); OrderSyntax withNoArguments(); OrderSyntax with(Constraint c1); // etc. } interface OrderSyntax { StubSyntax after(String id); } interface StubSyntax { void will(Stub stub); }

Ennek köszönhetően az olyan, hibás használata a beállításoknak, mint az alábbi példa:

mock.expects(once()) .withNoArguments() .will(returnValue(20)) // nincs jó sorrendben .method(“m”) .after(“n”);

Nem lesz a Java szemantikája szerint érvényes program, mert a withNoArguments meghívása után az eredmény egy OrderSyntax objektum lesz, amin nincs will függvény.

Ezzel a módszerrel lehetővé válnak az opcionális beállítási lehetőségek is. Használható arra is, hogy objektumokat lépésenként hozzunk létre, de arra is, hogy valamilyen cselekvéssorozatot elvégezzünk velük.

Szintaktikus cukor konténer segítségével

A Java nyelvben nincsenek globális függvények. Emiatt alapvetően nem írhatók olyan függvények, mint az alábbi például az alább kiemelt függvények (a BuyerTest osztálynak nem függvényei, és nem is örökli őket).

public class BuyerTest extends TestCase { void testAcceptsOfferIfLowPrice() { offer = mock(Offer.class); context.checking(new Expectations() {{ offer.expects( once() ) .method(“buy”) .with( eq(QUANTITY) ) .will( returnValue(receipt) ); }} } }

Ezeket a statikus függvényeket az Expectations osztálytól kapjuk. Itt nem egészen triviális, hogy miért szerepelnek dupla kapcsos zárójelek, de ha elég sokáig böngésszük a Java specifikációt, akkor rájövünk, hogy itt valójában az történik, hogy egy névtelen osztályt származtatunk az Expectation ősosztályból, majd definiálunk neki egy konstruktort.

Természetesen ilyen módszerek használata általában nem célravezető, mert nehéz megérteni, hogy egy adott kódrészlet valójában mit csinál. Azonban a DSL megalkotásának egyik lényege, hogy magasabb absztrakciós szintet ad. Valójában nem muszáj megérteni, hogy miért szükséges a dupla kapcsos zárójel, ha csak használni szeretnénk a DSL-t, akkor elfogadhatjuk, hogy a szintaxis része.

Haskell

A Haskell jól használható környezetet ad DSL-ek fejlesztéséhez, akkor is, ha azok működése nem a funkcionális programozás elvei szerint történik. Sok pozitív tulajdonsága van, de leginkább a letisztultsága, a rugalmassága (nyelvbeli absztrakciók, nyelvi kiterjesztések), a szintaktikus lehetőségei (operátorok, mintaillesztés) miatt érdemes használni. Persze a funkcionális gondolkodásmóddal nem árt megbarátkozni, mielőtt az ember egy nagyobb projektbe fog.

Jó eszközöket ad a sekély DSL-ek kifejlesztéséhez, vagyis olyan DSL-ek létrehozására, amiknek nincsen a gazdanyelvben reprezentációjuk, hanem rögtön végrehajtódnak. Ez annak köszönhető, hogy kifinomult eszközök vannak a függvények használatára és manipulációjára. Ezek sok esetben hasznosak lehetnek, hasonlóan működnek egy nyelvi modul felületéhez.

Ha mégis szeretnénk reprezentációt létrehozni a DSL-ünk számára, mert például szeretnénk valamilyen transzformációt elvégezni a DSL kódon, akkor is segítségünkre lesz a Haskell. Az algebrai adattípusok segítségével szintaktikus zaj nélkül, szinte EBFN szintaxissal alkothatjuk meg a reprezentáció adattípusait.

Példaként álljon itt egy egyszerű, kifejezések leírására alkalmas DSL reprezentációja:

data ArithExpr = Lit Integer | Add ArithExpr ArithExpr | Mul ArithExpr ArithExpr | Sub ArithExpr ArithExpr

Ehhez könnyedén tudok készíteni egy kiértékelőfüggvényt is, ami egy kifejezésből előállítja annak eredményét.

evalArith :: ArithExpr -> Integer evalArith (Lit x) = x evalArith (Add x y) = evalArith x + evalArith y evalArith (Mul x y) = evalArith x * evalArith y evalArith (Sub x y) = evalArith x - evalArith y

Egy ilyen egyszerű példa persze gond nélkül továbbfejleszthető abba az irányba, hogy kezelni tudjon változókat, vagy éppen utasítások szerepeljenek benne egyszerű kifejezések helyett.

Megszorítások a reprezentáción

Az algebrai adattípusok jó lehetőségeket adnak a DSL típusrendszerének megalapozására. Az előző példa ArithExpr adattípusa mellé vegyünk fel egy BoolExpr-t is. Ebben az esetben lényegében kikényszerítettük azt, hogy az And két oldalán csak logikai kifejezések állhatnak, a Greater pedig két aritmetikai kifejezésből egy logikai kifejezést ad eredményül.

data ArithExpr = IntLit Integer | Add ArithExpr ArithExpr data BoolExpr = BoolLit Bool | And BoolExpr BoolExpr | Greater ArithExpr ArithExpr

A típusfüggvények használatát is lehetővé tevő típusrendszer pedig sok invariánst már reprezentációs szinten is ki tud fejezni a DSL nyelv szemantikájával kapcsolatban.

A következő változatban egy megkötést vezetek be: A kifejezés típusa tetszőleges (szám típus) lehet, de ekkor minden részkifejezése is azonos típusú. Ennek az lesz az eredménye, hogy a kifejezésben levő összes literál azonos típusú.

data ArithExpr t = Lit t | Add (ArithExpr t) (ArithExpr t) | Mul (ArithExpr t) (ArithExpr t) | Sub (ArithExpr t) (ArithExpr t)

Itt lényegében a Haskell típusrendszerét használtuk arra, hogy a DSL típusrendszerét kialakítsuk. Természetesen nem lehet minden szabályt kifejezni ilyen módon kikényszeríteni, de kezdetnek ez sem rossz.

Típusosztályok

A típusosztályok segítségével az is lehetővé válik, hogy a DSL nyelv nagyon közel álljon a hagyományos Haskell kódhoz.

Határozzuk meg például, hogy az ArithExpr adattípus a Num osztályhoz tartozik. Ekkor meg kell adnunk néhány előre meghatározott függvényt. Minden műveletnek megfeleltetjük a saját DSL-ünkbeli megfelelőjét.

instance Num ArithExpr where x + y = Add x y x * y = Mul x y x - y = Sub x y abs x = error "Not implemented" // Ezek a műveletek még nem szerepelnek a signum x = error "Not implemented" // DSL-ben. fromInteger x = Lit x

Ennek két pozitív következménye lesz a nyelvünkre nézve. Elsősorban lehetővé válik, hogy kifejezéseinket ugyanúgy írjuk fel, mint a Haskell kifejezéseket.

expr :: Int expr = 4 + 3*2 // fölírok egy hagyományos Int típusú kifejezést > 10 / eredménye egy szám expr’ :: ArithExpr expr’ = 4 + 3*2 // fölírom ugyanazt a kifejezést, csak annyi a különbség // hogy a típusa más > Add (Lit 4) (Mul (Lit 3) (Lit 2)) // az eredmény teljesen más: megkapom a kifejezés // DSL-beli reprezentációját

Ez már önmagában is jelentős eredmény, hiszen DSL kifejezéseket írni semmiben sem más, mint egyszerű Haskell kifejezéseket írni. Azonban van ennek még egy előnye, a Haskell gazdag függvénykészlete immár a DSL-ünk számára is elérhető lesz. Használjuk például a sum függvényt, amely egy beépített Haskell függvény:

sumexpr :: ArithExpr sumexpr = sum [3, 4 * 3, 2 - 7] > Add (Add (Add (Lit 0) (Lit 3)) (Mul (Lit 4) (Lit 3))) (Sub (Lit 2) (Lit 7)) // Ahogy azt a sum függvénytől megszoktuk, összegzi a // lista elemeit. Kivéve, hogy itt ezt a DSL-en belül végzi. evalArith sumexpr > 10 // Csak ellenőrzés végett értékeljük ezt ki.

Scala

A Scala igazán gazdag eszközkészletet ad DSL-ek tervezésére. Az előző félév során ezzel kapcsolatban bővítettem a nyelv leírását, és van néhány érzékletes példa is. Röviden összefoglalva, a Java és Haskell nyelvnél felsorolt jó tulajdonságok közül sok megvan benne, köszönhetően annak, hogy a nyelv tervezésének során külön foglalkoztak a DSL-ek beágyazásának kérdésével. Sajnos az nem mondható, hogy a nyelv szabályai egyszerűek lennének, különösen, ha már mélyebben foglalkozunk velük.