Jeg rotet meg litt bort i programmeringspråket Ruby for noen år siden, og har lovet meg selv å komme tilbake til det nå når IronRuby blir modent på .NET plattformen. Her kan du lese det jeg har skevet om dette..

Telle ord-forekomster med F#

Thursday, March 28th, 2013
11 kommentarer

I dagens blogpost går jeg gjennom litt F#-kode jeg skrev for en konkret oppgave på jobben. Oppgaven ble først løst i Ruby, men jeg trengte bedre ytelse, og kodet det derfor etterpå i F#. Er du en F#-nybegynner kan det godt tenkes du kan plukke med deg flere tips her. Jeg vil forsøke å trekke frem noen av de mer avanserte elementene i koden.

Oppgaven

Hin dagen lagde jeg et lite script for å telle opp antall forekomster av ord i en tekstfil – altså gruppere resultatet på hvert ord i filen (“bil” forekom 17 ganger, “båt” forekom 5 ganger osv.). Dette ville jeg gjøre for å kunne lage en ord-sky basert på tekstfilen. Her er for eksempel en ord-sky over de ti siste postene på bloggen min:

BLOG_RSS

Det kan se ut som om jeg er litt selvopptatt :)

Her er en beskrivelse av scriptet:

  1. Ta inn stien til en fil med tekst som første argument.
  2. Les linje for line, og splitt linjen opp i ord.
  3. Fjern punktum, komma, utropstegn eller kolon hvis det er siste tegn i ordet.
  4. Se bort fra ordet hvis det er ett av ordene i en svarteliste.
  5. Inkrementer en teller for antall forekomster av hvert ord.
  6. Etter at alle ordene er registrert med antall, fjern ord som ikke forekomer mer enn X ganger.
  7. Skriv ut resultatet til en fil, med ett ord og antallet pr linje.

Resultatfilen kan jeg så bruke som innput til Wordle og få generert en ord-sky. Ganske enkelt egentlig, og jeg laget raskt et script for dette i Ruby. Er du interessert finner du scriptet her.

Trengte bedre ytelse

Som jeg sa innledningsvis ønsket jeg bedre ytelse enn hva Ruby kunne gi meg. Filen jeg trengte å analysere var på over 50 MB og bestod av 500.000 linjer. Den inneholdt ca 518.000 unike ord, og over 6 millioner ord totalt. Jeg jobber jo med SMS, og dette var faktisk en halv million SMS-meldinger som skulle analyseres.

På min superduper-spekkede laptop brukte Ruby-scriptet mitt 88 sekunder på denne filen, og konsumerte 185 MB minne. Dette var med Ruby versjon 1.9.3. Til sammenligning bruker F#-programmet jeg nå skal lage under 16 sekunder på den samme filen (82% reduksjon i kjøretid), og trenger ikke mer enn 50 MB RAM (73% reduksjon).

88 sekunder er en evighet, mens jeg har tolmodighet nok til å vente i 16.

BONUSOPPGAVE TIL LESEREN: Klarer du å skrelle mer tid av algoritmen – enten i Ruby eller F#?

En løsning i F#

I F# må du definere en funksjon før du kan bruke den, så det er naturlig både å kode og å presentere koden bottom up.

Partial Application av infix operator

open System
open System.Collections.Generic

let blacklist = [""; "på"; "og"; "er"; "om"; "fra"; "med"; "i"; "to";
                 "det"; "å"; "ditt"; "til"; "ved"; "fra"; "for"; "av";
                 "en"; "din"; "du"; "at"; "vi"; "har"; "vil"; "nå";
                 "det"; "som"; "dere"; "kan"; "vår"; "så"]

Først importerer jeg et par navnerom (samme som using i C#), og så lager jeg en liste med de ordene jeg ikke ønsker å inkludere i analysen.

Jeg trenger også en liten funksjon for å sjekke om et ord er svartelistet:

let blacklisted (word : string) =
    List.exists ((=) (word.ToLower())) blacklist

Her bruker jeg exists-funksjonen i modulen List. Den tar to argumenter: En funksjon som returnerer true hvis elementet er funnet, og listen man skal lete i. Men funksjonsargumentet her er litt spesielt. Hva betyr ((=) (word.ToLower()))?

Dette er en såkalt partial application, som jeg har snakket om mange ganger før. = er en funksjon som sjekker om to verdier er like. Den brukes normalt infix, det vil si at den plasseres mellom sine to argumenter. Men om vi plasserer den mellom to paranteser blir den omgjort til en normal prefix-funksjon.

Det neste jeg har gjort er å sende ett – og bare ett – argument til funksjonen. Dette blir da omgjort til en ny funksjon som forventer ett argument til. ((=) (word.ToLower())) er dermed en funksjon som sjekker om noe er lik lowercase-versjonen av strengen word. De fre følgende uttrykkene er helt ekvivalente, og returnerer alle true:

((=) ("Foo".ToLower())) "foo"

(fun x -> ((=) ("Foo".ToLower())) x) "foo"

(fun x -> x = "Foo".ToLower()) "foo"

Å lage en egen infix operator

Vi går videre. Nå vil jeg lage en funksjon som stripper bort siste tegn fra en streng, men bare hvis det siste tegnet er en av et bestemt sett med tegn. I Ruby var dette gjort på én linje med gsub og et regulært uttrykk, men i F# benytter jeg anledningen til å eksperimentere litt.

(* Removes c from s if c is the last char *)
let (?<<) (s:string) (c:char) =
    let lastIndex = s.Length - 1
    if lastIndex >= 0 && s.LastIndexOf c = lastIndex
    then s.Substring (0, lastIndex)
    else s

Her har jeg laget en funksjon med det litt merkelige navnet ?<<. I tillegg har jeg lagt paranteser rundt, og slik blir det en operator på lik linje med pluss, minus, gange og deling.

Det ser rart ut, men den er elegant i bruk:

let stripPunctuation (word : string) =
    word ?<< '.'
         ?<< ','
         ?<< '!'
         ?<< ':'

La oss si at word inneholder strengern "foo!". Funksjonen ?<< kalles da først med argumentene "foo!" og '.', og resultatet er "foo!" (ingen endring altså). Dette resultatet blir nå første argument til den neste ?<<-funksjonen, som har ',' som andre argument. Og slik fortsetter det. Den tredje ?<< kalles med "foo!" og '!', og returnerer da bare "foo".

Denne teknikken kalles gjerne pipelining.

Å jobbe med en Dictionary

I dette programmet vil jeg bruke en generisk Dictionary<Key, Value>, slik som mange av oss er vandt til fra C#. Koden som følger er ikke spesielt funksjonell, fordi jeg baserer meg på å “mutere” (dvs. endre) dataene i dictionarien direkte. Jeg har likevel valgt å gjøre det fordi jeg jo “oversatte” et Ruby-script som gjorde det på denne måten, og fordi det vil være minst like raskt som en mere funksjonell løsning som ikke muterer data.

Først definerer jeg en ny type. Dette gjør jeg egentlig kun for å være litt eksplisit i koden, og slippe å måtte skrive Dictionary<string, int> fullt ut diverse steder.

type WordMap = Dictionary<string, int>

Jeg lager så en hjelpefunksjon som inkrementerer telleren for et bestemt ord, eller setter verdien 1 om ordet ikke finnes i dictionarien m fra før:

let increment (m : WordMap) word =
    if m.ContainsKey word
    then m.[word] <- m.[word] + 1
    else m.[word] <- 1

Når man muterer en variabel i F# bruker man ikke = slik man gjør i typiske imperative språk. I stedet bruker man <-.

let countWord acc word =
    if not (blacklisted word)
    then increment acc word

countWord er en funksjon som teller et ord fra filen jeg analyserer, men kun hvis ordet ikke er svartelistet. acc er en WordMap, men jeg behøver ikke fortelle F# det (det kan jo ikke være noe annet).

På tide å telle ordene

Nå har vi kommet til selve funksjonen som parser en fil og teller opp ordene i den – og her bruker vi alt vi har laget sålangt. Dette er den delen av programmet som konsumerer så og si all kjøretiden.

let parseWords fileName =
    let accumulator = new WordMap()
    let counter = countWord accumulator
    use file = IO.File.OpenText fileName
    while not file.EndOfStream do
        let words = file.ReadLine().Split [|' '|]
                    |> Array.map stripPunctuation
        Array.iter counter words
    accumulator

Når jeg oppretter variabelen counter så bruker jeg partial application igjen – du husker kanskje at countWord er en funksjon som tar to argumenter, men her gir jeg bare ett.

Legg også merke til nøkkelordet use som jeg bruker når jeg åpner filen. Dette er det sammen som using i C#, og file vil bli lukket/disposed før variabelen går ut av scope.

Filtrere bort skjeldne ord

Jeg ønsker også å fjerne ord som ikke gjentas mer enn X ganger i input-filen. Jeg har eksperimentert litt, og funnet at X = 1000 fungerer bra for de filene jeg jobber med, så jeg hardkoder likegreit denne verdien.

let trimRareWords pairsWithIntValue =
    Seq.filter (fun (KeyValue(_, count)) -> count > 1000)
               pairsWithIntValue

Argumentet til trimRareWords kommer til å være en WordMap, altså en Dictionary<string, int>. Og det kan man også se på som en sekvens med KeyValue<string, int>. Derfor kan jeg bruke Seq.filter til å luke bort ord. I den anonyme funksjonen jeg sender til filter bruker jeg pattern matching til å plukke ut antallet forekomster for et ord.

Skrive ut i Wordle-format

For å produsere filen på det formatet jeg ønsker trenger jeg to funksjoner til. Den første tar et ord med tilhørende antall og lager en tekststreng som Wordle vil forstå:

let wordleLine (KeyValue(word, count)) =
    sprintf "%s:%i" word count

Så trenger jeg også en funksjon som kan ta en sekvens av slike strenger og skrive dem til en fil:

let linesToFile fileName ls =
    IO.File.WriteAllLines(fileName, seq ls)

Main

Da gjenstår det bare å sy det hele sammen. Jeg må ta imot input-filnavnet (argument til programmet) og sette opp en sekvens med funksjoner som kan ta imot dette og produsere Wordle-filen.

For å måle og rapportere hvor lang tid dette tar har jeg brukt en funksjon jeg har kalt benchmark. Denne vil jeg presentere i en blogpost senere, men den tar i alle fall to argumenter: En streng som vil bli brukt som en label ved rapportering, og en parameterløs funksjon (også kalt en thunk). Den vil eksekvere thunken, måle og printe ut hvor lang tid den tok, og returnere resultatet av thunken – som i dette tilfellet er unit (altså ingen verdens ting).

[<EntryPoint>]
let main argv =
    benchmark "Elapsed"
              (fun () -> Seq.head argv
                      |> parseWords
                      |> trimRareWords
                      |> Seq.sortBy (fun (KeyValue(_, v)) -> v)
                      |> Seq.map wordleLine
                      |> linesToFile "out2.txt")
    0 // return an integer exit code

Og da var vi ferdige! Jeg håper du lærte noe. Og hvis du tror du kan lære meg noe, så vil jeg gjerne høre det – forslag til strukturelle forbedringer, eller ting som vil redusere kjøretiden mottas med takk!

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.

Ropy

Tuesday, January 1st, 2013
2 kommentarer

Hva er dette her?

image

Det du ser over er kildekoden til et program. Språket som er brukt heter Ropy, og programmet skriver som seg hør og bør ut teksten “hello, world”.

Ropy er altså et programmeringsspråk. Et ganske lite et. Og ganske ubrukelig egentlig, selv om det strengt tatt er turing-komplett, og dermed like “kraftig” som alle andre språk. Faktisk er det et forsøk på å lage en type språk vi kaller for en turing tarpit:

Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.

Alan Perlis, Epigrams on Programming

Jeg lagde Ropy i løpet av et par kvelder i desember. Språket er mitt bidrag til desember-konkurransen på PLT Games (Programming Language Theory Games). Hver måned blir man bedt om å lage et nytt programmeringsspråk, og i den påfølgende måneden vil man kunne stemme på hvilket bidrag man synes er best for å avgjøre en vinner. Språkene skal bedømmes i forhold til innovasjon, kompletthet og hvor godt de følger det valgte temaet for den måneden.

Hvis du har fulgt denne bloggen en stund så vet du at det er én oppgave jeg alltid pleier å løse: Å finne summen av alle tall under 1000 som er delelige på 3 eller 5. Og her er løsningen i Ropy:

image

Ropy er et stack-basert språk. Programmet begynner å kjøre oppe til venstre, og følger så tegnene. Når programmet har flere steder det kan gå – når “tauet” deler seg – velges den første veien i klokkeretningen. Men hvis verdien øverst på stacken er “0″ velger den vei mot klokkeretningen. Med dette kan vi simulere både if og løkker.

Den erfarne leser vil se at Ropy er inspirert av Befunge, som jeg presenterte i julekalenderen 2011.

Ropy-tolkeren er skrevet i Ruby, og er på godt under 200 linjer. Du kan ta en titt på den her, og vil du teste den selv er det bare å laste ned. Du finner også en eksekverbar spesifikasjon som vil hjelpe deg å forstå hvordan språket virker.

Ropy community

Kort tid etter at jeg hadde gjort Ropy tilgjengelig på Github fikk jeg også en pull requestSteffen Hageland, kjent som etse, hadde utvidet språket. Så jeg er tydeligvis ikke alene om å være helt sprø :)

Her er et eksempelprogram skrevet av Steffen:

image

Jeg trenger din stemme!

Som sagt er Ropy laget som et bidrag til en konkurranse. Og da hadde det selvsagt vært gøy om jeg fikk noen stemmer. Alle kan registrere seg for å være med å avgjøre vinneren (du må ha en github-bruker), og hvis du vil gi meg eller Ropy litt “cred” på den måten blir jeg veldig glad.

Får jeg litt stemmer kan det kanskje inspirere meg til å delta flere ganger også. Kanskje det blir 12 egenproduserte programmeirngsspråk å presentere på bloggen i år?! I januar skal det konkurreres om å lage et språk som fokuserer på automatisert testing. Ideer mottas med takk!

De viktigste lenkene

Noen utvalgte bidrag fra PLT Games du kan ta en titt på:

  • Colossal – som gjør programmering om til et tekstbasert adventure-spill.
  • nb – utfører programmer gjennom å simulere planet-baner i verdensrommet.
  • Turtle Graphics – Klassisk Turtle, turing tar-pit style. Test ut her!
  • turipong – utfører programmer ved å simulere en pong-ball som spretter fra vegg til vegg.
  • Hugo – et språk som stort sett består av GOTO-statements, og som har en veldig morsom README.
  • Seeker – programmene er node-grafer.
  • wc3pl – et språk inspirert av Warcraft III
  • cyprus“modeled as chemical reactions happening within protocellular constructions, producing new chemicals”
  • Ape – et interessant, minimalistisk språk implementert i noen få linjer F# av en Microsoft-ansatt språk-nerd.

No ifs and buts

Sunday, May 13th, 2012
Ingen kommentarer

For noen dager siden hadde vi en ny CodingDojo i Bergen. Oppgaven denne gangen var å presse objektorientering til det ytterste og trene på polymorfi gjennom å skrive kode helt uten bruk av IF, SWITCH eller tilsvarende konstruksjoner. Er du interessert i bakgrunnen for denne øvelsen kan du ta en titt her på bloggen til professor Eugene Wallingford. Du bør også sjekke ut the Anti-If Campaign.

Jeg jobbet sammen med @Olsenius for anledningen. Vi brukte Ruby. Oppgaven vi forsøkte å implementere var en billett-automat. Desverre ble dette mer et eksperiment i hvilke Ruby-triks vi kunne missbruke for å unngå IF enn en trening i god objektorientering. Koden vi produserte er noe av det værste jeg kan huske å ha laget :)

Vi ble ikke helt ferdige – jeg måtte gå litt tidlig – men koden vi produserte er gjengitt i sin helhet nedenfor. Vi brukte TDD, men testene har jeg droppet her.

Ser du hvordan vi har oppfunnet et nytt pattern både for IF og for UNLESS (altså “not if”)?

10 # Monkey patch: Just adding a convenient sum-method to arrays
11 class Array
12   def sum
13     self.inject(0){|x, y| x + y}
14   end
15 end
16 
17 class TicketMachine
18   def initialize
19     @balance, @total = [], []
20     @printers = { "total=5" => ValidTicket.new }
21     @money_validators = { "-" => InvalidMoney.new }
22   end
23 
24   # Properties:
25   def balance; @balance.sum; end
26   def total; @total.sum; end
27 
28   # Lets user print his ticket,
29   # but raises an Exception unless @balance is 5 !!!
30   def print_ticket
31     @printers["total=#{@balance.sum}"].print
32     @balance = []
33   end
34 
35   # Lets user insert money into the machine
36   # Will raise exception if money is a negative number !!!
37   def add money
38     begin
39       @money_validators[money.to_s[0]].fail_if_valid
40       raise "INVALID MONEY"
41     rescue NoMethodError
42       @balance << money
43       @total << money
44     end
45   end
46 end
47 
48 class ValidTicket
49   def print
50     puts "THE TICKET"
51   end
52 end
53 
54 class InvalidMoney
55   def fail_if_valid; end
56 end

That’s all!

Opus Polyglotis II: Ruby

Wednesday, February 22nd, 2012
Ingen kommentarer

act2

OPUS POLYGLOTIS II, Act 2
Tagline: "Noen har lest Gang of Four"
På scenen: Ruby

I forrige post så du en Python-løsning som var grei nok, men den hadde et par problem. Koden manglet struktur – det var bare en rekke funksjoner i toppnivået. Og for å legge til flere formater, en endring man må kunne forvente, måtte man modifisere dispatch-koden. Det er noe man generelt bør unngå.

I dag presenterer jeg en løsning på den samme oppgaven i Ruby. Den er objektorientert, og benytter et kjent design pattern for å fikse de nevnte problemene.

La oss begynne å kode. Jeg lager en modul jeg kaller OpusPolyglotis. I den lager jeg en ny modul jeg rett og slett kaller Template.

 22 module OpusPolyglotis
 23 
 24   module Template
 25     def initialize headers
 26       @headers = headers
 27     end
 28     def header ; '' ; end
 29     def row r  ; '' ; end
 30     def footer ; '' ; end
 31     def all_rows rs
 32       rs.map{|r| row r}.join ''
 33     end
 34   end

Template danner basisen for et format. Den har en konstruktør som tar inn headers fra csv-filen, og flere metoder som vil bli brukt til å generere output. header, row og footer har default-implementasjone som bare returnerer en tom streng.

PS: Det er kanskje ikke akkurat Template Method Pattern dette her, men måten jeg bruker det på er ikke så forferdelig langt unna heller.

Vi er fortsatt inne i OpusPolyglotis-modulen, og nå kommer et lite, ideomatisk Ruby-triks som oppretter en variabel i selve OpusPolyglotis. Du kan tenke på formatters omtrent som en statisk variabel i en C#-klasse. Her vil jeg etterhvert laste inn de ulike format-definisjonene.

 36   class << self
 37     attr_accessor :formatters
 38   end
 39   self.formatters = {}

Fortsatt i OpusPolyglotis oppretter jeg så en klasse jeg kaller Formatter. Dette er selve kjernen i programmet. I konstruktøren leser jeg en csv-fil ved hjelp av Ruby’s utmerkede csv-støtte. Metoden format_as velger ut en format-definisjon fra formatters-variabelen, oppretter en instans av den, og produserer resultatet. Som du skjønner forventer denne koden at formatters inneholder klasser som implementerer metodene jeg definerte i Template-modulen.

 41   require 'csv'
 42 
 43   class Formatter
 44     def initialize csvfile
 45       data = CSV.read csvfile, :col_sep => ';'
 46       @headers = data.first
 47       @rows = data.drop 1
 48     end
 49 
 50     def format_as key
 51       formatter_class = OpusPolyglotis.formatters[key]
 52       f = formatter_class.new @headers
 53       f.header + f.all_rows(@rows) + f.footer
 54     end
 55   end
 56 
 57 end # module OpusPolyglotis

La meg bruke et diagram til å illustrere hva jeg har laget og hvor jeg vil hen:

ruby_objects

Jeg kommer til å opprette tre nye klasser som implementerer hvert av formatene jeg skal støtte. Disse vil arve egenskapene til Template-modulen – ikke gjennom klassisk arv; formatene vil bruke Template som en mixin (ikke at det er sentralt for løsningen, men det er ganske praktisk likevel).

De forskjellige formatene vil også registrere seg selv i formatter-variabelen. Dermed trenger jeg ikke endre dispatch-koden i Formatter-klassen, som jo bare henter ut definisjonen fra denne variabelen.

La oss ta en titt på den enkleste format-definisjonen:

 59 module OpusPolyglotis
 60   class YamlFormatter
 61     include Template
 62     def header
 63       'Yaml not yet implemented!'
 64     end
 65   end
 66 
 67   self.formatters['yaml'] = YamlFormatter
 68 end

Som du ser “åpner jeg opp igjen” OpusPolyglotis-modulen, slik at jeg får plassert format-definisjonen i det “namespacet”. Dette er ikke essensielt, men ryddig. Jeg lager en klasse som så inkluderer Template. Jeg overstyrer en av funksjonene fra Template, nemlig header, som nå bare vil returnere “YAML not yet implemented!”.

Og så til den sentrale linjen under klassedefinisjonen: Her legger jeg inn selve klassen YamlFormatter som en verdi i formatters, og bruker “yaml” som nøkkel. Neat! Nå kan jeg hente ut de definerte formatene hvor som helst og studere dem ved å si OpusPolyglotis.formatters.

Jeg tror du skjønner hvordan det henger sammen nå, og jeg kan vise deg XmlFormatter:

 73 module OpusPolyglotis
 74   class XmlFormatter
 75     include Template
 76 
 77     def header; "<records>" ; end
 78     def footer; "\n</records>" ; end
 79 
 80     def row r
 81       "\n\t<record>#{
 82         @headers.
 83           map{|h| h.gsub /\ /, '_' }.
 84           zip(r).
 85           map{|kv| "\n\t\t<#{kv[0]}>#{kv[1]}</#{kv[0]}>"}.
 86           join("")
 87       }\n\t</record>"
 88     end
 89   end
 90 
 91   self.formatters['xml'] = XmlFormatter
 92 end

Denne gangen overstyrer jeg header- og footer-metodene til å returnere henholdsvis start- og slutt-tag for XML-dokumentet. Og så bruker jeg metoder jeg er veldig glad i – map, zip og join – til å lage et record-element i row-metoden. Ser du hvor enkel og elegant formatdefinisjonen har blitt fordi jeg baserer meg på en felles Template modul?

Zip er forresten en metode jeg kanskje ikke har snakket om før. Dictionary.com sier: “A function that takes two lists and returns a list of pairs. The idea can easily be extended to take N lists and return a list of N-tuples.” Jeg bruker den til å “pare opp” en og en header med de tilhørende feltverdiene i raden. Zip er en kjekk funksjon fra den funksjonelle paradigmen, men ikke så vanlig som map, filter og fold.

Og så – hvis du henger med fortsatt – tar vi en titt på implementasjonen av JSON-formatet:

100 module OpusPolyglotis
101   class JsonFormatter
102     include Template
103 
104     def header; '[' ; end
105     def footer; ']' ; end
106 
107     def row r
108       '{' + get_fields(r) + "}"
109     end
110 
111     def get_fields r
112       (@headers.zip(r).map do |key_val|
113         '"' + key_val[0] + '":' + format_value(key_val[1])
114       end).join ', '
115     end
116 
117     def format_value v
118       return v if /^\d+.?\d*$/ =~ v
119       return '"' + v + '"'
120     end
121 
122     # override all_rows to add newline between rows
123     def all_rows rs
124       rs.map{|r| row r}.join "\n"
125     end
126   end
127 
128   self.formatters['json'] = JsonFormatter
129 end

Ganske likt som for XML egentlig: zip, map og join bygger opp den nødvendige strengen. Som jeg gjorde i Python-løsningen må jeg også her validere om en verdi er et gyldig tall (metoden format_value). I så fall skal det ikke ha hermetegn.

Legg merke til at jeg også overstyrer Template’s all_rows-metode, noe jeg ikke trengte i XML. Dette gjør jeg bare for at output skal bli litt finere – jeg putter nemlig inn et linjeskift mellom JSON-objektene.

Det eneste som gjenstår nå er plukke opp argumentene til programmet, og å generere og printe ut  formatet. I Ruby lager vi typisk ikke main-metoder, men konstruerer et lite idiom som sjekker om det er denne filen som har blitt eksekvert (de magiske __FILE__ og $0 variablene). Og så er vi i mål!

131 if __FILE__ == $0
132   some_format, csvfile = ARGV
133   formatter = OpusPolyglotis::Formatter.new csvfile
134   puts formatter.format_as some_format
135 end

Oppsummering

Det ble litt mere kode enn i act 1, men det ble en fin og anvendelig struktur. Strategiene (ja, dette er Strategy Pattern) er self contained, og nye formater kan legges til helt uten å redigere eksisterende kode. Bruken av en Template-modul til strategiene gjorde også at selve koden for å generere de ulike formatene ble ganske ryddig.

Jeg er altså ganske fornøyd med denne løsningen. Følg med i neste episode, hvor det godt kan hende jeg roter det litt til igjen! ;)

Euler #1 som et sett-problem

Friday, November 25th, 2011
6 kommentarer

I forrige bloggpost presenterte jeg ulike løsninger på Euler-problem nummer 1. Men alle disse løsningene var i prinsippet like; iterer over alle tall mellom 0 og 1000, og filtrer ut og pluss sammen tallene som er multipler av 3 eller 5.

Jeg klarer ikke helt å slippe denne oppgaven. Alternativt kan vi nemlig se på det som et matematisk sett-problem, og da kan vi implementere løsninger som er litt mere deklerative. Hvis A er alle tall som er multipler av 3, og B er alle tall som er multipler av 5, så er løsningen å summere tallene i unionen av A og B.

Euler_sets

La oss ta en titt på ulike måter å implementere dette på…

A + B – snittet av a og b

Gitt at A og B er settene som beskrevet over. Da er en løsning å legge sammen summen av tallene i A og summen av tallene i B. Problemet nå er at tall som både er multipler av A og multipler av B blir lagt til i summen to ganger. For å løse dette kan vi trekke fra summen av alle disse tallene – nemlig alle tall som er multipler av 3 ganger 5, altså 15.

Her er en løsning i Ruby:

10 def sum_multiples args
11   (args[:of]..args[:upto]).
12     step(args[:of]).
13     reduce(:+)
14 end
15 
16 sum  = sum_multiples :of => 3, :upto => 999
17 sum += sum_multiples :of => 5, :upto => 999
18 sum -= sum_multiples :of => 3*5, :upto => 999
19 puts sum

Jeg liker hvor leselig koden blir: “Sum er lik sum multiples av 3 opp til 999″ osv. Men ved å introdusere litt kodeduplisering kan det hele selvsagt gjøres endel enklere:

23 puts (3..999).step(3).reduce(:+) +
24      (5..999).step(5).reduce(:+) -
25      (3*5..999).step(3*5).reduce(:+)

Det vi trekker fra på slutten er altså snittet mellom A og B.

Unionen av a og b direkte

Mange programmeringsspråk/standard bibloteker har direkte støtte for å beregne unionen av to sett, og det kan vi også benytte. Her er en tilsvarende Ruby-implementasjon som oppretter A og B, tar unionen av disse, og summerer sammen:

30 def set args
31   (args[:of]..args[:upto]).
32     step(args[:of]).
33     to_a
34 end
35 
36 multiples_of_3 = set :of => 3, :upto => 999
37 multiples_of_5 = set :of => 5, :upto => 999
38 multiples_of_3_or_5 = multiples_of_3 | multiples_of_5
39 
40 puts multiples_of_3_or_5.reduce(:+)

Nå begynner det å komme seg :) Som en referanse slenger jeg med den samme løsningen implementert i Clojure, som har en egen set-datatype:

1 (defn multiples_of [x upto]
2   (set (range x upto x)))
3 
4 (defn multiples_of_3_or_5 []
5   (clojure.set/union (multiples_of 3 1000)
6                      (multiples_of 5 1000)))
7 
8 (->> (multiples_of_3_or_5) (reduce +) println)

Som du forstår er det mange ulike måter å løse en programmeringsoppgave på – selv en så enkel oppgave som dette. Ulike språk foretrekker ulike løsninger, og det er en av grunnene til at det er interessant å studere dem.

NÅ er det på tide å gi seg! Håper jeg ikke har kjedet vettet av deg med denne oppgaven – men nå er du i alle fall skikkelig primet og klar til å studere løsninger programmert i diverse språk du sansynligvis aldri har sett før. Neste blogpost kommer 1. desember!

Hello World, Euler style

Thursday, November 24th, 2011
17 kommentarer

I den oppkommende adventskalenderen, hvor jeg hver dag frem til Jul vil introdusere deg for et nytt programmeringsspråk, har jeg valgt én bestemt oppgave for å vise frem språkene – nemlig den første og enkleste oppgaven på Project Euler.

Oppgaven lyder:

Hvis vi lister opp alle naturlige tall under 10 som er multipler av 3 eller 5 så får vi 3, 5, 6 og 9. Summen av disse er 23. Finn summen av alle multipler av 3 eller 5 under 1000.

Jeg bruker denne oppgaven som min Hello, World når jeg lærer meg nye språk. Den sørger for at jeg raskt lærer meg flere grunnleggende byggestener i språket, som hvordan man itererer, og hvordan man tar avgjørelser basert på numeriske operasjoner.

Jeg har også brukt oppgaven her på bloggen tidligere, i posten Parallellisering av en algoritme i Erlang. I dag gir jeg deg mulighet til å gjøre deg kjent med oppgaven, og viser noen implementasjoner i de språkene du normalt ser på programmeringsbloggen.

Løsning i C#

C# er språket jeg bruker til daglig, så det er et greit sted å begynne. Her følger en komplett løsning som looper gjennom alle tallene opp til 1000, sjekker om de er multipler av 3 eller 5 (ved å bruke modulo), og summerer opp tallene i sum-variabelen. Det siste som skjer er at tallet skrives ut.

10 class Program
11 {
12     static void Main()
13     {
14         int sum = 0;
15         for (int i = 0; i < 1000; i++)
16             if (i % 3 == 0 || i % 5 == 0)
17                 sum += i;
18         System.Console.WriteLine(sum);
19     }
20 }

Som sagt er det en meget enkel oppgave – så sant du kjenner til modulo da.

En alternativ måte å løse oppgaven er å bruke en funksjonell, stream-basert teknikk. I .NET vil dette si Linq. Her er en løsning i C# som bruker Range, Where og Sum:

10 using System.Linq;
11 
12 class Program
13 {
14     static void Main()
15     {
16         System.Console.WriteLine(
17             Enumerable.Range(1, 999)
18             .Where(i => i % 3 == 0 || i % 5 == 0)
19             .Sum());
20     }
21 }
22 

I julekalenderen vil du komme til å se en god mix av disse to fremgangsmåtene, og noen som er helt anderledes.

Løsning i Ruby

Ruby er det språket det er lettest for meg å hoppe inn i om jeg skal lage et kjapt lite verktøy, skal teste ut en algoritme eller noe lignende. Her følger en imperativ løsning:

1 sum = 0;
2 (1...1000).each do |i|
3   sum += i if i.modulo(3) == 0 or i.modulo(5) == 0
4 end
5 puts sum

I Ruby bruker jeg omtrent aldri vanlige for-løkker – å opprette en range, og så kjøre en kodeblokk for hvert element er mere naturlig.

En stream-basert løsning er mere elegant. Da bruker jeg select for å filtrere tallene, og reduce for å summere, slik som dette:

7 puts (1...1000).
8   select{|x| x.modulo(3) == 0 or x.modulo(5) == 0}.
9   reduce(:+)

Løsning i Clojure

Clojure er fortsatt favorittspråket mitt, så jeg må ta med noen løsninger i det også. Selv om det er helt unaturlig å bruke en løkke i Clojure så har jeg kokt sammen en slik løsning for denne ene gangens skyld:

 1 (loop [sum 0, numbers (range 1000)]
 2   (if (seq numbers)
 3     (let [i (first numbers)]
 4       (if (or (zero? (mod i 3))
 5               (zero? (mod i 5)))
 6         (recur (+ sum i) (rest numbers))
 7         (recur sum (rest numbers))))
 8     (println sum)))

Det fungerer, men jeg gremmes! I Clojure er stream-basert programmering det naturlige, og jeg kan bruke range, filter og reduce til å løse oppgaven ganske enkelt:

 1 (println
 2   (reduce +
 3           (filter #(or (zero? (mod % 3))
 4                        (zero? (mod % 5)))
 5                   (range 1000))))

Det er derimot ikke sikkert du synes det er så enkelt å lese denne koden. Trikset med å lese Clojure/Lisp-syntaks er som følger: Skann raskt med øynene fra venstre mot høyre til du kommer til uttrykkets siste del, som i dette tilfellet er (range 1000). Arbeid deg så tilbake mot høyre.

For å gjøre koden litt mere leservennlig tilbyr Clojure deg å gjøre det samme på følgende måte:

10 (->> 1000 range
11      (filter #(or (zero? (mod % 3))
12                   (zero? (mod % 5))))
13      (reduce +)
14      println)

Denne koden kan leses fra venstre mot høyre. “Ta tallet 1000, og lag en range av det. Filtrer med en anonym funksjon som sjekker om tall er delelig på 3 eller 5. Reduser med pluss (altså pluss dem sammen), og skriv til slutt ut”.

Løsning i Common Lisp

Og helt på tampen tar jeg med en løsning i Common Lisp – fordi man der finner den kuleste og mest komplette varianten av en for-løkke som finnes i noe språk, nemlig loop makroen:

1 (print (loop for x below 1000
2              when (or (zerop (mod x 3))
3                       (zerop (mod x 5)))
4              sum x)

Du har nå sett oppgaven løst med fire ulike språk, og i desember vil du få se flere løsninger. Da vil jeg sansynligvis bryte oppgaven opp i flere steg eller funksjoner, slik at jeg får presentert enda flere aspekter ved språkene jeg forteller om.

Jeg håper du gleder deg like mye som meg :)

Hvem bor hvor? (CodingDojo/ZipTalk)

Wednesday, October 19th, 2011
3 kommentarer

I forrige uke hadde jeg en litt spesiell Coding Dojo / ZipTalk på jobben. Jeg startet med å gi utviklerne på teamet en oppgave, og sa at de hadde en halv time til å komme opp med en løsning i felleskap. Oppgaven er hentet fra Structure and Interpretation of Computer Programs (hvor den ble brukt til å illustrere ikke-deterministisk programmering), og lød som følger:

Baker, Cooper, Fletcher, Miller og Smith bor i samme hus med fem etasjer, men i hver sin etasje. Baker bor ikke i øverste etasje, og Cooper bor ikke nederst. Fletcher bor hverken øverst eller nederst. Miller bor høyere opp enn Cooper. Smith bor ikke i etasjen direkte over eller under Fletcher. Fletcher bor ikke i etasjen direkte over eller under Cooper.

Lag et program som forteller i hvilken etasje hver av dem bor!

Hvis du ønsker å forsøke deg på oppgaven selv anbefaler jeg at du gjør det før du leser videre!

Jeg kastet med vilje utviklerne ut på dypt vann, og hadde ikke regnet med at de ville komme i mål. Noe de heller ikke gjorde. Målet mitt med oppgaven var blant annet å gi dem erfaring med å kaste seg over en oppgave under sterkt tidspress, og se hvordan de sammarbeidet. Men det var også å presentere en alternativ måte å løse slike oppgaver på – mer om det litt lenger nede.

Da halvtimen var over snakket vi litt om oppgaven og hvordan det hadde gått, og så presenterte jeg et par løsninger på problemet i Ruby. Den første er ikke spesielt elegant, men fungerer. Prinsippet er at jeg ved hjelp av fem nøstede for-løkker genererer alle mulige kombinasjoner av etasjer for de fem beboerne. I den innerste løkken skipper jeg løsninger som ikke er korrekte i forhold til reglene i oppgaven ved å bruke next (samme som continue i C# og andre steder).

Til slutt skriver jeg ut de akseptable løsningene. Her følger koden:

10 for baker in 1..5
11   for cooper in 1..5
12     for fletcher in 1..5
13       for miller in 1..5
14         for smith in 1..5
15           all = [baker, cooper, fletcher, miller, smith]
16           next unless all.count(1) == 1
17           next unless all.count(2) == 1
18           next unless all.count(3) == 1
19           next unless all.count(4) == 1
20           next unless all.count(5) == 1
21 
22           next if baker == 5
23           next if cooper == 1
24           next if fletcher == 1 or fletcher == 5
25           next unless miller > cooper
26           next if (smith - fletcher).abs() == 1
27           next if (fletcher - cooper).abs() == 1
28 
29           puts "Baker = #{baker}"
30           puts "Cooper = #{cooper}"
31           puts "Fletcher = #{fletcher}"
32           puts "Miller = #{miller}"
33           puts "Smith = #{smith}"
34           puts
35         end
36       end
37     end
38   end
39 end

Deretter demonstrerte jeg en løsning hvor jeg utnytter litt flere av Ruby’s muligheter. I linje 10 til 16 nedenfor monkey-patcher jeg Array-klassen med litt syntakstisk sukker for å gjøre løsningen mere lesbar. Linje 27 til 34 skriver ut de gyldige løsningene.

Selve løsningen på problemet ligger i linje 18 til 25. Her bruker jeg permutation-metoden til å gi meg alle kombinasjonene av et array – det tilsvarer de 10 første linjene i løsningen over. Deretter bruker jeg find_all (tilsvarer filter i mange andre språk) til å begrense permutasjonene til de som er gyldige.

10 class Array
11   def baker; self[0] end
12   def cooper; self[1] end
13   def fletcher; self[2] end
14   def miller; self[3] end
15   def smith; self[4] end
16 end
17 
18 answer = [1,2,3,4,5].permutation.find_all do |solution|
19   not solution.baker == 5 and
20   not solution.cooper == 1 and
21   not solution.fletcher == 1 and not solution.fletcher == 5 and
22   solution.miller > solution.cooper and
23   not (solution.smith - solution.fletcher).abs() == 1 and
24   not (solution.fletcher - solution.cooper).abs() == 1
25 end
26 
27 answer.each do |a|
28   puts "Baker = #{a.baker}"
29   puts "Cooper = #{a.cooper}"
30   puts "Fletcher = #{a.fletcher}"
31   puts "Miller = #{a.miller}"
32   puts "Smith = #{a.smith}"
33   puts
34 end

Men poenget mitt med denne CodingDojoen/ZipTalken var å introdusere noe helt annet…

En løsning i prolog

Prolog er et logisk programmeringsspråk. I motsetning til Ruby, som er imperativt, er logiske språk deklerative. Det vil si at i stedet for å implementere en algoritme for å løse problemet så beskriver vi bare selve problemet, og Prolog løser det for deg!

Prolog kan se ganske gresk ut for dem som ikke har vært borti det før, men la oss bare hoppe i det. Jeg skal forsøke å forklare underveis.

 1 apartments([]).
 2 apartments([Head|Tail]) :-
 3   member(Head, [1,2,3,4,5]), apartments(Tail).

Linjene over er en regel som forteller Prolog hva en liste av leiligheter/etasjer er for noe. Linje 1 sier at en tom liste, alstå [], er en liste av leiligheter. De neste to linjene er en rekursiv regel som sier at en ikke-tom liste er en liste av leiligheter om det første elementet (Head) er et medlem av listen [1,2,3,4,5], og resten av elementene (Tail) også er en gyldig liste av leiligheter.

Tok du den?

 5 distinct([]).
 6 distinct([Head|Tail]) :-
 7   \+member(Head, Tail), distinct(Tail).

Linje 5 til 7 beskriver regelen som sier at alle må bo i forskjellige etasjer – altså at løsningen må være en liste hvor alle elementene er unike. Først sier jeg at en tom liste er ok. Deretter sier jeg at en ikke-tom liste er ok om det første elementet (Head) ikke finnes i resten av elementene (Tail), og resten av elementene også er en unik liste. Backslash og pluss betyr “ikke” i Prolog.

I den neste linjen lager jeg for enkelhets skyld en regel som kombinerer de to forrige reglene:

 9 distinct_apartments(L) :- apartments(L), distinct(L).

Og så er det på tide å fortelle Prolog hva det vil si å bo rett over eller rett under en annen beboer. I linje 11 og 12 sier jeg at X og Y bor i tilstøtende etasjer hvis absoluttverdien av X minus Y (som jeg kaller Diff) er lik 1.

11 adjacent(X, Y) :-
12   Diff is abs(X - Y), Diff = 1.

Da gjenstår det bare å lage en hovedregel som inkluderer alle detaljene fra oppgaven. I dwelling sier jeg at Baker, Cooper, Fletcher, Miller og Smith bor i hver sin etasje, at Baker ikke bor i femte, at Cooper ikke bor i første, at Fletcher hverken bor i femte eller første, at Miller bor høyere opp enn Cooper, og at Smith og Fletcher, og Fletcher og Cooper ikke bor like over eller under hverandre.

14 dwelling(Baker, Cooper, Fletcher, Miller, Smith) :-
15   distinct_apartments([Baker, Cooper, Fletcher, Miller, Smith]),
16   Baker \= 5,
17   Cooper \= 1,
18   Fletcher \= 1, Fletcher \= 5,
19   Miller > Cooper,
20   \+adjacent(Smith, Fletcher),
21   \+adjacent(Fletcher, Cooper).

Og da er vi ferdige. Fyrer jeg opp Prolog og kjører regelen min så får jeg svaret:

dwelling

Prolog sier helt til slutt “no” fordi jeg spurte om det fantes flere løsninger – og det gjorde det ikke.

Hva var poenget?

Poenget var at med Prolog så har jeg ikke behøvd å tenke på hvordan jeg skulle løse oppgaven algoritmisk. Jeg har ikke behøvd å generere en masse forslag til løsninger som jeg så må filtrere. I stedet har jeg bare reformulert oppgaven på en måte som Prolog forstår, og Prolog finner selv den korrekte løsningen for meg.

Det er dette vi kaller deklerativ programmering.

Da teamet forsøkte å løse oppgaven under sterkt tidspress klarte de ikke å komme opp med den nødvendige algoritmen. Om de hadde kunnet Prolog hadde de ikke behøvd det. Er ikke det ganske interessant?

Bergen CodingDojo

For de som er interessert i Coding Dojo konseptet vil jeg benytte anledningen til å nevne at det i disse dager er tatt initiativ til en CodingDojo i Bergen. Det er Kjerti Berg som jobber i konsultenselskapet Miles som står bak. Og målet er at folk som ønsker å bli bedre programmerere skal få øve sammen med likesinnede gjennom kode kataer i en sosial setting.

Det første møtet blir 22. november. Kommer du?

Relaterte innlegg: Coding Dojo på Contiki, ZipTalks på jobben.

loggfil-hacking med Ruby

Tuesday, September 20th, 2011
2 kommentarer

loggfilhacking

Ruby er et perfekt språk til å lage små verktøy for å automatisere ting man som gode utviklere uansett bør være for late til å gjøre manuelt. Jeg bruker først og fremst Ruby til å lage små scripts som hjelper meg i bygging og/eller deployment av prosjekter, men jeg lager endel andre scripts også. Tenkte nå jeg skulle vise frem ett av dem.

I denne blogposten vil du se et enkelt script som leser loggfiler fra en webapplikasjon, og presenterer en rapport med oversikt over aggregerte data fra loggene. Målet er ikke å vise noe som helst revolusjonerende eller fantastisk – jeg vil bare dele litt kode, og kanskje inspirere flere til å automatisere slike ting.

Loggfilene

Filene jeg skal rapportere på er hentet fra en ASP.NET app som bruker log4net til logging. Her er en liten eksempelfil:


2011-09-06 00:33:43,821 [1] INFO  APPLICATION STARTED
2011-09-06 00:33:45,075 [5] DEBUG Default.aspx (loading..) | 4666/acme/PROD1
2011-09-06 00:33:50,043 [6] DEBUG Report.aspx (loading..) | 4666/acme/PROD1
2011-09-06 00:33:57,680 [5] DEBUG Report.aspx (loading..) | 4666/acme/PROD1
2011-09-06 00:33:57,726 [5] INFO  Loading report "Traffic report (monthly)" | 4666/acme/PROD1
2011-09-06 01:01:07,555 [5] DEBUG Default.aspx (loading..) | 1233/foobar/PROD1
2011-09-06 01:01:41,472 [6] DEBUG Report.aspx (loading..) | 1233/foobar/PROD1
2011-09-06 01:01:57,638 [9] DEBUG Report.aspx (loading..) | 1233/foobar/PROD1
2011-09-06 01:01:57,681 [9] INFO  Loading report "Incoming traffic report (weekly)" | 1233/foobar/PROD1
2011-09-06 01:02:18,194 [6] INFO  Loading report "Keyword period statistics" | 1233/foobar/PROD1

Her er det mye nyttig informasjon jeg gjerne vil rapportere på. Hver gang en side lastes logges det en debug-linje med sidens navn, og på slutten av linjen legges det på informasjon om brukeren. Og hver gang en bruker kjører en rapport logges det en info-linje med rapportens navn.

Webapplikasjonen genererer én ny logfil hver dag. De har et fast navn, med datoen lagt på som et suffix. Dagens logg heter MyWebApp.log, loggen for i går heter MyWebApp.log20110910, osv. Skriptet som analyserere filene må kunne slå sammen data fra et vilkårlig antall logfiler.

Resultatet

Jeg ønsker en tredelt rapport basert på disse logfilene. For det første ønsker jeg å vite hvor mange ganger de ulike sidene har blitt lastet. Deretter vil jeg vite hvilke brukere som er mest aktive, og til slutt vil jeg vite hvilke rapporter som kjøres flest ganger.

Scriptet mitt skal printe ut rapporten til konsollet. Ønsker jeg å lagre rapporten kan jeg da enkelt “pipe” output til en fil i stedet. Her er output basert på filen ovenfor:

Analysing file MyWebApp.log

Page                                                    Hits
------------------------------------------------------------
Report                                                     4
Default                                                    2

User                                                    Hits
------------------------------------------------------------
foobar              PROD1  ID:1233                         5
acme                PROD1  ID:4666                         4

Report                                                  Hits
------------------------------------------------------------
Keyword period statistics                                  1
Incoming traffic report (weekly)                           1
Traffic report (monthly)                                   1

Scriptet

Jeg skal ikke si så alt for mye om koden – håper den klarer å stå på egne ben. Men jeg har delt den opp litt for å forenkle lesingen. Her er første chunk, som i hovedsak består av en main-metode som først validerer parametrene, deretter parser log-filene (linje 35) og til slutt skriver ut rapporten (37).

10 unless ARGV.length == 2
11   raise <<EOF
12 
13 
14 Usage:   mywebapp_stats.rb folder filepattern
15 
16 Example: mywebapp_stats.rb . AccountWeb.log*
17 
18 EOF
19 end
20 
21 $user_hash = {}
22 $page_hash = {}
23 $report_hash = {}
24 
25 def main
26   log_dir = ARGV[0]
27   files_pattern = ARGV[1]
28 
29   if File.directory?(log_dir)
30     Dir.chdir log_dir
31   else
32     raise "#{log_dir} is not a directory"
33   end
34 
35   Dir.glob(files_pattern).each { |f| parse_file f }
36 
37   report
38 end

Og så følger koden som parser selve logfilene. Parsingen er basert på regulære uttrykk, så det kan se litt kryptisk ut. Magien ligger i reg_count, som inkrementerer en teller i en hashtabell hvis det regulære uttrykket matcher linjen. inc_hash! gjør selve teller-inkrementeringen.

40 ### ANALYSE FILE STUFF ###############################################
41 
42 def parse_file filepath
43   puts "Analysing file #{filepath}"
44   File.open(filepath, "r") do |f|
45     while (line = f.gets)
46       reg_count(/Loading report "(.*)"/, line, $report_hash)
47       reg_count(/\| (\d+)\/(.*)\/(PROD[123])/, line, $user_hash) do |m|
48         m[0][1].ljust(20) + m[0][2] + "  ID:" + m[0][0]
49       end
50       reg_count(/ ([A-Za-z]+)\.aspx \(loading\.\.\)/, line, $page_hash)
51     end
52   end
53 end
54 
55 def reg_count pattern, line, hash, &block
56   match = line.scan pattern
57   if match.length > 0
58     key = block.call(match) if block
59     inc_hash! key || match[0][0], hash
60   end
61 end
62 
63 def inc_hash! k, h
64   n = h[k] || 0
65   h[k] = n + 1
66 end

Det eneste som gjenstår da er å skrive ut rapporten.

68 ### REPORT OUTPUT STUFF ###############################################
69 
70 def report
71   report_hash "Page", "Hits", $page_hash
72   report_hash "User", "Hits", $user_hash
73   report_hash "Report", "Hits", $report_hash
74 end
75 
76 def report_hash col_a, col_b, h
77   report_header col_a, col_b
78   h.
79     sort{|a,b| a[1] <=> b[1]}.
80     reverse.
81     each { |key, value| report_line(key, value) }
82 end
83 
84 def report_header col_a, col_b
85   puts
86   report_line col_a, col_b
87   puts "-" * 60
88 end
89 
90 def report_line a, b
91   puts a.ljust(50) + b.to_s.rjust(10)
92 end
93 
94 main # run the program

Spørsmål? Uforståelig? Elementært?

Her er noen andre eksempler på hva jeg bruker Ruby til: Script IIS Manager med IronRuby | Litt ADO.NET i IronRuby | Slette/tømme MSMQ-køer med IronRuby | En minimal HTTP-server i Ruby

Mist får en side

Sunday, September 4th, 2011
6 kommentarer

Denne blogposten presenterer den nye websiden jeg har laget for programmeringsspråket Mist, samt de rundt 20 linjene Ruby-kode som skulle til for å generere siden.

Ethvert programmeringsspråk trenger en egen side – i alle fall om andre enn designeren selv skal bruke det. Og nå har jeg som sagt begynt på en side for Mist. Den har ikke så mye innhold enda, men design og struktur er på plass. Her er et screenshot (klikk for å gå til siden):

mist_page

Siden skiller seg bort fra ting jeg har gjort tidligere på flere punkter. Blant annet har jeg innsett at HTML ikke er XML, og har nå gått for mere pragmatisk HTML5, hvor jeg blant annet har droppet ting som HTML, HEAD og BODY-tags (de skal faktisk være helt unødvendige). Jeg har også brukt mye absolutt posisjonering, og til og med litt fixed. Jeg har satset på rent design og ren markup, og er ikke nervør om noen skulle velge å titte på koden.

Jeg har også oppdaget Google Web Fonts, som lar meg inkludere et hav av ulike skrifttyper på sidene uten store problemer. Eneste faren er at jeg kanskje går litt for langt, sånn som man ofte gjør når man får et nytt leketøy. Synes du jeg har gjort det?

Generering av statiske websider med Ruby

Mist-siten er hostet som Github Pages, tilknyttet Github repositorien. Sider som serves på denne måten må være statiske html-sider. Jeg ville derimot basere siten min på en template, så for å ikke måtte repetere meg selv måtte jeg generere sidene før jeg lastet dem opp.

For dette formålet støtter Github Pages et verktøy som heter Jekyll, som jeg har brukt tidligere, og som lar deg generere statiske websider basert på maler. Denne gangen valgte jeg likevel å lage mitt eget opplegg basert på kun noen få linjer Ruby.

Først lagde jeg en fil som jeg kalte genlib.rb. Den inneholder to funksjoner: Den første leser malen som er lagret i filen template.html, og returnerer den som en streng. Den andre – generate – tar inn en hash med parametre, henter ut malen, fletter inn det som er sidespesifikt i malen, og lagrer resultatet som en html-fil.

10 def template
11   template = ''
12   File.open('template.html', "r") do |file|
13     while (line = file.gets)
14       template += line
15     end
16   end
17   return template
18 end
19 
20 def generate args
21   name = args[:file] + ".html"
22   File.open(name, "w") do |file|
23     file.puts template.
24       gsub(/\{CONTENT\}/, args[:content]).
25       gsub(/\{QUOTE\}/, args[:quote]).
26       gsub(/\{TITLE\}/, args[:title])
27   end
28   puts "Generated file #{name}"
29 end

For hver siden jeg skal generere lager jeg så en ny fil som skal kalle generate-funksjonen. Nedenfor er gen_download.rb, som genererer download.html. Den definerer ønsket filnavn (linje 13), sidetittel (14), et sitat som skal brukes i toppen av siden (16-23) og selve innholdet på siden (25-33). Til slutt kaller den generate, og siden blir produsert.

10 require "./genlib"
11 
12 page = {}
13 page[:file] = "download"
14 page[:title] = "Download Mist"
15 
16 page[:quote] = <<EOF
17 <p class="quote">
18     "Language designers are not intellectuals.<br/>
19     They're not as interested in thinking as you might hope.<br/>
20     They just want to get a language done and start using it."<br/>
21     - Dave Moon
22 </p>
23 EOF
24 
25 page[:content] = <<EOF
26 
27 <h1>Download Mist</h1>
28 <p>
29   No downloads here yet! Get the source from 
30   <a href="https://github.com/tormaroe/mist">Github</a> 
31   if you're interested.
32 </p>
33 EOF
34 
35 generate page

Til slutt laget jeg en fil jeg kalte gen.rb. Den henter ut og kjører alle filer som har et navn som begynner på “gen_”. Ved å kjøre gen.rb vil jeg dermed få generert opp alle de statiske sidene mine, klare for opplasting til serveren.

1 if __FILE__ == $0
2   Dir.foreach(".") {|f| f.match(/gen_/) { load f } }
3 end

Alternativt kunne jeg har brukt Ruby ERB templating (ala klassisk ASP eller PHP), eller kanskje et annet templating system, for å generere filene. Men for mitt formål valgte jeg altså denne gangen en mere basic løsning.

Har du noen bemerkninger til Mist, webdesignet, det kommende innholdet, eller til Ruby-koden min? Nøl ikke med å legge igjen en kommentar!

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!