Artikler om domenespesifike språk og språkorientert utvikling.

Log alert DSL

Saturday, January 19th, 2013
Ingen kommentarer

Jeg eksperimenterer MYE med ulike måter å lage og eksekvere programmeringsspråk på for tiden. Denne gangen har jeg gjort et proof-of-concept på å lage et lite domenespesifikt språk. Med dette språket skal en kunne definere opp regler for overvåking og trigging av alarmer basert på logfiler.

Dette er bare en kjapp blogpost hvor jeg vil dokumentere litt av hvordan jeg gjør det.

1. Språket

Dette er et veldig enkelt eksempel på hvordan en overvåkingsdefinisjon kan se ut:

Alert definition
  When line matches /\[ERROR\]/
  When next matches /NRDB/
  Trigger MEDIUM alert "NRDB seem to be down"
end

Alert definition
  When line matches /\[ERROR\]/
  Trigger CRITICAL alert "#{ line }"
end

PS: NRDB er den nasjonale databasen over alle norske telefonnummer – noe vi bruker flittig i SMS-bransjen.

DSL’en bør være grei å forstå – en alarm består av en eller flere regler, og reglene har regulære uttrykk som vil brukes til å matche mot linjene i loggfilen. Alarmdefinisjonen vil også ha en eller flere triggere som avgjør hvordan det logges. Definisjonene listes i prioritert rekkefølge, og hvis en av dem slår til skal ikke noen andre trigge en alarm på den linjen.

Her er et eksempel på en ekstremt enkel logfil som jeg har brukt til å teste med:

[DEBUG] Some stuff
[DEBUG] Some more stuff
[ERROR] in log file
  The errror is in NRDB
[ERROR] some other error
[DEBUG] foo bar quux

2. Lexing

Jeg har implementert en ganske standard lexer i Ruby basert på regulære uttrykk. Koden for Lexeren og alle de andre delene av DSL-implementasjonen kan du se her – ta en titt nå eller etter at du har les hele blogposten.

Hvis jeg bruker Lexeren til å parse eksempelloggen på denne måten:

code = File.read("logalert_1.txt")
p Lexer.new.tokenize(code)

..får jeg følgende resultat:

[ [:ALERT, "Alert"],
  [:DEFINITION, "definition"],
  [:WHEN, "When"],
  [:LINE, "line"],
  [:MATCHES, "matches"],
  [:REGEX, /\[ERROR\]/],
  [:WHEN, "When"],
  [:NEXT, "next"],
  [:MATCHES, "matches"],
  [:REGEX, /NRDB/],
  [:TRIGGER, "Trigger"],
  [:MEDIUM, "MEDIUM"],
  [:ALERT, "alert"],
  [:STRING, "Comoyo: NRDB error"],
  [:END, "end"],
  [:ALERT, "Alert"],
  [:DEFINITION, "definition"],
  [:WHEN, "When"],
  [:LINE, "line"],
  [:MATCHES, "matches"],
  [:REGEX, /\[ERROR\]/],
  [:TRIGGER, "Trigger"],
  [:CRITICAL, "CRITICAL"],
  [:ALERT, "alert"],
  [:STRING, "\#{ line }"],
  [:END, "end"]
]

3. Parsing

For å parse token-listen jeg har fått via lexingen, og bygge et abstrakt syntakstre (AST), har jeg benyttet en Ruby gem som heter Racc. Racc er en såkalt LALR(1) parser generator. LALR står for Look-Ahead, Left to Right, og denne teknikken for å bygge syntakstrær ble funnet opp på 60-tallet. Noen mere kjente slike generatorer er Yacc og GNU Bison.

Det Racc trenger er en grammar-fil. Den er også endel av det du ser her. Jeg bruker så Racc til å generere en parser-klasse i Ruby. Når jeg tester denne parseren på følgende måte:

code = File.read("logalert_1.txt")
p Parser.new.parse(code)

..får jeg følgende resultat:

#<Alerts:0x28926d8 @nodes=[
  #<Alert:0x2892798
    @when_rules=[
      #<WhenRule:0x2892900 @order=0, @regex=/\[ERROR\]/>,
      #<WhenRule:0x2892870 @order=1, @regex=/NRDB/>],
    @trigger_rules=[
      #<TriggerRule:0x2892810 @severity="MEDIUM",
        @message="Comoyo: NRDB error">]>,
  #<Alert:0x28925d0
    @when_rules=[
      #<WhenRule:0x28926a8 @order=0, @regex=/\[ERROR\]/>],
    @trigger_rules=[
      #<TriggerRule:0x2892630 @severity="CRITICAL",
        @message="\#{ line }">]>]>

4. Evaluering

Dette er (foreløpig) en ganske enkel DSL, og jeg bestemte meg for at det enkleste var å evaluere AST’en direkte. Alternativt kunne jeg definert det Martin Fowler kaller en semantisk modell i Ruby, og eksekvert denne. Jeg lager også en “runtime” som tre-nodene kan benytte når de skal evalueres.

Igjen finner du alle detaljer her – se nodes_eval.rb for evalueringskoden, og Logalert.rb for runtimen.

Når jeg så kjører Logalert fra kommandolinjen med eksempelfilen som parameter, får jeg følgende output:

MEDIUM ALERT: NRDB seem to be down
CRITICAL ALERT: [ERROR] some other error

Her er selvsagt tanken at alarmene ikke skal skrives til konsollet, men sendes til et sentralt overvåkingssystem.

Konklusjon så langt

Å lage dette her var ganske enkelt. Lexing er en smal sak, og parsingen ble også enkel når jeg brukte en parser generator. Jeg tror jeg nå har et brukbart utgangspunktet får å kunne lage en fleksibel og samtidig enkel DSL for å definere komplekse overvåkingsregler.

Øyvind programmerer babyer [Luke 17, 2012]

Monday, December 17th, 2012
Ingen kommentarer

Øyvind Fanebust (@oyvindfanebust) er et aktivt medlem av utviklermiljøet i Bergen. Han sitter blant annet i styret i NNUG, og er med og arrangerer Booster-konferansen (tidligere ROOTS).

Øyvind har en spesiell forkjærlighet for domenespesifike språk – såkalte DSL’er. I dagens blogpost demonstrerer han i detalj hvordan man ganske enkelt kan lage en DSL ved hjelp av en parserkombinator i C#.

oyvind

Hvem er du?
Systemutvikler med lidenskap for alt som har med programvareutvikling å gjøre.

Hva er jobben din?
Lead developer hos Frende Forsikring AS.

Hva kan du?
Jobber mye med webapplikasjoner .NET / C#, der jeg jobber med alt fra å kartlegge behov, lage arkitektur og utvikle alle deler av løsningen. Har også en brennende interesse for domenespesifikke språk.

Hva liker du best med yrket ditt?
Å hele tiden måtte vri hjernen for å løse nye problemer.


Parsing av domenespesifikke språk med C# og Sprache

Language shapes the way we think, and determines what we can think about. - Benjamin Lee Whorf

Jeg har siden begynnelsen av oktober vært hjemme i pappaperm, så da Torbjørn spurte om jeg hadde lyst til å fylle en luke i julekalenderen hans tenkte jeg at jeg på en eller annen måte måtte klare å vinkle posten inn på dette temaet. Siden det andre temaet jeg hadde veldig lyst til å skrive noe om var domenespesifikke språk (DSL-er) bestemte jeg meg for å lage BabyTalk; et domenespesifikt språk for å programmere babysimulatorer.

En babysimulator er en dukke som vordende foreldre kan bruke til å forberede seg på å ta vare på en baby (se for eksempel RealCare Baby). En babysimulator kan for eksempel simulere barnegråt når babyen har et behov; for eksempel når babyen er sulten, og slutte å gråte når behovet er møtt; for eksempel når babyen har fått mat.

Et domenespesifikt språk kan i følge Martin Fowler defineres på følgende måte.

et programmeringsspråk med begrenset uttrykksfullhet fokusert på et bestemt domene

Vi skal altså lage et programmeringsspråk for et spesifikt domene, nemlig babysimulatorer. Språket kan utelukkende benyttes til å programmere babysimulatorer, en kan altså ikke generere Fibonacci-sekvenser med BabyTalk. Det er dette som menes med begrenset uttrykksfullhet. For en grundig innføring i alt som har med domenespesifikke språk å gjøre anbefaler jeg Fowler sin bok Domain Specific Languages. I løpet av posten kommer jeg til å lenke til teknikkene jeg har benyttet for å lage DSL-en.

Kravene til BabyTalk er følgende:

  • En skal kunne definere et sett med tilstander babyen kan være i. Eksempler på slike tilstander er “glad”, “trøtt” eller “har full bleie”.
  • En skal kunne definere overganger mellom tilstandene der en spesifiserer hvilke hendelser som gjør at babyen går over i en annen tilstand. For eksempel skal en kunne definere en overgang fra tilstanden “trøtt” der hendelsen “blir bysset” fører til at babyen går over i tilstanden “sover”.
  • En skal kunne definere et tidsskjema over hendelser som oppstår i et gitt intervall. For eksempel må det være mulig å si at hendelsen “er søvnig” skal utløses hver 3. time eller hvert 30. minutt.
  • Et viktig krav er at det må være relativt enkelt for en person med en viss teknisk innsikt både å forstå og skrive simulatorprogrammer.

Når en skal lage en DSL vil det i de fleste tilfeller være lurt å lage en såkalt semantisk modell. En semantisk modell er ikke så skummelt som det høres ut som, i bunn og grunn dreier det seg om noe så kjedelig som en API skrevet i språket som benyttes til å parse DSL-en. I vårt tilfelle er den semantiske modellen en objektmodell i C#.

Jeg har bestemt meg for å modellere BabyTalk som en tilstandsmaskin (State Machine). For å kunne lage en tilstandsmaskin som kan konfigurasjonstyres har jeg benyttet en teknikk som Martin Fowler kaller tilpassbar modell. Objektmodellen vår definerer et skjelett for tilstandsmaskinen og blir konfigurert av data fra DSL-skriptene.

I vår semantiske modell innkapsler State klassen en tilstand samt alle hendelser som utløser overgang til andre tilstander. Metoden “AddTransition” brukes til å konfigurere modellen med hendelser og tilstander som metoden “Trigger” bruker når den skal bytte til en annen tilstand.

public class State
{
    public string Name { get; private set; }

    private readonly Dictionary<string, State> _transitions;

    public State(string name)
    {
        Name = name;
        _transitions = new Dictionary<string, State>();
    }

    public void AddTransition(Transition transition)
    {
        _transitions.Add(transition.Event, transition.State);
    }

    public State Trigger(string @event)
    {
        if(!_transitions.ContainsKey(@event))
        {
            return this;
        }
        return _transitions[@event];
    }
}

Baby klassen holder styr på hvilken tilstand babyen til en hver tid befinner seg i og har metoder for å trigge overganger mellom hendelser. Den holder også styr på tidsinnstilte hendelser ved hjelp av en “planlegger” (grensesnittet IScheduleEvents).

public class Baby
{
    private readonly IScheduleEvents _scheduler;

    public string Name { get; private set; }
    public State State { get; set; }

    public Baby(IScheduleEvents scheduler, string name)
    {
        _scheduler = scheduler;
        _scheduler.EventTriggered += Trigger;
        Name = name;
    }

    public void Trigger(string @event)
    {
        State = State.Trigger(@event);
    }

    public void ScheduleEvent(string @event, TimeSpan interval)
    {
        _scheduler.Schedule(@event, interval);
    }
}

public interface IScheduleEvents
{
    event Action<string> EventTriggered;
    void Schedule(string @event, TimeSpan interval);
}

I det følgende eksempelet definerer vi den svært klossete babyen “Bumpy”. Bumpy har to tilstander; happy og angry. I tilstanden happy definerer vi en overgang til tilstanden angry når hendelsen head_bumped inntreffer. I tilstanden angry definerer vi en overgang tilbake til happy når hendelsen comforted inntreffer. Hendelsenhead_bumped inntreffer hvert tiende minutt. Bumpy sin tilstand er i utgangspunktet happy.

var happyState = new State("happy");
var angryState = new State("angry");
happyState.AddTransition("head_bumped", angryState);
angryState.AddTransition("comforted", happyState);
var baby = new Baby(_fakeScheduler, "Bumpy") { State = happyState };
baby.ScheduleEvent("head_bumped", TimeSpan.FromMinutes(10));

Dersom vi simulerer at hendelsen head_bumped blir trigget, ser vi at babyen går over i tilstanden angry.

_fakeScheduler.SimulateTriggeringOf("head_bumped");
Assert.That(baby.State.Name, Is.EqualTo("angry"));

Når babyen får trøst, med andre ord når hendelsen comforted inntreffer, går babyen tilbake til tilstanden happy.

baby.Trigger("comforted");
Assert.That(baby.State.Name, Is.EqualTo("happy"));

Modellen vår tilfredsstiller kravene om at en skal kunne definere hendelser, overganger og lage tidskjema. Måten vi definerer babysimulatoren på når vi bruker den semantiske modellen direkte gjør det derimot ikke særlig lett å forstå intensjonen bak koden, eller å skjønne hvordan alt henger sammen.

Her er en annen måte å definere den samme babysimulatoren på:

baby Bumpy
    events
        trigger head_bumped every 10 minutes
    end

    state happy
        switch to angry when head_bumped
    end

    state angry
        switch to happy when comforted
    end
end

Definert som en DSL blir intensjonen bak koden straks mye klarere. Målet mitt er at at vi basert på denne syntaksen kan populere den semantiske modellen i eksempelet over. For å få til dette trenger vi en parser.

Å implementere en DSL

Det finnes flere alternative måter å parse en DSL på. En kan skrive en parser fra grunnen av, noe som ikke er spesielt vanskelig men krever en del repetativ kode og gjør det vanskelig å skjønne helheten. Et annet alternativ er å benytte en såkalt parser generator og få generert kode for en parser. Det finnes mange parsergeneratorer tilgjengelig og flere av de er svært kraftige. (Antlr er en mye brukt parser generator som er tilgjengelig både for Java og .NET). Problemet med parser generatorer er at de typisk krever et ekstra kompileringssteg for å generere parseren. Dette gjør at de kan være litt krøkkete å jobbe med. Et tredje alternativ er en parserkombinator og det er en slik løsning jeg har valgt å benytte i dette eksempelet.

En parserkombinator er en måte å bygge parsere på der en setter sammen stadig mer kompliserte parsere av enklere parsere. For eksempel kan en sette sammen en tekststrengparser og en tallparser til en parser som først matcher en tekststreng og så et tall. Parserkombinatorer er mye brukt i funksjonelle språk. Det mest kjente eksempelet jeg kan komme på er Parsec, en parserkombinator for Haskell (Parsec er også portet til blant annet F#).

Sprache er et parserkombinatorbibliotek som gjør det relativt enkelt å bygge parsere ved hjelp av C#. Selv om det ikke kan måle seg med tunge parser generatorer når en skal lage mer komplekse språk er det etter min mening velegnet til enkle domenespesifikke språk.

Sprache gjør det enkelt å parse strukturert tekst til en datastruktur som matcher strukturen på teksten. Det er derimot ikke fullt så lett å populere en struktur som ikke har samme hierarkiske oppbygning. Dette gjelder blant annet den semantiske modellen vår. Jeg velger derfor å bruke Sprache til å populere dataklasser som jeg deretter bruker til å fylle den semantiske modellen.

Når en skal skrive en parser synes jeg ofte at det er letterst å starte “innerst” og arbeide seg utover. Jeg begynner derfor med å definere paseren for en overgang til en ny tilstand basert på en hendelse.

switch to angry when head_bumped

Når jeg skal parse en overgang begynner jeg med å parse nøkkelordene “switch to”. I eksempelet under bruker jeg Sprache til å lage en parser som prøver å matche denne tekststrengen. Metoden “Token” gjør at parseren ignorerer eventuelle mellomrom før og etter tekststrengen.

Parse.String("switch to").Token()

Alle stikkord kan i grunn parses på akkurat samme måte så jeg lager en metode som tar inn en tekststreng og returenerer en parser.

private static Parser<IEnumerable<char>> Keyword(string keyword)
{
    return Parse.String(keyword).Token();
}

Deretter trenger vi å matche navnet på tilstanden vi skal bytte til når hendelsen oppstår. Jeg ønsker at en tilstand skal identifiseres med bokstaver eller tall og benytter derfor den innebygde parseren “LetterOrDigit”. Identifikatoren kan også inneholde understrek, så jeg benytter metoden “Or” til å kombinere “LetterOrDigit” parseren med en parser som matcher understrek. Jeg ønsker at identifikatoren skal inneholde minst ett tegn og vil gjerne ha resultatet ut som en tekststreng. Dette får jeg til ved å benytte metodene “AtLeastOnce” og “Text”. Alle identifikatorer i BabyTalk har samme struktur. Jeg lagrer derfor denne parseren i en egen variabel.

var id = Parse.LetterOrDigit.Or(Parse.Char('_')).AtLeastOnce().Text().Token()

Parseren for “when” stikkordet lages på samme måte som parseren for “switch to” stikkordet. Da er vi klare for å sette sammen alt til en parser.

var transitionStatement = Keyword("switch to")
    .Then(switchTo => id
        .Then(stateName => Keyword("when")
            .Then(when => id
                .Then(eventName => Parse.Return(
                    new TransitionElement(stateName, eventName))))));

Jeg synes ikke at denne typen nøsting gir særlig lesbar kode. Noen smarte hoder har imidlertid funnet ut at ved å implementere Linq-metodene “From” og “Select” kan en utnytte C# sitt “syntaktiske sukker” for Linq-uttrykk. På denne måten kan parseren skrives sekvensielt og blir etter min mening mye mer lesbar.

var transitionStatement = from switchTo in Keyword("switch to")
                          from state in id
                          from when in Keyword("when")
                          from @event in id
                          select new TransitionElement(state, @event);

Nå som vi er ferdige med å parse overgangen kan vi gå videre til selve tilstanden.

state happy
    ...
    ...
end

Når vi skal parse seksjonen for en tilstand bruker vi mange av de samme teknikkene som over. I tillegg til å matche stikkordet “state” og navnet på tilstanden benytter vi her transitionStatement, parseren vi laget over. Metoden “Many” gjør at en vil forsøke denne parseren mange ganger og lagre overgangene i en enumerering. Resultatet er et element som representerer en tilstand med et sett hendelseslyttere.

var stateBlock = from state in Keyword("state")
                 from name in id
                 from transitions in transitionStatement.Many()
                 from end in Keyword("end")
                 select new StateElement(name, transitions);

Tidsinnstilte hendelser er ikke mye mer kompliserte å parse.

trigger head_bumped every 10 minutes

Intervallet for tidsinnstilte hendelser kan defineres enten i minutter eller timer. Her bruker vi “Or” metoden til å matche enten “minutes” eller “hours”.

var eventTriggerStatement =
    from trigger in Keyword("trigger")
    from @event in id
    from every in Keyword("every")
    from number in Parse.Number.Text().Token()
    from timeType in (Keyword("minutes").Or(Keyword("hours"))).Text()
    select new ScheduledEventElement(@event, timeType, int.Parse(number));

Hendelsesseksjonen ser omtrent ut som tilstandsseksjonen bare enda enklere.

events
    ...
    ...
end

Her benytter vi igjen metoden “Many” til å hente ut alle tidsintstilte hendelser.

var eventsBlock = from events in Keyword("events")
                  from statements in eventTriggerStatement.Many()
                  from end in Keyword("end")
                  select statements;

Da var tiden kommet til rotseksjonen “baby”.

baby Bumpy
    ...
    ...
end

Det eneste nye her er måten jeg gjør hendelsesblokken valgfri på. Siden en parser alltid er nødt til å returnere et resultat må jeg bruke metoden “Or” til å si at dersom “eventsBlock” parseren ikke matcher, skal en tom liste returneres. Jeg har laget en enkel utvidelsesmetode for parsere som returnerer enumerables. Dermed kan jeg i stedet gjøre et kall til metoden “Optional” på eventsBlock, noe jeg synes er litt mer lesbart.

var babyBlock = from baby in Keyword("baby")
                from babyName in id
                from eventTriggers in eventsBlock.Optional()
                from states in stateBlock.Many()
                from end in Keyword("end")
                select new BabyElement(babyName, eventTriggers, states);

// ...

public static Parser<IEnumerable<T>> Optional<T>(this Parser<IEnumerable<T>> parser)
{
    return parser.Or(Parse.Return(new T[0]));
}

Det eneste som gjenstår nå er å populere den semantiske modellen med resultatet av parsingen. Koden for å gjøre dette er i hovedsak nokså grei. Vi begynner med å definere en symboltabell med alle tilstandsobjektene. Jeg benytter en Dictionary der nøkkelen er navnet på tilstanden og verdien er selve tilstandsobjektet.

var stateSymbolTable = babyElement.StateElements.ToDictionary(s => s.Name, s => new State(s.Name));

Deretter opprettes babyobjektet og starttilstanden settes til den første definerte tilstanden.

var baby = new Baby(_scheduler, babyElement.Name){    State = stateSymbolTable[babyElement.InitialStateName]};

Tidsinnstilte hendelser legges til ved å løpe igjennom alle elementene og kalle metoden “Schedule” på den semantiske modellen.

foreach (var scheduledEventElement in babyElement.ScheduledEventElements)
{
    var scheduledEvent = new ScheduledEvent(scheduledEventElement.Event,                                             scheduledEventElement.Interval);
    baby.Schedule(scheduledEvent);
}

Når vi skal opprette overgangene mellom tilstander benyttes symboltabellen til å slå opp kilde- og måltilstand.

foreach (var stateElement in babyElement.StateElements)
{
    var state = stateSymbolTable[stateElement.Name];
    foreach (var transitionElement in stateElement.TransitionElements)
    {
        var targetState = stateSymbolTable[transitionElement.StateName];
        var transition = new Transition(transitionElement.Event, targetState);
        state.AddTransition(transition);
    }
}

Da kan vi omsider teste det ferdige resultatet.

var parser = new BabyParser();
var translator = new Translator(_fakeScheduler);

var babyElement = parser.ParseBaby(File.ReadAllText("Bumpy.bt"));
var baby = translator.Translate(babyElement);

Assert.That(baby.State.Name, Is.EqualTo("happy"));
_fakeScheduler.SimulateTriggeringOf("head_bumped");
Assert.That(baby.State.Name, Is.EqualTo("angry"));
baby.Trigger("comforted");
Assert.That(baby.State.Name, Is.EqualTo("happy"));

All kildekoden jeg benytter i denne posten er tilgjengelig på github.

Jeg håper at jeg med denne posten har vist at det ikke trenger å være særlig vanskelig å lage enkle parsere og at noen kanskje har blitt inspirert til å prøve å eksperimentere litt med domenespesifikke språk. Flere poster om temaet kommer forhåpentligvis til å dukke opp med jevne mellomrom på den nye bloggen min “The Domain Linguist”.

Mer eksperimentering med PogoScript

Sunday, September 23rd, 2012
2 kommentarer

I går skrev jeg en blogpost hvor jeg blant annet introduserte programmeringsspråket PogoScript. Dette språket fortjente litt mer eksperimentering, så jeg utvidet eksempelet litt, og har tenkt å vise det her.

Poenget med PogoScript – det som skiller språket fra alt annet jeg har sett – er at det enkelt muliggjør å skrive kode som nesten er normal engelsk (eller norsk for den del). Det er skjelden jeg ønsker å gå så langt som det PogoScript tillater, men om man får behov for å skrive kode som ikke-programmerer skal forstå, og man kan gjøre det i et språk som kompilerer til JavaScript, så er PogoScript en veldig interessant mulighet.

For eksempel kan jeg se for meg at man kan skrive enkelte forretningsregler i PogoScript. Et program skrevet i for eksempel C# for .NET kan så kompilere PogoScript-koden til JavaScript og eksekvere denne med med en interpreter (f.eks. Jint). Og hey presto: Du har et veldig fleksibelt system hvor forretningsfolk kan lese og forstå deler av koden, og til og med kan endre funksjonaliteten selv ved behov.

En DSLifisering av bowling-koden

Jeg har altså gjort et lite forsøk på å vise hvordan jeg kan gjøre koden min mere leselig for ikke-utviklere vha. PogoScript. Koden du får se beregner poengsummen for en bowlingserie. Den første funksjonen heter “total score for (game)”. game er parameteret til funksjonen – et array med de ulike kastene i serien.

total score for (game) =
    result = 0
    do the following once for every frame
        result = result +
        score of next frame in (game)

    result

Les koden! “Do the following once for every frame”. “Result = result + score of next frame in game”. Dette er ganske enkelt å forstå – om man kan bowling. “score of next frame in (game)” er en annen funksjon, og den kommer her:

score of next frame in (rolls) =
    if (first throw in (rolls) got all pins down)
        take the first pin count in (rolls) +
        the next two throws in (rolls)
    else if (first two throws in (rolls) got all pins down)
        take the two first pin counts in (rolls) +
        the next throw in (rolls)
    else
        take the two first pin counts in (rolls)

Dette ser ut som en beskrivelse, som såkalt pseudokode, men det er faktisk eksekverbar kode. Er det ikke kult? En utvikler vil kanskje ikke synes det er så veldig leselig, men en som ikke kan programmering vil forhåpentligvis kunne sette pris på det.

De to funksjonene over kaller en rekke andre funksjoner, som utgjør det vi kan kalle en DSL – et domenespesifikt språk for domenet bowling. DSL-funksjonen skjuler de teknsike detaljene. Og her er de:

do the following once for every frame (block) =
    for (i = 0, i < 10, i = i + 1)
        block()

first throw in (rolls) got all pins down =
    rolls.0 == 10

first two throws in (rolls) got all pins down =
    (rolls.0 + rolls.1) == 10

take the two first pin counts in (rolls) =
    rolls.shift() + rolls.shift()

take the first pin count in (rolls) =
    rolls.shift()

the next throw in (rolls) =
    rolls.0

the next two throws in (rolls) =
    rolls.0 + rolls.1

Skulle jeg faktisk ha brukt dette her hadde jeg nok gjort meg mere flid med oppbygningen av DSL’en, men koden her illustrerer i alle fall noen av mulighetene.

Til slutt kan jeg laget en tilfeldig bowlingserie og beregne score. Selv disse to linjene er veldig leselige – jeg tror Donald Knuth (opphavsmannen til Litterær programmering) ville vært stolt.

random game = [10,8,2,5,5,7,3,10,10,7,1,10,10,10,10,8]

console.log (total score for (random game))

Resultatet av dette skriptet er tallet 213.

Event sourcing med s-expressions

Sunday, June 10th, 2012
2 kommentarer

Helt siden i september 2009 (da Greg Young var på besøk i Bergen) har jeg tenkt på event sourcing. Og i årene etter har dette blitt en mer og mer populær arkitektur blant de softwareutviklerne som er mest cutting edge.

Men jeg har ikke bygget noe med det i tiden som har gått siden da. Å treffe Greg igjen på NDC i år har derimot stimulert meg på nytt, og her følger en blogpost som forklarer hva det går i, og som implementerer et lite eksempel.

Blogposten er også en liten leksjon i programmeringsspråket Clojure. Eksempelet er en nedskalert begynnelse på et verktøy jeg kommer til å lage for eget bruk, og dette språket er ideelt for oppgaven. Men kunnskap om Clojure er absolutt ikke nødvendig for å henge med i det jeg gjør, alt du behøver er et åpent sinn.

Hva er Event Sourcing?

Event sourcing består av to steg:

  1. Man lagrer alle endringer i et system som en sekvens av hendelser.
  2. Man bygger opp igjen systemets tilstand fra disse registrerte hendelsene.

event_sourcing

Se for deg hvordan en bank overfører penger fra en konto A til en konto B. De gjør ikke som i de typiske kodeeksemplene hvor man har en databasetransaksjon, og oppdaterer saldoen på de to kontoene i én atomisk operasjon. Nei, saldoen i banken din består av en serie med hendelser:

  1. Konto opprettet, saldo kr 0,-
  2. Innskudd a kr 30 000,-
  3. Overføring fra konto X a kr 560,-
  4. Betalt regning a kr 1 499,-
  5. 1,1% renter effektuert
  6. Debetkort belastet kr 19.50
  7. osv…

Selv om den ikke står der eksplisitt er det enkelt å beregne saldoen.

På grunn av at man kan bygge opp igjen systemets tilstand fra hendelsene kan man ofte klare seg uten noen annen form for lagring av tilstand. I praksis vil man nok ofte likevel lagre data i et annet format for eksempel for rapportering, og man vil også ta “snapshots” av tilstand for å slippe å kjøre gjennom hele loggen hver gang man trenger tilstand. Men rapportene og snapshotene kan når som helst forkastes og genereres på nytt fra loggen.

Fordelene med event sourcing

Fordelene med event sourcing er mange, men det kan være et komplekst tema å diskutere. Jeg har ikke mye erfaring, så jeg vil ikke gå i dybden, men her er de viktigste punktene slik jeg ser det:

  • Event sourcing innebærer såkalt append only data storage – man behøver ALDRI oppdatere data. Dette løser flere problemer, spesielt knyttet til transaksjoner og samtidighet, og skrivehastigheten til systemet kan øke dramatisk. Skulle samtidighetskonflikter mellom endringer snike seg inn så er det også mye enklere å rydde opp i dem med en hendelseskø.
  • Et system som baserer seg på event sourcing er godt tilrettelagt for testing. Man kan simulere all mulig adferd ved å sette opp en sekvens av hendelser. Og opplever man problemer i produksjon kan feilene alltid gjenskapes ved å kjøre hendelsesloggen derfra. Dette er gull!
  • Hendelsesloggen inneholder ikke bare hvilke dataendringer som har skjedd, men eksplisitt hvorfor de skjedde. Meningen bak tilstandsendringene blir tatt vare på. Dette er noe som mangler i de fleste “vanlige” systemer i dag, og er verdifullt av mange grunner. En av dem er at man etter at systemet er i produksjon kan komme opp med nye måter å analysere bruken på, og så kjøre disse analysene på historiske hendelser.
  • Og finner man en bug i systemet; da fikser man den, ruller systemet tilbake, og alle hendelsene blir registrert på nytt med den nye koden. Feil i data vil “fikse seg selv”.

Hvorfor (og hva er) s-expressions?

For noen år siden var XML det ultimate dataformatet. Nå føles det som om JSON er blitt vårt standard format, både for lagring og for kommunikasjon mellom tjenester. Men det har lenge funnes et annet format i denne kategorien, et format som i sin tid ble utviklet for å representere både kode og data (i form av lister og trær) i programmeringsspråket LISP. Dette er såkalte symbolske uttrykk, gjerne forkortet til sexprs eller sexps.

Sexps er enkle å parse, mye mindre bråkete enn XML, og mere fleksible enn JSON. Du ser ikke mye til dette formatet utenfor Lisp-sfæren, men det er ekstremt kraftig. Hvorfor? Fordi de kan representere data og programkode på en og samme tid.

I denne blogposten skal jeg implementere en del av et todo-program. I programmet skal man kunne opprette todo-items, flytte dem rundt mellom ulike “bøtter”, markere dem som utført, slette dem, og lignende handlinger.

Hva om jeg lar todo-programmet logge alt som skjer til en fil, og jeg logger på et format som dette:

(todo-created (id t1) "Experiment with event sourcing s-expressions")
(todo-created (id t2) "Draw some good event sourcing illustrations")
(todo-created (id t3) "Blog about event sourcing")
(todo-moved   (id t1) (todo-bucket today))
(todo-moved   (id t2) (todo-bucket tomorrow))
(todo-moved   (id t3) (todo-bucket tomorrow))
(todo-done    (id t1))
(todo-created (id t4) "Create an awesome todo system")
(todo-moved   (id t4) (todo-bucket in-two-days))
(todo-created (id t5) "Rule the World!")
(todo-moved   (id t5) (todo-bucket later))
(todo-deleted (id t5) (reason "Just kidding"))

Det du ser over er kun en tekstfil. Men loggelinjene er formatert som sexps. Altså er de gyldige Lisp-uttrykk. Hva om jeg så implementerte funksjoner som het todo-created, todo-moved osv., og så kjørte loggfilen gjennom en Lisp-tolker – f.eks. Clojure?

Loggen vil da ha blitt til et program skrevet i et domenespesifikt språk som gjenskaper tilstanden som hendelsene loggen er generert ut ifra en gang før skapte.

Det kan tenkes du bør lese den forrige setningen mer enn én gang!

En implementasjon i Clojure

Så la oss skrive litt kode som er i stand til å gjøre dette. Jeg begynner med å deklarere et navnerom, og oppretter så en variabel todos (globalt i navnerommet). Denne peker til en hash / dictionary / lookup table som vil holde programmets todo-items.

(ns worklog.core)

;; Todo items are stored here
(def todos (atom {}))

Deretter begynner jeg å skrive implementasjoner for nøkkelordene i loggfilen. Jeg tar de enkleste først – dette er små makroer som transformerer symbolene de omkranser til spesifike datatyper i Clojure (henholdsvis string for id og keyword for todo-bucket). Reason, som brukes i siste linje i loggfilen, tar en streng og ikke et symbol som parameter, og returnerer den derfor bare som den er. Dette er et eksempel på en funksjon som bare eksisterer for å gjøre loggfilen mere leselig.

(defmacro id [id]
  (str id))

(defmacro todo-bucket [bucket-id]
  (keyword bucket-id))

(def reason identity)

Og så har jeg kommet til den første seriøse funksjonen, nemlig todo-created. Parametrene er en todo-id og en todo-beskrivelse. Hvis et todo-item med gitt id allerede finnes i todos kaster den et exception. Hvis ikke legger den til et nytt objekt i en default bøtte (staging) og med default tilstand (open):

(defn todo-created [id description]
  (if (@todos id)
    (throw (Exception.
             (str "Todo with id " id
                  " already exist, can't create!")))
    (swap! todos assoc id { :id     id
                            :desc   description
                            :bucket :staging
                            :state  :open })))

Før jeg implementerer de resterende tre funksjonene trenger jeg en liten makro – dette er for å unngå at jeg repeterer meg selv i koden. De tre funksjonene skal nemlig oppdatere et todo-item, så da lager jeg en makro jeg kaller update-todo! som gjør det for en gitt todo-id, et feltnavn, og en verdi.

Makroen kontrollerer at et todo-item med den gitte identifikatoren eksisterer, og kaster et exception hvis det ikke gjør det:

(defmacro update-todo! [id key val]
  `(if-let [todo# (@todos ~id)]
     (swap! todos assoc-in [~id ~key] ~val)
     (throw (Exception.
              (str "Unable to find todo " ~id)))))

OBS: Nærmere gjennomlesning fikk meg til å innse at jeg her har laget en makro helt unødvendig – dette kunne ha vært en helt vanlig funksjon. Jaja..!

Når jeg har update-todo! er det trivielt å implementere funksjonene for å flytte, markere som utført, og å slette et todo-item:

(defn todo-moved [id bucket]
  (update-todo! id :bucket bucket))

(defn todo-done [id]
  (update-todo! id :state :done))

(defn todo-deleted [id _reason-ignored]
  (update-todo! id :state :deleted))

Den siste funksjonen illustrerer også et case hvor loggfilen inneholder informasjon som jeg ikke behøver for å gjenskape programmets tilstand – nemlig årsaken til hvorfor et todo-item ble slettet.

På tide å laste event-historikken

Alt jeg nå trenger å gjøre for å laste todo-listen min er å laste logfilen og eksekvere den. Følgende funksjon gjør det (den er egentlig bare en wrapper som gir meg en funksjon med et litt mere spesifikt navn):

(defn load-events-from-file [file]
  (load-file file)
  :ok)

Hvis jeg nå fyrer opp en REPL i terminalen min og laster DSL-koden så kan jeg kjøre funksjonen og gi den stien til loggfilen min (altså filen gjengitt i toppen av bloggposten):

worklog.core=> (require 'worklog2.core :reload)
nil
worklog.core=> (load-events-from-file "./todo.log")
:ok

Å skrive ut todo-listen

load-events-from-file svarer med nøkkelordet “ok”. todos-variabelen vil nå inneholde todo-itemene mine. Vi kan se på innholdet direkte, men i stedet tar jeg meg tid til å vise deg litt kode jeg har skrevet for å presentere listen litt bedre.

Funksjonene er små og har gode navn, så se om du klarer å skjønne hva de gjør (det kommer ingen forklaring):

;; Formating functions to print todo list

(defn todo-state-to-str [t]
  (condp = (:state t)
    :done    "DONE"
    :deleted "XXXX"
    :open    "    "))

(defn todo-2-str [t]
  (str "  [" (todo-state-to-str t)  "] "
       (:desc t)))

(defn todos-for-bucket [b todos]
  (filter #(= (:bucket %) b) todos))

(defn bucket-to-str [b-id]
  (let [s (name b-id)]
    (str (.toUpperCase (subs s 0 1))
         (.toUpperCase (subs s 1)))))

(defn print-bucket [id todos]
  (println (bucket-to-str id))
  (doseq [t (todos-for-bucket id todos)]
    (println (todo-2-str t))))

(defn print-todos
  ([] (print-todos (vals @todos)))
  ([t]
    (println "--- TODO-LIST ----------------------------")
    (doall (map #(print-bucket % t)
                [:today :tomorrow :in-two-days :later]))
    'EOF))

Om jeg nå går tilbake til REPL’en og evaluerer funksjonen print-todos så får jeg en formatert utskrift av todo-listen min slik den ble etter at jeg lastet inn alle eventene fra todo.log:

worklog.core=> (print-todos)
--- TODO-LIST ----------------------------
TODAY
  [DONE] Experiment with event sourcing s-expressions
TOMORROW
  [    ] Blog about event sourcing
  [    ] Draw some good event sourcing illustrations
IN-TWO-DAYS
  [    ] Create an awesome todo system
LATER
  [XXXX] Rule the World!
EOF

Som du kan se er alle items flyttet inn i riktige bøtter, dagens todo-item er utført, og verdensherredømme er kansellert.

Konklusjon

Du har sett meg fortelle om event sourcing, som er en teknikk hvor man lagrer alle hendelser i et softwaresystem, for så å kunne gjenskape nåtilstand (eller tilstanden for et hvilket som helst tidspunkt) ved å kjøre hendelsene på nytt. Dette eliminerer mer eller mindre behovet for klassisk lagring av tilstand, og gir en rekke nye muligheter.

Jo mer jeg tenker på disse mulighetene, jo mer verdifull blir denne teknikken for meg. Jeg kan f.eks. veldig enkelt lage ulike versjoner av DSL’en / parseren som utfører ulike ting basert på hendelsene i loggen. Mulighetene rundt testing, både hva angår enkelhet og styrke, er også veldig interessante.

Se ikke bort ifra at jeg kommer til å begynne å logge i sexps-format overalt fremover. Du er herved advart! :D

En Euler DSL

Saturday, December 24th, 2011
6 kommentarer

Julekalenderen for i år avsluttes med et programmeringsspråk spesialdesignet for å løse Euler 1 oppgaven. Det har ikke noe navn, men er en såkalt DSL, et domenespesifikt språk. Og det er hjemmesnekret av undertegnede i ett av språkene jeg har brukt tidligere i kalenderen – nemlig Rebol.

For å finne og skrive ut summen av alle multipler av 3 eller 5 under 1000 kan jeg med dette språket skrive følgende:

100 alert to-string
101   summing [
102     Add multiples of 3 below 1000
103     Add multiples of 5 below 1000
104     ; Since numbers that are both multiples of 3 and 5
105     ; will have been added twice, we now have to subtract 
106     ; those numbers once..    
107     Subtract multiples of (3 * 5) below 1000
108   ]

Som du ser er språket nokså deklerativt; jeg deklarerer hva jeg trenger i summen min, ikke hvordan man finner tallene. Og det er en DSL blant annet fordi de semantiske reglene er anderledes i deklarasjonen enn i Rebol forøvrig.

Rebol ble ikke valgt tilfeldig for å lage dette språket. Rebol har nemlig veldig gode muligheter for parsing av både tekst og symboler, og er tilrettelagt for å lage “dialekter” (som de kaller det) internt i språket.

Parse

Den sentrale funksjonen i implementasjonen av DSL’en heter parse. Den tar som input en streng eller en blokk med kode som skal parses, og en blokk med reglene som gjelder for parsingen – selve definisjonen for språket. Regex blir blant annet helt overflødig i Rebol, fordi denne parse-funksjonen er så kraftig.

Ser du på den andre linjen i koden over så står det “summing”. Det er selve DSL-funksjonen jeg har laget, som utfører parsingen og returnerer en sum. Her er implementasjonen:

 40     {
 41       The SUMMING word is set in the global context as a 
 42       function that takes a block of instructions as an
 43       argument and returns the resulting sum.
 44     }
 45     set 'summing func [instructions] [
 46       result: 0
 47       parse compose instructions [any summing-rules]
 48       return result
 49     ]

Reglene

Regelsettet som sendes inn til parse er gjengitt nedenfor. Siden språket jeg har laget er ganske enkelt er reglene også enkle. Reglene er faktisk også skrevet i en DSL/dialekt av Rebol, som en context-free grammer ala Backus-Naur Form (BNF) – en standard måte å beskrive syntaks for parsere.

 51     {
 52       The parsing rules. These are the MAIN PART you have to
 53       grok in order to use parsing and dialects in Rebol.
 54     }
 55     summing-rules: [
 56       set operation [ 'Add | 'Subtract ]
 57       'multiples
 58       'of
 59       set multiplier integer!
 60       'below
 61       set below integer!
 62       (do-instruction) ; called when instruction has been parsed
 63     ]

Her beskriver jeg altså hvordan en instruksjon i språket mitt skal se ut. Først forventer jeg ett av symbolene Add og Subtract. Symbolet lagres i variabelen operation. Deretter forventer jeg symbolene multiples og of. Så følger en integer som jeg lagrer i multiplier, etterfulgt av symbolet below, og til slutt en ny integer som lagres i variabelen below.

Når en instruksjon har blitt matchet kjører jeg prosedyren do-instruction, som vil utføre den.

Utføre beregningene

Når do-instruction blir kalt er altså variablene operation, multiplier og below satt til verdiene som er parset. Jeg kan nå generere tallene og oppdatere result-variabelen som ble opprettet tidligere.

Jeg kjører først en for-løkke for å finne multiplene, som jeg legger inn i en liste jeg kaller temp. Deretter bruker jeg fold til å summere dem sammen. Til slutt legger jeg summen til eller trekker den fra result, avhengig av verdien av operation.

 65     {
 66       Uses the result of parsing an instruction to create
 67       the multiples and modify the result
 68     }
 69     do-instruction: has [temp] [
 70       temp: copy [] ; block to contain the multiples
 71       for x multiplier below - 1 multiplier [
 72         append temp x
 73       ]
 74       ; Sum all the multiples into temp..
 75       temp: fold func[acc x][ acc + x ] temp
 76       ; Finally, either add or subtract to/from result 
 77       result: either operation = 'Add [
 78         result + temp
 79       ] [
 80         result - temp
 81       ]
 82     ]

Og det var hele implementasjonen. Eller i alle fall alt som er viktig! Det som mangler er at jeg egentlig har wrappet DSL’en inn i et objekt, og så har jeg implementert fold selv, siden Rebol ikke har den funksjonen. Den komplette koden kan du se her.

Om jeg nå ønsket at DSL-instruksjonene skulle leve i sin egen fil, og brukt DSL-implementasjonen til å kjøre DSL-filen – som om DSL’en var et selvstendig språk – så hadde ikke de krevd mer enn et par linjer kode til.

Oppsummering

Jeg har forsøkt å vise hvor enkelt det kan være å implementere en intern DSL i et språk som legger tilrette for det. Rebol er kanskje det mest egnede språket jeg har vært borti, men Lisp-dialektene er heller ikke langt unna. I språk som har mere syntaks blir DSL’ene ofte ikke så elegante, fordi man ikke kan bryte syntaks-reglene i språket.

Og grunnen til at jeg velger å avslutte julekalenderen med en DSL er at jeg ønsker å påpeke at språk er noe man faktisk kan lage selv. Det eksisterer mange programmeringsspråk allerede – jeg har bare presentert 23 av dem – men av og til kan det være best å designe et helt nytt et for en spesifikk oppgave.

Jeg håper du satte pris på julekalenderen min i år, og takker nå for følget. Men jeg håper selvsagt du vil fortsette å ta turen innom bloggen min i det nye året. Om ikke lenge vil jeg poste en større oppsummering over hva jeg har snakket om i desember, hva jeg har lært underveis, og hva jeg vil ta med meg videre.

God Jul alle sammen!

Regex

Friday, December 23rd, 2011
5 kommentarer

Dagens blogpost handler ikke om et programmeringsspråk som kan brukes til generelle formål. Regulære uttrykk – også kalt regex eller regexp – er et domenespesifikt språk man kan bruke til å “matche” tekst. Og alle utviklere, uansett hvilket språk de ellers bruker, kan dra nytte av dette. Desverre behersker alt for få utviklere den enorme styrken som bor i disse uttrykkene – om dette gjelder deg har du herved mottatt min oppfordring om å gjøre noe med det!

regex

Steve Yegge, en erfaren utvikler og bråkebøtte med bakgrunn fra Amazon og Google, har følgende å si om regulære uttrykk:

“If you aren’t extremely proficient with regular expressions right now, then you should drop everything and go become proficient with them. I bet I use regular expressions on 350 days out of every year: in my editor, on the command-line, in my code — anywhere that using one would save me time or make my code clearer. Oh, how it hurts to think about all the so-called “programmers” out there who don’t know how to use regexps. Argh.” (kilde)

Kanskje du har hørt et uttrykk som sier noe sånt som: “Du har ett problem, og bestemmer deg for å løse det med regex. Nå har du to problemer.” Bakgrunnen for dette er at regex-syntaksen er svært tett og komprimert, og lett å gjøre feil om man ikke kan det godt nok eller ikke sikrer seg godt ved å teste. Men dette er ikke grunner til å la være å bruke dem. Kraften er verdt anstrengelsen!

Hva kan jeg gjøre med regex?

Et veldig vanlig bruksområde for regex er input-validering. Du kan f.eks. validere at noe er en gyldig epost-adresse, url, telefonnummer, postkode osv. Man bruker det også til søking i større mengder tekst, og ulike parsing-oppgaver, f.eks. om man ønsker å fargelegge tekst etter forskjellige regler.

De fleste programmeringsspråk har regex-støtte, men vær oppmerksom på at metodenavn og måte de brukes på vil være forskjellig fra språk til språk. Selve regex-syntaksen kan også variere noe. Denne gangen velger jeg å vise eksempler i Ruby.

Validering

Se for deg at jeg har en rekke norske telefonnumre som jeg skal validere. De skal i utgangspunktet bestå av 8 siffer, men kan også ha en landkode, og den kan være formatert på tre ulike måter.

I eksempelet nedefor looper jeg over et array med numre, validerer det med et regex, og skriver ut om det er gyldig eller ikke.

 10 inputs = [
 11   '90909090',     # Valid: normal number
 12   '4790909090',   # Valid: number with country code
 13   '+4790909090',  # Valid: country code using +
 14   '004790909090', # Valid: country code using 00
 15 
 16   '+47909090',    # Invalid: + without country code 
 17                   #          or too short number
 18   '9090909o',     # Invalid: invalid character
 19   '9090909',      # Invalid: too few digits
 20   '+4690909090',  # Invalid: wrong country code
 21   '909090909',    # Invalid: too many digits
 22   '00474790909090' # Invalid: Trying to fool the regex now
 23 ]
 24 
 25 # Note: Assuming all spaces have been removed from numbers
 26 validation_format = /^((0047)?|(\+47)?|(47)?)\d{8}$/
 27 
 28 inputs.each do |x|
 29   valid = validation_format.match x
 30   puts "#{x} is #{valid ? "valid" : "INVALID!"}"
 31 end

Kommentere regex

Som sagt kan regulære utrykk være litt kryptiske, og det er viktig å dokumentere dem godt. I Ruby har jeg muligheten til å splitte dem opp over flere linjer, og da kan jeg enkelt kommentere hver lille del.

Her ser du samme regex som i telefonnummer-eksempelet over, men med kommentarer:

 35 validation_format =
 36   /^          # Start match at beginning of input
 37   (           
 38     (0047)?   # Optional "0047"
 39    |          # or
 40     (\+47)?   # optional "+47"
 41    |          # or
 42     (47)?     # optional "47"
 43   )
 44   \d{8}       # Followed by 8 digits
 45   $           # And the end should have been reached
 46   /x

Bruk av grupper

Parantesene i regexen danner det vi kaller grupper i uttrykket. Mange regex-metoder returnerer verdien av hver enkelt gruppe når jeg matcher. Dette kan jeg benytte meg av.

I neste eksempel endrer jeg litt på telefonnummer-regexen, og kjører en ny loop over alle numrene hvor jeg bruker resultatet til å normalisere dem og skrive dem ut. Resultatet av scan-metoden inneholder fem grupper (det er fem sett med paranteser i uttrykket), men jeg er kun interessert i det siste, som jeg binder til variabelen number:

 50 # Adding a group around \d{8}
 51 validation_format = /^((0047)?|(\+47)?|(47)?)(\d{8})$/
 52 
 53 inputs.each do |x|
 54   x.scan(validation_format) do |_, _, _, _, number|
 55     puts "+47 #{number}"
 56   end
 57 end
 58 # .. prints "+47 90909090" four times ..

Merk at denne loopen kun skriver ut de fire gyldige numrene fra arrayet mitt – kodeblokken blir ikke kalt om jeg ikke får en match.

Finne alle treff

Et annet vanlig bruksområde for regulære uttrykk er å finne alle treff av et mønster i en tekst. Her parser jeg et fritekst-felt hvor brukeren kan skrive inn telefonnummeret sitt. For å gjøre den enkelt er jeg optimistisk denne gangen og forutsetter at nummeret alltid kun består av 8 siffer. Da kan jeg meget enkelt finne telefonnumrene i teksten slik som dette:

 62 text = "Nummeret mitt er 90909090! (eventuelt 80808080)"
 63 
 64 number_finder = /\d{8}/
 65 
 66 print "Numbers: "
 67 puts text.scan(number_finder).join(", ")
 68 
 69 # .. prints "Numbers: 90909090, 80808080"

Hvordan ville du gjort dette uten regex? Du måtte ha behandlet teksten som et array av karakterer, testet hver karakter om det var et siffer, og hvis du fant et siffer måtte du sjekke om de syv neste også var det før du plukket ut delstrengen basert på indeks. Grisete!

Søk og erstatt

Jeg bruker regex til å søke og erstatte tekst hele tiden – ikke i kode men når jeg editerer tekst. Men i kode er det også ofte nyttig. I dette eksempelet har jeg en tekst som inneholder et versjonsnummer. Når vi oppraderer til en ny revisjon av produktet trenger jeg å oppdatere teksten. Tenk deg at versjonsnummeret står mange steder i teksten, og kanskje i mange dokumenter – da er det ikke kjekt å måtte gjøre det manuelt.

 75 text = "Bla bla. Product version: 1.5.16. Bla bla."
 76 
 77 version_pattern = /Product version: \d+\.\d+\.\d+/
 78 
 79 text.gsub! version_pattern, "Product version: 1.5.17"
 80 
 81 puts text
 82 # .. prints "Bla bla. Product version: 1.5.17. Bla bla." 

Greit nok, men vi kan bedre. Nedenfor har jeg laget kode som øker revisjonsnummeret med 1, slik at jeg slipper å spesifisere det nye nummeret eksplisitt. Jeg bruker da en kombinasjon av gsub! som du nettopp så, og scan som jeg brukte da jeg viste deg bruk av grupper. Slik kan jeg hente ut versjonsnummeret, gjøre hvilke endringer jeg vil, og oppdatere teksten med endringene.

 86 text = "Bla bla. Product version: 1.5.16. Bla bla."
 87 
 88 version_pattern = /Product version: (\d+)\.(\d+)\.(\d+)/
 89 
 90 text.gsub!(version_pattern) do |version|
 91   version.scan(version_pattern) do |major, minor, rev|
 92     version = "Product version: #{major}.#{minor}.#{rev.to_i + 1}"
 93   end
 94   version
 95 end
 96 
 97 puts text
 98 # .. prints "Bla bla. Product version: 1.5.17. Bla bla." 

Enkel parsing av en logg

TIl slutt vil jeg vise hvordan jeg kan parse en loggfil. Denne filen har en viss struktur, men det er ikke en fastbredde-fil, og den har ikke en fast felt-separator. Dette gjør at det er vanskelig å parse den med kun substring- og split-funksjoner.

Studer regexen nøye. Den lar meg plukke ut elementene jeg er interessert i, slik at jeg kan printe ut en rapport på et litt mere vennlig format.

102 file_content = <<EOF
103 2011-12-23 04:32:56 235 [Main] This is a test
104 2011-12-23 04:32:57 311 [Dispatcher] Some stuff..
105 2011-12-23 04:33:02 1344 [Dispatcher] More stuff...
106 EOF
107 
108 pattern =
109   /^                        # beginning of line
110   (                         # Group 1:
111   \d{4}\-\d{1,2}\-\d{1,2}   #   date part
112   \s{1}                     #   a space
113   \d{1,2}:\d{1,2}:\d{1,2}   #   time part
114   )
115   \s{1}\d+\s{1}             # irrelevant thread id
116   \[(\w+)\]                 # Group 2: logging entity
117   \s                        # a space
118   (.*)                      # Group 3: the message
119   $                         # line ending
120   /x
121 
122 require 'time'
123 file_content.each_line do |line|
124   line.chomp.scan(pattern) do |time, logger, message|
125     time = Time.parse(time).strftime('%H:%M')
126     puts "At #{time} #{logger} said: '#{message}'"
127   end
128 end
129 
130 # prints:
131 # At 04:32 Main said: 'This is a test'
132 # At 04:32 Dispatcher said: 'Some stuff..'
133 # At 04:33 Dispatcher said: 'More stuff...'

Oppsummering

Dette var bare de mest grunnleggende måtene å bruke regex på. Etterhvert som dette blir et naturlig verktøy for deg vil du se flere bruksområder. Språket vil gjøre deg mere produktiv, og vil gjøre at du unngår å skrive annen parse-logikk som det er lett å introdusere bugs med.

Du finner et hav av gode regex-referanser og tutorials på nettet. Let opp dokumentasjonen som gjelder for akkurat ditt programmeringsspråk, og lær deg dette grundig. Og med regex er det veldig viktig å eksperimentere – du blir ikke god i dette kun ved å lese, det lover jeg!

Og så var det bare én dag igjen til Jul :)

PingLang del 4: En parser

Thursday, February 24th, 2011
Ingen kommentarer

Dette er del 4 i en dokumentarserie hvor jeg går gjennom hvordan man implementerer et såkalt domenespesifikt språk (DSL). Vil du se språket i aksjon kan du ta en titt på teaseren.

I del 1 ble du introdusert for mitt nye språk PingLang. I del 2 utviklet jeg en lexer, som tok som innput en PingLang-fil og returnerte en liste med token-objekter. Og i del 3 tok vi et lite steg tilbake og du fikk se en formell definisjon av syntaksen i PingLang. Nå er det på tide å implementere parseren, som tar token-listen fra del 2 og produserer det vi kaller for et abstrakt syntakstre (AST).

parser

Formålet med en parser

En parser har to oppgaver. For det første skal den validere koden den parser, og melde fra om eventuelle syntaks-feil. Om syntaksen er ok må parseren i tillegg produsere en representasjon av koden som det er lett å jobbe videre med. Denne strukturen kalles et abstrakt syntakstre (AST). Studer illustrasjonen ovenfor for å forstå hva en AST inneholder, det sier mer enn jeg kan gjøre med tusen ord.

Selve implementasjonen av AST er en veldig enkel, rekursiv klasse som inneholder en referanse til den tokenen den representerer, samt en liste med undernoder.

10     public class AST
11     {
12         public Token Token {get; private set;}
13         public List<AST> Children {get; private set;}
14 
15         public AST(Token token)
16         {
17             Token = token;
18             Children = new List<AST>();
19         }
20     }

Parser-implementasjonen er basert på grammaren

Parseren er en såkalt Recursive Descent Parser. Implementasjonen har jeg delt i to: En baseklasse som inneholder generelle metoder som kan gjenbrukes i andre parsere, og en konkret PingLang-parser. Baseklassen har jeg ikke gjengitt her, men du kan ta en titt på den på GitHub om du vil.

Den konkrete parseren er mer interessant. Den inneholder metoder som direkte mapper mot reglene fra grammar’en i del 3. Ta for eksempel regelen som sier at et PingLang-program består av null eller flere actors, muligens etterfulgt av det jeg kalte for en T (ett eller flere linjeskift eller semikolon), og avsluttet med et EOF-token.

Denne regelen er implementert i parseren i form av metoden Program:

 11         /// <summary>
 12         /// program      : actor* T? EOF                         ;
 13         /// </summary>
 14         protected override void Program()
 15         {
 16             SetRoot(new Token(Tokens.PROGRAM, ""));
 17 
 18             UntilToken(Tokens.EOF, () =>
 19             {
 20                 Actor();
 21                 Consume_T();
 22             });
 23         }

SetRoot og UntilToken er hjelpemetoder fra baseklassen, men det som skjer her er at AST’ens rot-node settes til å være et PROGRAM-token, og så looper jeg over det jeg forventer å være actors inntil jeg finner en EOF-token.

La oss ta en regel til; her er koden for actorBody, og regelen er dokumentert i BNF-form i metodens kommentar:

 41         /// <summary>
 42         /// actorBody    : T? (listenStmt|countStmt|whenBlock)* . ; 
 43         /// </summary>
 44         private void ActorBody()
 45         {
 46             Consume_T();
 47             DoUntilToken(Tokens.ACTOR_END, () =>
 48             {
 49                 PreserveCurrentNode(() =>
 50                 {
 51                     switch (CurrentToken.Type)
 52                     {
 53                         case Tokens.LISTEN: ListenStmt(); break;
 54                         case Tokens.COUNT: CountStmt(); break;
 55                         case Tokens.WHEN: WhenBlock(); break;
 56                         default: ThrowParseException("Unexpected actor body"); break;
 57                     }
 58                 });
 59             });
 60         }

Det er ikke nødvendig at du skjønner detaljene her før du får behov for å implementere din egen parser. Min bruk av lambda-uttrykk bør gjøre koden leser-vennlig, men detaljene er godt skjult. Prinsippet er at enhver regel fra grammaren har en tilsvarende metode i parseren. Dette er den beste måten å implementere en ryddig parser på.

Legg også merke til at ActorBody vil validere input i form av at den vil kaste en exception om den støter på en token-type den ikke forventer.

La oss se på en av undernodene til ActorBody også, f.eks. CountStmt:

 72         /// <summary>
 73         /// countStmt    : COUNT INT unit T?                     ;
 74         /// </summary>
 75         private void CountStmt()
 76         {
 77             AddCurrentTokenAndSetAsCurrentNode(Tokens.COUNT);
 78             AddCurrentToken(Tokens.INT);
 79             Unit();
 80             Consume_T();
 81         }

CountStmt består av to eller tre terminals (angitt med store bokstaver) og én underregel (unit). La oss dykke ned i implementasjonen av unit også:

 83         /// <summary>
 84         /// unit         : 'second'|'seconds'                    ;
 85         /// </summary>
 86         private void Unit()
 87         {
 88             if (IsCurrentOneOf("second", "seconds"))
 89                 AddCurrentToken(Tokens.ID);
 90             else
 91                 ThrowParseException("Unexpected unit literal");
 92         }

Her ser du hvordan parseren validerer at en av de gyldige beskrivelsene av en unit er brukt.

Jeg håper dette var en ok smakebit på en parserimplementasjon. Jeg har ikke forklart særlig nøye hvordan parseren fungerer, men en slik beskrivelse kan du finne mange andre steder. Og velger du å benytte en parser generator vil du ikke måtte implementere din egen engang – den vil da bare basere seg på grammaren du definerer.

Det viktige er at parseren validerer kildekoden, og at den produserer et tre som vi kan jobbe videre med når vi skal kjøre programmet.

Den fullstendige parseren kan du se her.

Testing av parseren

For ordens skyld vil jeg ta med et par tester også. En parser er svært godt egnet for enhetstesting, og jeg ville aldri forsøkt å implementere en uten.

I det første eksempelet kontrollerer jeg at parseren klarer å finne en syntaks-feil i en liten PingLang-kodesnutt.

 78         [Test]
 79         public void Test_unexpected_event()
 80         {
 81             typeof(Exception).ShouldBeThrownBy(() =>
 82             {
 83                 var parser = new Parser(new Lexer(Tokens.All));
 84                 parser.ConstructTree(
 85                     @"Pinger
 86                             listen on port 9876
 87                             when earthquake print ""Foo""."
 88                     );
 89             },
 90             ex => ex.Message.Equals("Unexpected event spec <ID,\"earthquake\">"));
 91         }

Den neste testen parser et gyldig PingLang-program, og her kontrollerer jeg den resulterende AST’en.

14         [Test]
15         public void Tiny()
16         {
17             var parser = new Parser(new Lexer(Tokens.All));
18             parser.ConstructTree(
19                 @"Ponger when pinged print ""Pong""."
20                 );
21             parser.AST.Children.Count.ShouldEqual(1);
22             var actor = parser.AST.Children[0];
23             actor.Token.Type.ShouldEqual(Tokens.ID);
24             actor.Token.Text.ShouldEqual("Ponger");
25 
26             var eventHandler = actor.Children[0];
27             eventHandler.Token.Type.ShouldEqual(Tokens.WHEN);
28             eventHandler.Children[0].Token.Type.ShouldEqual(Tokens.PINGED);
29             eventHandler.Children[1].Token.Type.ShouldEqual(Tokens.PRINT);
30             eventHandler.Children[1].Children[0].Token.Text.ShouldEqual("\"Pong\"");
31         }

Parseren er nå ferdig, men foreløpig har vi kun brydd oss med syntaksen i PingLang, og ikke snakket så mye om semantikk – dvs. hva syntaksen betyr. Videre i denne serien vil jeg fortelle deg hva en semantisk modell er for noe, hvordan vi kan lage en slik basert på AST’en, og hvordan den kan brukes til å utføre programmererens intensjon.

PingLang del 3: Litt gramatikk

Thursday, February 17th, 2011
Ingen kommentarer

Før jeg fortsetter implementasjonen av PingLang er det på tide å snakke litt om grammars – dvs. hvordan man kan beskrive syntaksen til et programmeringsspråk på en presis og formell måte. Dette er ikke noe du gjøre for å kunne implementere et språk. For enkle språk eksisterer gjerne syntaks-reglene i hodet på utvikleren, og det kan fungere helt greit. Men en grammar kan fungere som en slags pseudo-kode, og kan gjøre det enklere å implementere språket.

For å spesifisere en grammar bruker man en notasjon som kalles Backus-Naur Form (BNF). Backus (som jeg skrev om i julekalenderen, luke 13) utviklet BNF for å spesifisere programmeringsspråket ALGOL, og hans notasjon har siden blitt en standard for å spesifisere syntaks.

Om man ønsker å bruke en såkalt parser generator for gjøre implementasjonen av et språk/DSL enklere, vil man som regel bruke BNF som et språk for å drive genereringen. Jeg gjør ikke det for PingLang, men BNF-reglene jeg skriver her vil som sagt hjelpe meg mye når jeg programmerer parseren for hånd.

Terminals

Så la oss ta en titt på grammar’en til PingLang. Om vi begynner forsiktig så har vi først har vi en liste med såkalte terminals som i praksis utgjør alle nøkkelordene i PingLang.

LISTEN       : 'listen on port'                      ;
COUNT        : 'count every'                         ;
STARTING     : 'starting'                            ;
ERROR        : 'error'                               ;
PINGED       : 'pinged'                              ;
MESSAGE      : 'message'                             ;
COUNTER      : 'counter'                             ;
GT           : '>'                                   ;
PRINT        : 'print'                               ;
PING         : 'ping'                                ;
RESET        : 'reset counter'                       ;
WAIT         : 'wait'                                ;
SEND         : 'send'                                ;
TO_PORT      : 'to port'                             ;

Hvis du mener du har sett denne listen før så har du helt rett. Dette er nemlig en del av listen over mulige tokens som jeg implementerte da jeg lagde lexeren i PingLang del 2. Men det mangler noen terminals – dette var kun de som består av konstante tekststrenger. I tillegg har vi disse:

ID           : ('a'..'z'|'A'..'Z')+ (\w|\-)*         ;
STRING       : '\"' [any char not newline]* '\"'     ;
INT          : '0'..'9'+                             ;
T            : (\r|\n|;)+                            ;

Her sier jeg f.eks. at en identifikator (ID) i PingLang består av en bokstav mellom a og z, etterfulgt av et vilkårlig antall bokstaver, tall eller bindestreker. Jeg sier at en streng er alt mellom to hermetegn, sålenge det ikke er et linjeskift. Et tall (INT) er ett eller flere siffer.

Den litt spesielle terminatoren T har jeg definert til å være en eller flere linjeskift og/eller semikolon. Dette vil gjøre det litt vanskeligere å parse koden, men vil gi oss litt mere fleksibilitet når vi skal kode i PingLang. Du vil se igjen T’en i mange regler nedenfor, men forsøk å ikke henge deg for mye opp i det. De er der for å sørge for at programmereren kan legge inn et vilkårlig antall linjeskift der hvor det er naturlig.

Videre har vi et par terminals som vil bli forkastet av av lexeren, nemlig whitespace (WS) og kommentarer (COMMENT). En kommentar er noe som begynner med enten to backslasher eller et hash-symbol (både C og Ruby-stil tillat altså), og som fortsetter til første linjeskift.

WS           : ( |\t)+                               ;
COMMENT      : (\\\\|#) [any char not newline]*      ;

Jeg håper du ser hvordan disse reglene begynner å gi språket vårt form. Nå følger de reglene som ikke er terminals. Disse er det ikke lexeren som er ansvarlig for – de vil bli brukt til å implementere resten av parseren (i neste del i serien).

Sammensatte regler

Her er den aller første regelen, som definerer hvordan et PingLang-program ser ut:

program      : actor* T? EOF                         ;

Et program er altså null eller flere actors, muligens etterfulgt av en T (definert tidligere som en eller flere linjeskift og/eller semikolon), etterfulgt av EOF (end of file). Vi trenger så å vite hva en actor er:

actor        : T? ID actorBody                       ;

En actor består muligens av en T, etterfulgt av en ID (navnet på actoren), etterfulgt av en actorBody. Vi går videre til actorBody:

actorBody    : T? (listenStmt|countStmt|whenBlock)* . ;

En actorBody består igjen muligens av en T, etterfulgt av null eller flere av tre ulike elementer: en listen-definisjon, en count-definisjon, eller en when-block. Til slutt avsluttes actorBody av et punktum.

Vi må fortsette å grave oss ned i reglene til vi kommer til terminals. Listen-definisjonen (listenStmt) er et eksempel på en slik regel:

listenStmt   : LISTEN INT T?                         ;

Den begynner med en LISTEN terminal, som vi tidligere har definert til å være strenger “listen on port”. Deretter kommer et tall (INT), muligens etterfulgt av en T.

Og slik fortsetter vi til vi har definert hele språket..

countStmt    : COUNT INT unit T?                     ;
unit         : 'second'|'seconds'                    ;

whenBlock    : WHEN eventSpec eventBody              ;
eventSpec    : STARTING
             | ERROR
             | PINGED
             | MESSAGE
             | COUNTER GT INT                        ;
eventBody    : action
             | action T
             | T+ (action T)+ END T+                 ;

action       : PRINT printArgs
             | PING INT
             | RESET
             | WAIT INT unit
             | SEND STRING TO_PORT INT               ;

printArgs    : (INT|STRING|MESSAGE|COUNTER)*         ;

Og det var det hele. Ut fra denne BNF-definisjonen kan du utlede hvordan alle gyldige PingLang-programmer kan se ut. Reglene sier ingen ting om hva de ulike tingene betyr – altså semantikken i språket – kun hva som er tillat syntaks.

Jeg står over å forklare i detalj hvordan BNF fungerer, men håper du forstod det grunnleggende. Disse reglene vil vi ta med oss videre i neste artikkel hvor jeg skal implementere PingLang-parseren. Er du interessert i å se flere grammars kan du ta en titt på Grammar Downloads, hvor du finner et hav av eksempler.

PingLang del 2: En Lexer

Friday, February 4th, 2011
Ingen kommentarer

I del 1 ble du introdusert for det nye språket PingLang. Nå som vi har en ide om hvordan det skal se ut og hva det skal kunne gjøre så kan vi så smått begynne å implementere. Alle eksterne DSLer og andre programmeringsspråk må parses, det vil si at kildekoden til et program må analyseres for å finne struktur og mening. Og det aller første steget når man parser kode kalles leksikalsk analyse.

Leksikalsk analyse går ut på å sette sammen enkelttegn i kildekoden til ord som har betydning i språket. Til å gjøre dette implementerer vi det som ofte kalles en Lexer, men som også går under navnene Scanner og Tokenizer. Resultatet er en liste med tokens.

lexer

I dette prosjektet implementerer jeg min egen lexer, og det anbefales gjerne for alle som har lyst til å begynne med språkutvikling. Når man forstår grunnprinsippene har man derimot muligheten til å bruke verktøy som gjør mesteparten av jobben for en – såkalte parsergeneratorer. I gode gamledager brukte man ofte to programmer som heter henholdsvis Lex og Yacc, hvor Lex genererte en Lexer. I dag finnes det mange andre verktøy, og ett jeg har sett litt på er ANTLR. Men det er ikke vanskelig å implementere leksing og parsing selv, og det gir også størst kontroll.

Implementasjon

(PS: Steder i kildekoden hvor jeg bruker mye backslash og hermetegn kan bli litt rare pga. måten min fantastiske blogmotor parser teksten. I bunn av denne artikkelen finner du linker til alle filene, slik at du kan se dem i all sin prakt på Github.)

Jeg har valgt å bruke et pattern som heter Regex Table Lexer. Det er en helt generell lexer som tar som innput teksten/kildekoden som skal skannes og en tabell/liste med regulære uttrykk og token-typer. Token er en veldig enkel klasse med bare to properties: Type (jeg har valgt å bruke en int for å spesifisere type) og Text (som er en streng).

 9     public class Token
10     {
11         public Token(int type, string text)
12         {
13             Text = text;
14             Type = type;
15         }
16         public int Type { get; set; }
17         public string Text { get; set; }
18 
19         public override string ToString()
20         {
21             return string.Format("<{0},\"{1}\">",
22                 Tokens.TokenNames[Type],
23                 Text.Replace("\n", "\\n").Replace("\r", "\\r"));
24         }
25     }

En liste med Tokens er altså resultatet av den leksikalske analusen. Lexeren kommer også til å bruke noe jeg har kalt en TokenRecognizer, som er en mapping mellom en token-type og et regulært uttrykk. I tillegg kan jeg spesifisere om tokenet skal inkluderes i resultatet eller ikke ved å sette output-parameteren. Typiske tokens som ikke skal inkluderes kan være blanke tegn og kommentarer.

 9     public class TokenRecognizer
10     {
11         public TokenRecognizer(int tokenType, string regexPattern, bool output)
12         {
13             TokenType = tokenType;
14             Output = output;
15             Pattern = new Regex(regexPattern, RegexOptions.Compiled);
16 
17         }
18         public Regex Pattern { get; private set; }
19         public bool Output { get; private set; }
20         public int TokenType { get; private set; }
21     }

Og da er vi klare for å ta en titt på selve lexeren..

10     public class Lexer
11     {
12         private readonly IEnumerable<TokenRecognizer> _recognizers;
13         private string _inputBuffer;
14 
15         public Lexer(IEnumerable<TokenRecognizer> recognizers)
16         {
17             _recognizers = recognizers;
18         }
19 
20         // Will contain the result of the scan
21         public List<Token> Tokens { get; private set; }
22 
23         public void Tokenize(string input)
24         {
25             _inputBuffer = input;
26             Tokens = new List<Token>();
27 
28             while (MatchToken()); // Loop until MatchToken returns false
29 
30             // End the token stream with a special End Of File token
31             Tokens.Add(new Token(PingLang.Core.Lexing.Tokens.EOF, ""));
32         }
33 
34         private bool MatchToken()
35         {
36             foreach (var recognizer in _recognizers)
37             {
38                 var match = recognizer.Pattern.Match(_inputBuffer);
39                 if (match.Success)
40                 {
41                     if (recognizer.Output)
42                         Tokens.Add(new Token(recognizer.TokenType, match.Value));
43 
44                     // Consume the matched token from input
45                     _inputBuffer = _inputBuffer.Substring(match.Length);
46                     return true;
47                 }
48             }
49             return false;
50         }
51     }

Man oppretter Lexeren ved å sende inn en liste med TokenRecognizers. Man kaller så metoden Tokenize, og sender inn kildekoden som argument. Tokenize vil kalle metoden MatchToken inntil denne returnerer false, fordi den ikke klarer å matche flere tokens i input. Jeg legger så til et avsluttende EOF (end of file) token.

Det er verdt å merke seg at MatchToken vil loope over alle recognizerne, og vil godta det første treffet det finner. Det vil si at recognizernes rekkefølge har en betydning. Har man f.eks. et uttrykk som skal matche strengen “>>” og et annet som skal matche “>”, så er det avgjørende at det første kommer først.

Regex Table Lexer er som sagt en generell metode som kan brukes til å analysere mange språk. Men vær oppmerksom på at det er enkelte syntakser algoritmen ikke egner seg like godt for. Python f.eks., hvor innrykk har betydning for strukturen, lexes best på andre måter.

PingLang tokens

Vi står nå igjen med definisjonen av de spesifike token-typene for PingLang-språket, og recognizerne som Lexeren vil bruke. Det er på sin plass å advare deg om at koden for dette ikke er spesielt fin å se på. Den er ikke spesielt objektorientert, og inneholder mye repitisjon. Den fungerer derimot bra, og jeg bruker et lite triks som gjør at jeg ikke blir gal av det du skal se nå. Selve koden ser du på Github: Tokens.cs.

For å unngå problemet med repitisjon hadde det vært ideelt å hatt makroer i C#, slik at jeg kunne generert koden i filen Tokens.cs. C# har ikke makroer, men vi har et lite brukt verktøy som heter Text Template Transformation Toolkit (T4). Hvis jeg legger til en fil i prosjektet mitt som jeg kaller Tokens.tt, vil denne generere en Tokens.cs fil hver gang jeg lagrer. Og denne teknikken fungerte fint i dette tilfellet. Her er et lite utdrag fra tt-filen min, som er selve spesifikasjonen av token-typene i PingLang.

 37   TokenInfo[] tokens = new TokenInfo[]{
 38     new TokenInfo { token = "EOF" },
 39     new TokenInfo { token = "WS",         regex = "^([ \\t])+", dont_include = true },
 40     new TokenInfo { token = "T" ,         regex = "^[\r\n;]+"},
 41     new TokenInfo { token = "ACTOR_END" , regex = "^\\."},
 42     new TokenInfo { token = "LISTEN" ,    regex = "^listen on port"},
 43     new TokenInfo { token = "WHEN" ,      regex = "^when"},
 44     new TokenInfo { token = "MESSAGE" ,   regex = "^message"},
 45     new TokenInfo { token = "PRINT" ,     regex = "^print"},
 46     new TokenInfo { token = "PINGED" ,    regex = "^pinged"},
 47     new TokenInfo { token = "STARTING" ,  regex = "^starting"},
 48     new TokenInfo { token = "PING" ,      regex = "^ping"},
 49     new TokenInfo { token = "WAIT" ,      regex = "^wait"},
 50     new TokenInfo { token = "SEND" ,      regex = "^send"},
 51     new TokenInfo { token = "TO_PORT" ,   regex = "^to port"},
 52     new TokenInfo { token = "ERROR" ,     regex = "^error"},
 53     new TokenInfo { token = "COUNT" ,     regex = "^count every"},
 54     new TokenInfo { token = "RESET" ,     regex = "^reset counter"},
 55     new TokenInfo { token = "COUNTER" ,   regex = "^counter"},
 56     new TokenInfo { token = "END" ,       regex = "^end"},
 57     new TokenInfo { token = "GT" ,        regex = "^\\>"},
 58     new TokenInfo { token = "ID" ,        regex = "^([a-zA-Z])+([\\w\\-])*"},
 59     new TokenInfo { token = "COMMENT" ,   regex = "^[(\\/\\/)#][^\\r\\n]*", dont_include = true },
 60     new TokenInfo { token = "INT" ,       regex = "^(\\d)+"},
 61     new TokenInfo { token = "STRING" ,    regex = "^\"[^\\r\\n\"]*\""},
 62     new TokenInfo { token = "PROGRAM" }
 63   };

Og da er egentlig lexeren ferdig. Vil du se flere detaljer, og kanskje bruke dette som utgangspunkt for din egen lexer, kan du ta en titt på github her, hvor du finner alle filene.

Til slutt to ord om testing. En lexer egner seg utmerket til enhetstester, og det anbefales på det sterkeste. I blogposten om strømlinjeformede enhetstester inkluderte jeg en test av en lexer. For den faktiske testkoden av PingLang-lexeren kan du se her.

Opprummering

I del 2 har vi tatt første steg for å implementere PingLang, som var å lage en Lexer. Man må ikke gjøre dette for hånd – det finnes mange verktøy som kan hjelpe deg – men det er ikke vanskelig å gjøre selv heller. Lexeren tar tekst skrevet i språket vi lager, og returnerer en liste med ord eller byggestener, såkalte tokens, som vi kan jobbe videre med. Denne listen vil gjøre det enklere for oss å analysere den faktiske meningen i koden. I de neste artiklene i serien vil vi gjøre nettopp dette.

PingLang del 1: La oss bygge et språk

Thursday, February 3rd, 2011
5 kommentarer

I denne dokumentarserien vil vi bygge et programmeringsspråk sammen. Det blir kun et lite språk, et såkalt domenespesifikt språk (DSL). Teknikkene du får se er derimot generelle, og kan brukes til å implementere mange, ulike DSLer, og også generelle programmeringsspråk om du ønsker det.

I fjor høst kjørte jeg en annen artikkelserie på denne bloggen som het Ping Ring. Her løste jeg en passe enkel programmeringsoppgave i en rekke forskjellige språk, for å studere ulikhetene, og for å se om noen av språkene var bedre egnet enn andre.

Nå vil jeg utvikle et nytt språk vi vil kalle PingLang, som er spesialdesignet for å løse Ping Ring og lignende oppgaver. Du har sett teaseren allerede, og her er den offisielle verdenspremiæren:

helloworld

Det du ser over er verdens første PingLang-program. Du har aldri sett noe annet skrevet i dette språket, men det er likevel ganske enkelt å forstå, ikke sant? Når HelloWorld starter vil det først printe ut “Hello, world”, deretter vente i fem sekunder, før det printer ut “Take care now!”.

Målet mitt med PingLang er å designe et språk som på enkelte punkter skiller seg fra alle andre programmeringsspråk jeg har vært borti. Jeg ønsker også at det skiller seg fra typiske eksempler på DSL’er. For å løse selve Ping Ring-oppgaven er det selvsagt overkill å implementere et språk som dette, men målet er å lære noe, og det tror jeg at oppgaven legger til rette for.

Merk også at PingLang er implementert som en ekstern DSL. Jeg har implementert flere interne DSLer tidligere, men da snakker vi om å utnytte fleksibiliteten i et eksisterende språk til å skape et slags API hvor man bedre kan uttrykke mening for et bestemt domene. En ekstern DSL er i mye større grad et fullverdig språk, for å si det sånn. Mer om dette siden..

Syntaks

La oss ta en litt grundigere titt på syntaksen jeg har kommet opp med:

syntaxstructure

PingLang er ikke et vanlig objektorientert språk. Det følger det som heter Actor Model (de som kjenner meg vet at jeg er fasinert av implementasjonen av Actor Model i Erlang). Et PingLang-program inneholder en eller flere actors. En actor begynner med et navn, og avsluttes med et punktum.

Deretter følger null eller flere linjer med opsjoner for actoren. HelloWorld hadde ingen av disse.

Så følger en eller flere eventhandlere, som definerer kodeblokker som skal utføres når diverse events intreffer. HelloWorld-programmet hadde én av disse som håndterte starting-eventet.

Syntaksen er litt mer fleksibel enn det vi er vandt til fra vanlige programmeringsspråk. F.eks. kan en enkel actor med bare én eventhandler og bare en handling skives på én linje slik som dette:

minimalactor

Hva du kan forvente deg videre

Gjennom denne dokumentaren vil du se meg implementere en sexy editor for det nye språket, med innebygget fargelegging av syntakselementer og en enkel intellisense. Du vil få se hvordan jeg implementerer en parser for PingLang, hvordan jeg omformer kildekoden til et abstrakt syntakstre, og hvordan jeg til slutt implementerer en tolker (interpreter) som vil kjøre programmene skrevet i språket.

Implementasjonen av alle komponentene vil bli gjort i C#, og PingLang kjører på .NET-plattformen. Jeg vil vise endel av koden, men ikke alt. Den komplette implementasjonen vil være tilgjengelig på Github, slik at du både kan lese den der, og laste den ned og kjøre den selv om du ønsker det.

I stedet for å diskutere alle detaljene i implementasjonen, vil jeg bruke artiklene til å snakke litt rundt de ulike elementene man må få på plass når man utvikler et språk, hvilke valg jeg har tatt, og hvilke andre valg man har. Det finnes mange veier til målet, og PingLang vil bare representere én slik vei.

Min egen motivasjon for serien er å lære mer om parser-teknologi, eksperimentere litt med syntaks, samt se litt på hvordan man kan implementere et actor model-rammeverk. Jeg håper du blir med meg på ferden!

Siste kommentarer

Torbjørn
PS: Takk til Børge Hansen, som delte SCARF-modellen med meg!...
Børge Hansen
Denne likte jeg veldig godt. Du skriver godt og har gode betraktninger  Keep it up – flere trenger å tørre å lære mer om ledelse – du l...
Tormod
Er egentlig ikke overrasket. F# sin fortè er programmererens produktivitet/kvalitet og anledning til parallell kjøring. Men kjøremotoren har ...
Stian
Ville også prøvd med et større problem (x100 eller x1000 f.eks). Når man snakker så små brøkdeler av et sekund som her så kan tiden for en ell...
Torbjørn
Har ikke sjekket - tar en titt i morgen hvis tid :)...
Einar W. Høst
Mhp tco: hva sier ILSpy?...
Torbjørn
Har ikke sett noe på PSeq før, men kjenner til den typen funksjoner fra blant annet Clojure. Og problemet med slike funksjoner i sammenhenger som de...
Håvard
Veldig bra sammenligning! Har du sett på ytelsen av PSeq.* fra powerpakken? Tipper den vil gi performancehit på små mengder, men kan kanskje resul...
Torbjørn
Jeg kom på en demonstrasjon-variant til jeg burde inkludere, nemlig bruk av list comprehension (en type computation expression (også kalt monads)). ...
Einar W. Høst
Interessant, det blir en trade-off mellom eleganse og fart på en måte. Den funksjonelle løsningen med vanlig filter er ren og pen, mens den imperat...
Creative Commons-lisens
Innholdet på denne bloggen er tilgjengelig under Creative Commons Navngivelse-Ikkekommersiell-DelPåSammeVilkår 3.0 Norge lisens.

Programmeringsbloggen
Kjempekjekt.com

© 2006-2013 Torbjørn Marø

Jeg har vært en profesjonell programmerer siden 1999, og dette er min blogg. Målet med bloggen er å stimulere meg selv og alle andre til kontinuerlig eksperimentering og læring.

Jeg forsøker å være allsidig, og programmerer blant annet i C#, Ruby, Erlang og Clojure.

Jeg praktiserer TDD og andre smidige utviklingspraksiser. Jeg er opptatt av kvalitet og ren kode.

Dette og ganske mye mer kan du lese om på denne bloggen!