Göm menyn

TDP007 Konstruktion av datorspråk

Seminarie 3: Parsning och Domänspecifika språk


Detta seminarium behandlar parsning av språk och domänspecifika språk. Parsning behandlar hur man bryter ner ett formellt språk i dess beståndsdelar för att sedan kunna tolka uttryck skrivna i språket. Domänspecifika språk är språk som är specifika för ett visst ändamål och som ofta är implementerade genom att man använder sig av ett grundspråk som utökas för ändamålet. Sättet man utökar språket på beror på språket. I fallet med Ruby så kan vi omdefiniera vad som händer när okända konstanter eller metoder hittas och därmed utöka språket. I Lisp och liknande språk så kan man manipulera uttryck som är syntaktiskt korrekta på en basal nivå så att de skrivs om till uttryck som känns igen av språkets programtolk (interpretatorn, motsvarande irb).

Att tänka på

Nu blir det allt viktigare att ni dokumentera ordentligt vad ni gjort och, när ni opponerar, att ni läser igenom vad den andra gruppen gjort. Arbetet med uppgifterna kommer att innebära att ni behöver analysera befintlig kod, felsöka, skriva tester och dokumentera era frågor ordentligt. Detta kommer utgöra grunden för vår bedömning av era inlämningar.

Uppgift 1: Domänspecifika språk

Introduktion

I denna uppgift ska ni skapa ett domänspecifikt språk för att uttrycka hur man beräknar försäkringspremien för en bilförsäkring. Ni ska först bestämma hur språket ska se ut. Det ska vara ett internt DSL i Ruby, d.v.s. den grundläggande syntaxen ska se ut som Ruby-kod. Därefter ska ni uttrycka informationen som finns i exemplet nedan i det här språket. Till sist ska ni skapa metoder för att läsa in och tolka den här informationen och på så sätt beräkna försäkringspremien för en person. Tanken är att det ska funka ungefär så här:

>> kalle=Person.new("Volvo","58435",2,"M",32) => #<Person:0x29bc890 ...> >> kalle.evaluate_policy("policy.rb") => 15.66

Exempel att utgå från

I vårt försäkringsbolag baseras premien för bilförsäkringen på ett antal olika faktorer. Dessa vägs samman och ger ett säkerhetsbetyg. Ju högre säkerhetsbetyg man får, desto lägre blir premien. I det här exemplet ska vi enbart beräkna säkerhetspoängen. Detta görs i två steg.

Först räknar vi ut en grundpoäng som baserar sig på ett antal olika tabeller. Det beror på bilmärke, var man bor (postnummer), hur många år man haft körkort, kön och ålder. Tabellerna ser ut så här:

Bilmärke Poäng
BMW 5
Citroen 4
Fiat 3
Ford 4
Mercedes 5
Nissan 4
Opel 4
Volvo 5

Postnummer Poäng
58937 9
58726 5
58647 3

Körkort antal år Poäng
0-1 år 3
2-3 år 4
4-15 år 4,5
16-99 år 5

Kön Poäng
Kvinna 1
Man 1

Ålder Poäng
18-20 år 2,5
21-23 år 3
24-26 år 3,5
27-29 år 4
30-39 år 4,5
40-64 år 5
65-70 år 4
71-99 år 3

(I just det här försäkringsbolaget hade man förut olika premier för män och kvinnor, men det har man inte längre, åtminstone inte i tabellerna.)

I nästa steg höjer eller sänker man grundpoängen baserat på speciella regler. I det här försäkringsbolaget har man två sådana regler

  • För män som haft körkort mindre än tre år multipliceras säkerhetspoängen med 0.9.
  • För personer som äger Volvo och som bor i ett postnummerområde som börjar med 58 (större delen av Östergötland) multipliceras säkerhetspoängen med 1.2.

Er uppgift är alltså att hitta på ett sätt att uttrycka tabellerna och reglerna. Ni ska forma ett eget domänspecifikt språk som dels följer grundsyntaxen i Ruby, dels är så enkelt att personalen som jobbar på försäkringsbolaget ska kunna ändra i specifikationen om det behövs.

Vad är det som ska hända?

Tanken är att när ni är klara ska ni kunna skriva ungefär så här:

>> kalle=Person.new("Volvo","58435",2,"M",32) => #<Person:0x29bc890 ...> >> kalle.evaluate_policy("policy.rb") => 15.66

I det första anropet skapar ni ett nytt objekt som innehåller information om en person. Han har en Volvo, bor i postnummerområde 58435, har haft körkort i två år, är man och 32 år. Den DSL-kod som uttrycker företagets regler finns i filen policy.rb. Metoden evaluate_policy läser in och tolkar denna kod som ett DSL, samtidigt som säkerhetspoängen räknas ut. I det här exemplet blir det 15.66.

Om ni vill får ni gärna hitta på alternativa tabeller och regler som är ungefär lika komplicerade. Tanken är att ni ska göra en så generell lösning ni hinner med.


Uppgift 2: Parsning

Ni ska använda er av en befintlig parser som ni hittar i filen rdparse.rb. Längst ner i filen hittar ni ett exempel på hur parsern kan instansieras med grammatiska regler och kod för att evaluera uttryck så att vi kan hantera ett språk för att rulla tärningar.

Er uppgift blir att implementera ett språk för enkla logiska uttryck med hjälp av den befintliga parsern. Språket har följande grammatiska specifikation:

VALID ::= ASSIGN
VALID ::= EXPR
ASSIGN ::= '(' 'set' VAR EXPR')'     <-- Tilldelning av variabel
EXPR ::= '(' 'or' EXPR EXPR ')'
EXPR ::= '(' 'and' EXPR EXPR ')'
EXPR ::= '(' 'not' EXPR ')'
EXPR ::= TERM
TERM ::= VAR                         <-- Variabel
TERM ::= 'true'
TERM ::= 'false'
VAR ska beteckna en variabel som ni själva får bestämma de syntaktiska reglerna för och VALID-reglerna beskriver vilka uttryck som ska vara tillåtna i språket. Resultatet av att läsa in ett uttryck ska motsvara det förväntade resultat ni fått om ni skrivit motsvarande uttryck direkt i Ruby. Ta gärna inspiration av hur DiceRoller-klassen fungerar för att se hur man kan implementera språket.

Sidansvarig: Pontus Haglund
Senast uppdaterad: 2024-01-15