Thomas lager verktøy [Luke 24, 2012]

Monday, December 24th, 2012
Ingen kommentarer

Thomas Kjeldahl Nilsson (@thomanil) er blant annet mannen bak Mine Verktøy, hvor han i drøyt et år publiserte en rekke intervjuer hvor norske geeks fortalte hvilket verktøysoppsett de hadde. Dagens blogpost – på selveste julaften – følger i det samme sporet. Thomas snakker om noe som er veldig viktig for en effektiv utvikler; kunsten å lage sine egne verktøy!

Thomas blogger også på kjeldahlnilsson.net.

colorPortrait

Hvem er du?
Trodde jeg skulle bli kjemiker på Blindern – men fant ut at jeg hadde mye lettere for programmering, gitt!

Hva er jobben din?
Partner og programmerer hos Gitorious AS – vi utvikler en fullstendig open source løsning for Git-hosting, og lever av å selge hosting og knallgod support til kunder over hele verden.

Hva kan du?
Pragmatisk allrounder som er spesielt glad i entreprenørskap, web-teknologi… og Emacs.

Hva liker du best med yrket ditt?
Å kunne skape noe ut av ingenting.


Verktøy for å lage verktøy: en Unix-safari

2239102681_bb9ca36abc_z

Jeg er fascinert av smiekunst.

Tradisjonelle smeder jobber med enkle, grunnleggende verktøy: esse, ambolt, hammer og tang. Smeden kan imidlertid lage mer effektive og spesialiserte verktøy ved behov. Det er såvidt jeg vet ingen andre håndtverkere som kan trekke seg selv opp etter bukseselene på denne måten…

unntatt oss programmerere. Med gode basis-verktøy kan vi bygge alt vi trenger for å jobbe raskere og mer effektivt.

I denne artikkelen skal vi se hvordan Unix-miljøer enkelt lar oss bruke basisverktøy til å bygge nye verktøy og arbeidsflyter. Vi jobber på tre nivåer: rett på kommandolinja, med shellscripting, og med Ruby.

Unix-idealene

Et av de beste utvikler-verktøyskrinene finner du i Unix-systemene (Linux, OS X, BSD, mfl): der får du mange små, fokuserte og gjenbrukbare byggeklosser du kan sette sammen til nye programmer. Med andre ord: de fleste verktøy du trenger enten finnes allerede, eller kan lages ved å kombinere allerede eksisterende verktøy.

Det er nyttig å omfavne — eller i det minste forstå — Unix-kulturen, for å absorbere en del prinsipper som har gjennomsyret denne tradisjonen gjennom flere tiår. Eric S Raymond oppsummerer disse prinsippene slik:

  • Modularitet: bygg enkle deler, koblet sammen med ryddige grensesnitt.
  • Klarhet: lettforståelige løsninger er bedre enn “lure knep”.
  • Komposisjon: design programmer slik at de kan kobles sammen med andre programmer.
  • Løskobling: skill kontrakt fra mekanisme, grensesnitt fra implementasjon.
  • Enkelhet: etterstreb enkelhet. Tillat bare kompleksitet der du er nødt.
  • Tilbakeholdenhet: bygg bare et stort program når det er helt nødvendig.
  • Transparens: gjør programmer synlige og åpne — det gjør testing og feilsøking enklere.
  • Robusthet: programmer som er enkle og åpenbare blir også mer robuste.
  • Representasjon: legg domenelogikk i dataen din, slik at selve programmet kan forbli rettfrem og robust.
  • Forutsigbarhet: grensesnittene dine bør være innlysende, ikke “overraskende”.
  • Stillhet: hvis et program ikke har noe nyttig å si, bør det holde kjeft og bare gjøre jobben sin.
  • Feilhåndtering: dersom programmet må feile, feil så tidlig og høylydt som mulig.
  • Økonomi: utvikler-tid er den mest verdifulle ressurssen din, så stress maskinen istedet for programmereren.
  • Generering: unngå å skrive ting for hånd. Forsøk istedet å skrive programmer som skriver programmer.
  • Optimalisering: prototyping før polering. Få det til å fungere før du optimaliserer det.
  • Mangfold: det er mange veier til Rom. Vær skeptisk til vedtatte sannheter og fasiter.
  • Utvidbarhet: design for fremtiden, den er her før du tror.

Disse prinsippene gjør deg til en bedre utvikler selv om du ellers ikke bruker Unix-systemer i det hele tatt.

Scenario: vi bygger en arbeidsbenk for email

Den beste måten å få dette til å synke inn er å bruke Unix-verktøy til å bygge noe konkret. Vi skal derfor bruke grunnleggende Unix-programmer til å bygge en enkel arbeidsflyt for å jobbe med Gmail-innboksen din.

Infrastruktur: installasjon av offlineimap og msmtp

Fundamentet for emailklienten vår blir verktøyene offlineimap (for å hente mail) og msmtp (for å sende mail).

Når disse er installert kommer vi til å ha en Maildir katalog som er synkronisert med Gmail-kontoen vår via IMAP, og vi kommer til å ha en enkel kommando for å sende mail rett fra kommandolinja. Begge verktøyene er tilgjengelig på både Linux, OS X og andre Unix-varianter.

Hvis du vil følge eksemplene mine på din egen maskin så bør du få dette til å fungere først. Hvis du bare vil lese resten av artikkelen kan du hoppe over denne seksjonen.

Merk: Jeg har bare testet oppsettet beskrevet nedenfor i Ubuntu — andre Unix-varianter kan være litt forskjellige.

offlineimap

I Ubuntu installerer du offlineimap slik:

sudo apt-get install offlineimap

Vi oppretter så en lokal Maildir katalog der vi vil at offlineimap skal legge den lokale kopien av gmail kontoen vår:

mkdir ~/Maildir

For å bruke verktøyet må vi konfigurere det til å snakke med vår egen gmail-konto. Vi oppretter fila ~/.offlineimaprc, og setter korrekte skrive/lese-rettigheter:

touch ~/.offlineimaprc && chmod 600 ~/.offlineimaprc

Vi fyller den med følgende innhold (oppdater til å matche din egen Gmail-konto):

[general]
accounts = gmail

# Kutt ut all output, med unntak av feilmeldinger
ui = quiet

[Account gmail]
localrepository = gmail-local
remoterepository = gmail-remote
status_backend = sqlite

[Repository gmail-local]
type = Maildir
localfolders = ~/Maildir/Gmail

[Repository gmail-remote]
type = Gmail
remoteuser = DIN_GMAIL_EMAIL_ADRESSE
remotepass = DITT_GMAIL_PASSORD

# Vi henter kun ned selve innboksen, ikke resten av folderne
folderfilter = lambda folder: folder in ['INBOX']

# Sletting lokalt skal ikke medføre sletting i Gmail-kontoen
realdelete = no

La oss teste om verkøyet fungerer som det skal. Kjør kommandoen offlineimap. Hvis konfigurasjonen din er korrekt så vil hele innboksen din plukkes ned og legges i katalogen Maildir i rota av hjemmeområdet ditt. Vær tålmodig: dette kan ta en stund hvis du har mye liggende i selve innboksen i gmail.

msmtp

I Ubuntu installerer du msmtp-pakken slik:

sudo apt-get install msmtp

Også dette verktøyet må konfigureres: vi oppretter fila ~/.msmtprc, og gir den korrekte skrive/lese rettigheter:

touch ~/.msmtprc && chmod 600 ~/.msmtprc

Vi fyller den med følgende innhold (oppdater til å matche din egen Gmail-konto):

defaults
auth on
tls on

account         gmail
host            smtp.gmail.com
port            587
from            DIN_GMAIL_EMAIL_ADRESSE
user            DIN_GMAIL_EMAIL_ADRESSE
password        DITT_GMAIL_PASSORD
tls_trust_file  /etc/ssl/certs/ca-certificates.crt

La oss teste om verkøyet fungerer som det skal. Kjør følgende kommando (bare bruk din egen mailadresse):

echo 'tester msmtp...' | msmtp -a gmail 'thomas@kjeldahlnilsson.net'

Sjekk så emailen din som vanlig. Dersom msmpt ble konfigurert riktig så skal du ha mottatt en mail fra deg selv nå med innholdet “tester msmtp…”

Fungerer alt som det skal? Flott, da fortsetter vi.

Kataloger, filer og strenger er alt vi trenger

Etter at vi har kjørt offlineimap første gang ender vi opp med en katalog ~/MailDir/Gmail, som inneholder underkataloger som representerer mapper og tilstander i hver mailfolder. Hver mail i disse folderne er lagret som en vanlig flatfil.

Folderstrukturen ser omtrent slik ut:

Maildir/
└── Gmail
    └── INBOX
        ├── cur
        │   └── 1352560840_1.25056.localhost,U=2,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,S
        ├── new
        │   ├── 1352560840_0.25056.localhost,U=1,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,
        │   └── 1352560841_0.25056.localhost,U=3,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,
        └── tmp

Filnavnene ser litt kryptiske ut, fordi offlineimap bruker filnavnene til å lagre en del informasjon om hver email: unike IDer, checksums etc. Filene under cur katalogen inneholder leste email, mens uleste mailer ligger under new.

Tilstand i denne lokale mailboksen synkroniseres til gmail-kontoen vår hver gang vi kjører offlineimap kommandoen. For eksempel kan vi flytte en email fra new til cur mappen og så kjøre offlineimap. Dette vil sette statusen på email til “lest” i Gmail-kontoen sentralt også.

Innholdet av en email ser ut omtrent slik:

MIME-Version: 1.0
Received: by 10.112.4.227; Sat, 10 Nov 2012 07:18:48 -0800 (PST)
Date: Sat, 10 Nov 2012 07:18:48 -0800
Message-ID: <CABQd01N+VFn89guaFCcBu+x=rJudno+yarZPZF1=CqWDz=sQnw@mail.gmail.com>
Subject: Import your contacts and old email
From: Gmail Team <mail-noreply@google.com>
To: Kensei Test Account <kensei.test@gmail.com>
Content-Type: multipart/alternative; boundary=00151748de9ec3315804ce259570

--00151748de9ec3315804ce259570
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.
     Import contacts and mail
=BB<https://mail.google.com/mail/#settings/accounts>

We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.

- The Gmail Team

Please note that importing is not available if you're using Internet
Explorer 6.0. To take advantage of the latest Gmail features, please upgrad=
e
to a fully supported
browser<http://support.google.com/mail/bin/answer.py?answer=3D6557&hl=3Den&=
utm_source=3Dwel-eml&utm_medium=3Deml&utm_campaign=3Den>
.

--00151748de9ec3315804ce259570
Content-Type: text/html; charset=ISO-8859-1

<html>
<font face="Arial, Helvetica, sans-serif">
<p>You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.</p>

<table cellpadding="0" cellspacing="0">
<col style="width: 1px" /><col /><col style="width: 1px" />
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
<tr>
  <td style="background-color: #ddd"></td>
  <td background="https://mail.google.com/mail/images/welcome-button-background.png"
      style="background-color: #ddd; background-repeat: repeat-x"
    ><a href="https://mail.google.com/mail/#settings/accounts"
        style="font-weight: bold; color: #000; text-decoration: none; display: block; padding: 0.5em 1em"
      >Import contacts and mail &#187;</a></td>
  <td style="background-color: #ddd"></td>
</tr>
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
</table>

<p>We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.</p>

<p>- The Gmail Team</p>

<p><font size="-2" color="#999">Please note that importing is not available if
you're using Internet Explorer 6.0. To take advantage of the latest Gmail
features, please
<a href="http://support.google.com/mail/bin/answer.py?answer=6557&hl=en&utm_source=wel-eml&utm_medium=eml&utm_campaign=en"><font color="#999">
upgrade to a fully supported browser</font></a>.</font></p>

</font>
</html>

--00151748de9ec3315804ce259570--

Hvorfor er dette nyttig?

Siden mailboksen vår er representert med vanlige kataloger, filer og strenger så kan vi bruke standard Unix-verktøy for å lese og manipulere email fra kommandolinja. Nå kan vi begynne å leke med verktøykassa vår!

“In The Beginning was the Command Line”

Unix-verktøy følger noen felles konvensjoner for å ta imot data og og sende resultater ut.

Programmer kjørt på kommandolinja tar data inn på STDIN strømmen, og spytter resultater ut igjen på strømmen STDOUT (med unntak av feilmeldinger , som spyttes ut på STDERR strømmen). Program-output ender enten i terminalen, eller kan sendes videre til et annet program.

Siden disse felles konvensjonene finnes kan vi kombinere programmer: Ved å kjede sammen flere verktøy på samme linje med | (pipe) mellom hver av dem, kan vi la data flyte gjennom dem etter tur som i et vannrør. Vi kan med andre ord enkelt bygge verktøy som består av pipelines av andre kommandoer.

Et eksempel: denne linja skrev jeg forrige uke for å finne alle sannsynlige synkroniserings-konflikter i Dropbox-mappa mi.

find ~/Dropbox | grep conflicted

Find-kommandoen lister opp alle filer, rekursivt, under den angitte katalogen. Hvert filnavn pipes videre til grep-kommandoen, som beholder de filstiene som har inneholder ‘conflicted’. Listen av filnavn ender så i terminalen min slik at jeg kan se hvilke filer jeg antagelig bør rydde vekk. Det finnes mer optimale måter å gjøre denne oppgaven, men poenget er at det tok meg bare noen sekunder å lage denne automatiseringen.

Å kjede sammen pipelines av filtere, transformasjoner etc føles svært naturlig dersom du allerede er komfortabel med å bruke map, filter og reduce-operasjoner i “ordentlige” programmeringsspråk.

La oss bygge noen verktøy for email.

Terminal-snutt: Send en mail

Denne har du kanskje allerede kjørt for å teste at msmtp fungerer.

echo 'Dette er body i en mail jeg sender fra terminalen' | msmtp -a gmail 'dinadresse@gmail.com'

echo skriver ut den påfølgende teksten til STDOUT. Hvis den står for seg selv så får vi teksten tilbake ut i terminalen. Her piper vi istedet teksten til msmtp, som bruker teksten den får inn på STDIN til som body i en ny mail.

Terminal-snutt: Sjekk antall uleste email

Følgende linje teller hvor mange uleste email vi har.

find ~/Maildir/Gmail/INBOX/new -type f | wc -l

Vi finner alle filer i new folderen (kun filer, ikke kataloger etc), og bruker wc til å telle hvor mange av dem det var. Jeg synes det er litt knapt å bare dumpe ut tallet, så jeg legger til litt beskrivende tekst også:

echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"

Nå har vi alt vi trenger for å lage en liten “widget” for å vise antall uleste email. Vi gjør det ved å ha et lite terminalvindu på desktopen vår, der vi kjører følgende kommando for å sette den igang:

watch -n10 'offlineimap && echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"'

watch kjører de påfølgende kommandoene hvert tiende sekund — vi synkroniserer først mailen vår, så kjører vi programsnutten vår for å telle uleste meldinger.

Nå har vi et terminalvindu som hele tiden ser slik ut, og blir oppdatert hvert 10 sekund:

Every 10.0s: offlineimap && echo "Uleste mail: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"          Fri Nov 30 21:14:11 2012

Uleste mail: 1

Terminal-snutt: Innboks-oversikt

En naturlig start er å lage oss et sammendrag av hvilke mail vi har liggende i innboksen vår. Her er en måte å skrive ut en liste over mailene våre:

grep -Rh ^Subject: ~/Maildir/Gmail/INBOX

Vi søker rekursivt (R) i innboks-folderen vår etter tilfeller av <Linjestart> Subject:. H-en er der for å kun skrive ut subjekt-linjen, ikke det kryptiske filnavnet i tillegg (grep skriver ellers ut både filnavn og hvordan treffet ser ut).

Dette bør treffe en gang i hver fil (vi går ut fra at dette er standardformatet på mailfilene og at det forhåpentligvis bare er en slik linje i hver fil). Disse matchende emne-linjene skrives ut til terminalen vår, og ser omtrent slik ut når vi kjører kommandoen vår:

➜  ~  grep -Rh ^Subject:  ~/Maildir/Gmail/INBOX
Subject: Get Gmail on your mobile phone
Subject: Import your contacts and old email
Subject: Customize Gmail with colors and themes

Nå har vi oversikt over innboksen vår.

Terminal-snutt: Les en mail

Vi bør også kunne lese en bestemt mail. Denne linja gjør det mulig å dumpe ut innholdet av mail nummer N fra lista ovenfor, telt fra toppen. Denne er noe mer innfløkt:

find ~/Maildir/Gmail/INBOX -type f | sed -n 2p | xargs cat

Vi finner alle filer rekursivt under innboksen vår. Vi plukker den nte linja i fil-lista (i dette tilfelle nummer to), og sender fil-stien til cat kommandoen, som bare dumper ut innholdet av den gitte fila i terminalen vår.

Disse email-kommandoene vi har laget her fungerer jo helt greit, men de er ikke så smidige å jobbe med hvis du trenger å bruke dem raskt og ofte. Det er på tide å ty til shellscripting for å forenkle og gjenbruke.

Sidespor: ta vare på småting du lærer og bygger

Jeg har problemer med å huske hendige terminal-snutter den første gangen jeg bruker dem. Her er noen grep som hjelper:

  • Du kan søke bakover i historikken din ved å trykke Ctrl-r. Hvis du begynner å skrive får du opp første mulige treff, og kan trykke pil-opp for å hoppe til neste kandidat som matcher søket bakover i historikken din. Og sørg for at terminalen din er satt til å bevare mye, eller all, historikk bakover i tid!
  • Personlige “cheatsheets”. Jeg har en orgmode-fil der jeg skriver ned kjekke one-liners, verktøy, program-snutter etc jeg kommer over — mens jeg jobber, fra artikler og bøker, fra kollegaer og så videre. Jeg er ikke så flink til å ta til meg nye greier på første forsøk, så jeg liker å skrive ting opp og vende tilbake til dem senere for å repetere og gjenoppdage.
  • Definer aliaser i terminal-miljøet ditt. Hvis du bruker Bash så oppretter eller oppdaterer du fila ~/.bashrc, og kan legge til linjer på denne formen:
alias helloworld="echo 'hello world'"

Når du har lastet miljøet på nytt vil du kunne bruke aliaset som en hvilken som helst annen kommando/script. Vi kan for eksempel forenkle en av linjene vi skrev ovenfor:

alias min_innboks="grep -Rh ^Subject: ~/Maildir/Gmail/INBOX"

Nå blir det litt enklere å sjekke mailboksen:

➜  ~  min_innboks
Subject: Get Gmail on your mobile phone
Subject: Import your contacts and old email
Subject: Customize Gmail with colors and themes

Når oneliners ikke strekker til banker shellscripting på døra

Vi kommer til et punkt der vi trenger mer faktisk programmering for å få ting gjort. Med andre ord: variable, conditionals, løkker, og ikke minst muligheten til å spre logikken over flere linjer.

La oss lage bash-scripts av operasjonene vi gikk gjennom i forrige seksjon. Da kan vi forbedre dem ved å gjøre dem tilgjengelige som kortere, parametriserte kommandoer.

All kode vi skriver her og i de neste seksjonene er forøvrig tilgjengelig for nedlasting.

Shellscript: Send en mail

Vi lager et script som heter send-email, som tar mottaker og mail-tekst inn som parametre.

#!/bin/sh

RECIPIENT=$1
TEXT=$2
echo $TEXT | msmtp -a gmail $RECIPIENT

Den aller første linja er en såkalt shebang som forteller hvordan scriptet skal eksekveres (i dette tilfellet sier vi at programmet er et shellscript). $1, $2 etc er variabler som bindes til å inneholde til shellscriptet. For lesbarhetens skyld tilegner vi dem til nye variable, før vi kjører samme kommando som vi gjorde ovenfor for å sende emailen.

Hvis du putter send-email i din egen path kan du kjøre den slik:

send-email thomanil@gmail.com "Sent from a tiny shellscript"

Dette verktøyet vårt ble litt mer brukervennlig nå, ikke sant?

Shellscript: Sjekk antall uleste email

Kommandoen vår for å holde styr på antall uleste mail gjør vi om til et script som heter watch-unread-emails, og ser slik ut:

#!/bin/sh

POLLING_INTERVAL=$1
watch -n$POLLING_INTERVAL 'offlineimap && echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"'

Kommandoen vår tar en parameter: antall sekunder mellom hver gang kommandoen skal kjøres/oppdateres. Slik kjøres den når scriptet er i pathen din:

watch-unread-emails 10

Shellscript: Innboks-oversikt

Vi lager en kommando som heter display-inbox for å liste opp innholdet i innboksen vår.

#!/bin/sh

grep -Rh ^Subject: ~/Maildir/Gmail/INBOX

Denne er bare den samme pipelinen vi tidligere skrev direkte inn i terminalen — bare mer brukervennlig siden vi slipper å huske den grep-kommandoen.

Når scriptet er tilgjengelig i pathen din kan du eksekvere det slik:

display-inbox

Shellscript: Les en mail

Linja vi tidligere lagde for å lese innhold av en bestemt mail var rimelig knotete, så det blir fint å forenkle litt.

#!/bin/sh

MAIL_NUMBER=$1
SED_COMMAND=$(printf "sed -n %sp" $MAIL_NUMBER)
find ~/Maildir/Gmail/INBOX -type f | $SED_COMMAND | xargs cat

Scriptet tar “mail nummer N fra toppen” som argument. Vi velger å bygge opp sed-kommandoen separat underveis for å gjøre det hele mer lesbart.

Når scriptet ligger i pathen din kan du kjøre det på følgende måte.

read-email 2

Helt klart enklere enn den kryptiske linja vi måtte skrive inn før.

Sidespor: gjør det enkelt for deg selv å scripte

Hvis du gjør terskelen for å skrive nye scripts så lav som mulig, så lager du også fler av dem og gjør derfor mer for å forbedre arbeidsflyten din. Her er to grep som hjelper:

  • Lag en katalog i hjemmeområdet ditt som er dedikert til nye scripts, f.eks ~/bin eller ~/scripts. Legg til denne katalogen i pathen slik at scriptene dine er tilgjengelige i miljøet ditt overalt. Bonus-poeng: lag en git-repo av script-katalogen din slik at har versjonskontroll, og hvis du jobber på flere maskiner, legg katalogen i Dropbox-folderen din og symlink dit fra alle maskinene du sitter på.
  • Lag et program som enkelt lar deg generere nye scripts. Under ser du mitt ~/script/generatescript bash-script. Det tar navn på nytt script som argument, oppretter det i standard-katalogen min (med riktige rettigheter), og fyrer opp standard-editoren min slik at jeg umiddelbart kan begynne å arbeide på det nye scriptet.
#!/bin/sh

SCRIPTPATH=~/scripts/$1
echo '#!/bin/sh
# Generated, add code here
' >> $SCRIPTPATH

touch $SCRIPTPATH
chmod a+x $SCRIPTPATH
$EDITOR $SCRIPTPATH

Når shellscripting blir for stygt sier skjønne Ruby hei

Perl kom til verden fordi Larry Wall syntes rå shellscripting ble for primitivt og lite ekspressivt. Etterhvert fikk vi også Ruby, Python, Groovy med fler, som sammen med Perl gjorde Unix-scripting langt mer behagelig.

Vi skal skrive om kommandoene våre til Ruby. Dette gir oss to umiddelbare gevinster: mer lesbare og utvidbare scripts og, for read-email kommandoen vår, parsing av email via et eksternt Ruby-bibliotek.

Ingenting av det vi gjør i Ruby-koden er umulig å få til i vanlige shellscripts — men Ruby er mer lettlest og vedlikeholdbart.

Ruby-script: Send en mail

Vi skriver om send-email shellscriptet vårt til Ruby:

#!/usr/bin/env ruby

if ARGV.length != 2
  puts "Usage: send-email TO_ADDRESS EMAIL_BODY"
  exit 1
end

recipient = ARGV[0]
text = ARGV[1]
puts `echo #{text} | msmtp -a gmail #{recipient}`

Vi starter scriptet med en ny “shebang”-linje, denne sier at systemet skal bruke ruby kommandoen i brukerens miljø til å eksekvere koden.

Vi legger også til en validering av at riktig antall parametre sendes inn til kommandoen. Argumenter til Ruby-programmer blir lagt i en konstant, global array som heter ARGV. Dersom det er for få eller for mange parametre skriver vi ut en kortfattet bruksanvisning og stopper videre kjøring.

Selve kjøringen av msmtp sheller vi ut til systemet. Dette er fordelen med å bruke Ruby og lignende språk: vi kan delegere ned til det underliggende systemet når som helst. Vi kan derfor velge hvor mye vi vil lene oss på vanlige Unix-verktøy kontra scriptspråkets egne biblioteker — det er ikke en “enten-eller” situasjon.

Ruby-script: Sjekk antall uleste email

Nestemann ut er watch-unread-emails scriptet vårt.

#!/usr/bin/env ruby

if ARGV.length != 1
  puts "Usage: watch-unread-emails POLLING_INTERVAL_SECONDS"
  exit 1
end

polling_interval = ARGV[0].to_i

while true
  new_mail_dir = File.expand_path("~/Maildir/Gmail/INBOX/new/*")
  unread_count = Dir[new_mail_dir].count { |file| File.file?(file) }
  puts `clear && offlineimap`
  puts "Unread emails: #{unread_count}"
  sleep polling_interval
end

Istedet for å lene oss på watch kommandoen så implementerer vi bare tilsvarende logikk selv i Ruby: skriv ut antall uleste email hvert nte sekund.

Vi tømmer terminalen for innhold og synkroniserer email ved å shelle ut til clear kommandoen og offlineimap. Selve antallet email finner vi ved å bruke Rubys egne file-apier istedet for find-kommandoen direkte.

Ruby-scriptet ble her en del lenger enn en det tilsvarende shellscriptet, fordi jeg kapper operasjonene opp i flere steg, variable og linjer. For meg føles dette noe mer utvidbart og lettlest enn det opprinnelige shellscriptet.

Ruby-script: Innboks-oversikt

display-inbox scriptet skriver vi bare om til Ruby for å være konsekvente: Ruby-versjonen sheller ut samme operasjon som tidligere. Jeg syntes at en enkelt grep-linje var helt greit, og det illustrerer at Ruby også kan fungere som en veldig tynn wrapper rundt ordinær shellscripting.

#!/usr/bin/env ruby

puts `grep -Rh ^Subject: ~/Maildir/Gmail/INBOX`

Ruby-script: Les en mail

Til slutt porter vi read-email til Ruby-kode.

#!/usr/bin/env ruby

if ARGV.length != 1
  puts "Usage: read-email EMAIL_NO"
  exit 1
end

#depends on the 'mail' gem, install like this: gem install mail
require 'mail'

maildir = File.expand_path("~/Maildir/Gmail/INBOX")
all_email_filepaths = Dir["#{maildir}/**/*"].select { |f| File.file?(f) }
mail_number = (ARGV[0].to_i)-1
mail_path = all_email_filepaths[mail_number]
mail = Mail.read(mail_path)
puts mail.text_part

Vi bruker Rubys fil-apier til å finne stien som inneholder den nte mailen fra toppen i innboksen. Så drar vi nytte av et eksternt Ruby-bibliotek (en såkalt gem) som heter Mail til å parse email-fila. Til slutt skrives mailen ut som html.

<html>
<font face="Arial, Helvetica, sans-serif">
<p>You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.</p>

<table cellpadding="0" cellspacing="0">
<col style="width: 1px" /><col /><col style="width: 1px" />
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
<tr>
  <td style="background-color: #ddd"></td>
  <td background="https://mail.google.com/mail/images/welcome-button-background.png"
      style="background-color: #ddd; background-repeat: repeat-x"
    ><a href="https://mail.google.com/mail/#settings/accounts"
        style="font-weight: bold; color: #000; text-decoration: none; display: block; padding: 0.5em 1em"
      >Import contacts and mail &#187;</a></td>
  <td style="background-color: #ddd"></td>
</tr>
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
</table>

<p>We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.</p>

<p>- The Gmail Team</p>

<p><font size="-2" color="#999">Please note that importing is not available if
you're using Internet Explorer 6.0. To take advantage of the latest Gmail
features, please
<a href="http://support.google.com/mail/bin/answer.py?answer=6557&hl=en&utm_source=wel-eml&utm_medium=eml&utm_campaign=en"><font color="#999">
upgrade to a fully supported browser</font></a>.</font></p>

</font>
</html>

Rå html-kode er ikke så lesbar, men vi kan kanskje bruke Firefox til å lese mailen?

read-email 2 > email.html && firefox email.html

Sidespor: shellscripting eller høynivå språk?

Er det lurt å holde seg så til så enkle verktøy som mulig, eller bør du alltid hoppe rett til det høyeste abstraksjonsnivået i verktøykassa?

Vel, du kan bygge hva som helst bare du har et turing-komplett språk — se for eksempel denne implementasjonen av Tetris i sed — men det er fint å kunne ta steget opp til mer ekspressive språk når vi har behov for det.

Fordelen med moderne scriptspråk som Ruby, Python etc er som nevnt over at de har mer behagelig syntax, og mange hendige eksterne biblioteker. De er også mer kryssplatform enn shellscripting, noe som gjør at du brått kan støtte mer enn bare Unix. For eksempel: ved å bruke Rubys fil-apier så kan scriptet ditt potensielt fungere på Windows også.

Ulempen med de moderne scriptspråkene er at de innfører flere eksterne avhengigheter: hvis du holder deg til shellscripting og standard Unix-verktøy så kan scriptet ditt fungere i minimale Unix-systemer uten å måtte installere flere eksterne pakker.

Det jeg ofte gjør når jeg skal skrive små programmer er å starte med noen enkle Unix-kommandoer i terminalen, og så trekke inn Ruby med en gang jeg ser at ting blir for komplekst for Bash.

Fjellvett: vend i tide, det er ingen skam å snu

Når du plukker opp nye byggeklosser på denne måten så ser du stadig flere løsninger på problemer, og det er fristende å bygge masse greier selv hele tiden. Men: bare fordi du kan bygge hva du vil fra bunnen av, er det ikke alltid lurt å gjøre det.

Vi må velge kampene våre. Noen ganger er det mer pragmatisk å gå ut og kjøpe et kanskje komplekst, suboptimalt, lukket verktøy… som faktisk gjør jobben uten at du trenger å bygge alt selv.

Det kommer an på situasjonen, tenk deg om før du hopper!

Ferris gjør event-sourcing i UI [Luke 23, 2012]

Sunday, December 23rd, 2012
Ingen kommentarer

Thomas Ferris Nicolaisen (@tfnico) har gjort noe spenende og lurt for å gjøre webapplikasjonen sin enklere å teste og å jobbe med – han har brukt event-sourcing! Gjennom illustrasjoner og videoer får du innblikk i hvordan dette fungerer.

2012-12-07 07.59.57

Hvem er du?
Gladlaks fra Mandal, deretter Java-konsulent i Oslo, deretter emigrant i Tyskland.

Hva er jobben din?
Systemutvikler hos Viaboxx GmbH

Hva kan du?
Mye forskjellig, med hovedvekt på Java, litt sånn DevOps-glad.

Hva liker du best med yrket ditt?
Å jobbe i en bransje hvor det er utrolig gøy og samtidig kjempeviktig å holde seg oppdatert.


Event-sourcing i GUI-laget

I min luke tenkte jeg det kunne være artig å vise frem litt ekte “German Engineering”. Dette er første gang vi snakker høyt om våre siste oppfinnelse(r), og jeg kan tenke meg at i enkelte kretser vil det kunne sees på som en revolusjonerende idé, pattern eller arkitektur.

På jobben min så går en god del av arbeidet ut på å lage GUI for trykkskjermer, ala billettautomater og minibanker. Helt spesifikt så lager vi software for pakkepost-automater, men disse har ikke tatt helt av i Norge ennå visstnok. Lesere i Oslo tafser jo en del på Flexus-automater har jeg hørt, så de av dere kan jo prøve å tenke dere et slikt brukseksempel.

 Sketch2832222

Vi pleide å utvikle GUI-biten i Flash/Flex en årrekke, men slet en del med det. Flex var bra på å lage et pent og snappy grensesnitt som passet fint for berøringsskjermer, men var tungvint i drift og utvikling. Mye av brukerens tilstand og forretningslogikk ble liggende i Flash-laget, hvor det var vanskelig med automatiserte tester og debugging.

I denne omgang så tenkte vi å prøve å lage noe med HTML5 og JavaScript, en såkalt single-page application.

hSAbfAyD-fX6h5gFBL7yFJ_7eIm9udpv7T9KR8EjWvGqRkfwr2udSM8mRNd0gX6mpg=s2000

Noe spesielt vi gjorde var å ha en event-basert, nær tilstandsløs browser, hvor eneste oppgave er å vise screens som kommer fra serveren. Jeg skal forklare hvordan dette foregår, men først vil jeg sette det i litt perspektiv:

Typiske web-applikasjoner er avhengige av å bygge opp en sesjon og følge en gitt navigasjons-sti for å havne i en gitt tilstand. Hvis du vil se hva “Mine bestillinger”-siden ser ut som, så må du logge inn og trykke på en lenke.

kdE-oLCeLLCG6AHuDuMmxsFX0vr_hbTtwt_fOLPHB0kzCflAddauvw0W4fyLShU7kg=s2000

Du er avhengig av riktig tilstand på både server og klient for i det hele tatt å kunne vise “Mine bestillinger” siden.

1DEsslgIvpDmavItDXlMJSDKye0NGMT46AyCg2dpR3nK2Cl1wZHPvVZajmKWVLs2VA=s2000

Dette mønsteret har gitt oss (og sikkert mange andre også) mye hodesmerte opp i gjennom årene. For mye tilstand og logikk på klientsiden gir rom for GUI-bugs som er vanskelig å gjenskape. Teste- og debuggingsverktøy på klientsiden er også gjerne under par det vi har til rådighet for koden på server-siden.

Det vi har gjort er å fjerne så godt som all brukertilstand fra klienten. All logikk ligger på serversiden.

wkakPEZqhGObnR41bm-Ye2zEeJjsLr-uoBQDed2FWW-9CUDgqcyrVxwhSHR-s3yjDw=s2000

Hvert eneste knappetrykk resulterer i en runde til serveren, hvor en tilstandsmaskin får beskjed om hva brukeren gjorde, for deretter å spytte tilbake en ny screen (logikken der inne er også veldig interessant, men det er ikke plass til dette teamet i denne bloggposten).

qczwS2hWWUTJAVrqOO99_detMl3xWPYJm1Kgwi-Bs9IybfAQpIVEph3Mw9WfR0FKOw=s2000

Dette høres kanskje laggy ut for en berøringsskjerm som skal være kjapp og reaktiv, men vi får det til på grunn av fire ting:

  1. Server kjører i samme automat (PC) som klient/browser, så det er er en meget kort tur over nettverket.
  2. Kommunikasjonen foregår over WebSockets, så events fra server får umiddelbar virkning.
  3. Noe smart caching og JavaScript for å oppdatere siden kjapt.
  4. Vi har veldig enkle screens med relativt lite innhold.

Hvordan ser egentlig en sånn screen ut? Her er et eksempel:

"decision": {
            "def": {
                "headline": "Please decide",
                "question": "Is yellow more green than blue?",
                "type": "decision"
                "events": [
                    {
                        "id": "yes",
                        "text": "Yes"
                    },
                    {
                        "id": "no",
                        "text": "No"
                    }
                ],
            }
        }

Ren JSON som representerer absolutt alt av tilstand nødvendig for å vise en skjerm til brukeren. Dette sender vi til browseren, som kjører det gjennom en generator (Mustache) så det blir om til HTML (som vi “swapper” inn og erstatter den gamle DOM’en). Så styler vi det opp med CSS og vips så ser det slik ut:

VyjkcLUZVtMtntceg8EwzaYxSDYKmnuVXtPkKtLDHK-WwCJZTNcbw-qkjTUrnB6rHA=s2000

Det som er utrolig digg å ha denne løse koblingen mellom hva som skal vises, og hvordan det blir generert.

Fordeler med dette da?

Fikse på webdesign uten å måtte trykke masse rundt i applikasjonen.

Du behøver ikke jobbe deg til riktig tilstand for å se på det du vil designe. Du trenger bare et snupp JSON (som den over). Her har jeg fyrt opp vår lille screen-IDE, den kjører på en lokal liten Node-server (se i høy oppløsning):

Man kan velge fra noen forhåndsdefinerte JSON-eksempler, eller man kan copy/paste inn fra loggfiler, og man kan justere innholdet for hånd for å få den tilstanden man vil se.

Dette gir ekstrem kjapp feedback til webdesigneren. Etter å ha endret CSS (eller LESS som vi bruker), Mustache-template eller Javascript trenger man bare å trykke F5/Refresh i browseren for å se screen’en bli rendret på nytt med det siste innholdet. Man kan også simulere enkle server-side events med litt JavaScript.

Man kan teste GUI akkurat i hvilken grad man vil

Tradisjonelt er det mange ulemper med å teste applikasjonen gjennom GUI, men mange gjør det fordi de har ingen annen måte å teste GUI på.

Vi kan derimot teste at..

  • riktig screen object blir produsert av tilstandsmaskinen (ren unit-test)
  • forventet JSON blir spyttet ut av gitt screen  (ren unit-test)
  • riktig HTML blir produsert fra gitt JSON (men da må man ha en headless browser som kan kjøre Javascript, ala PhantomJS)
  • HTML fra en screen ser OK ut (man kan ta screenshots med PhantomJS)
  • HTML fra en screen ser akkurat riktig ut (men da må man bruke produksjonsbrowseren (Chrome) på riktig operativsystem og ta screenshots med Selenium/WebDriver)

Alle disse typer tester gjør vi i forskjellige grader: de øverste veldig mye, og i mindre grad dem lenger ned på listen fordi de er dyrere ressursmessig, og brekker lettere.

Det som hjelper en del er at vi kan skille mellom (a) kommer det riktige innholdet ut (screens) fra serveren, og (b) blir en gitt screen seende riktig ut i browseren. Denne delingen gjør at vi kan holde testene våre utrolig kjappe: Rene unit-tester tester tilstandsmaskinen og screensene som spyttes ut, mens forskjellige browser-tester lager screenshots for screens.

Man kan også teste hele stacken sammen, eller i faser. Vi gjør sistnevnte når vi genererer dokumentasjon og screenshots basert på story-tester (men vi mocker ut hardware og tjenester):

  • Story-tester genererer dokumentasjon med JSON der screenshots skal legges inn
  • Dokumentasjonen kjøres gjennom et script, hvor JSON blir rendret til ekte screenshots.

Det hele går utrolig kjapt, og man trenger fortsatt ikke fyre opp noen ekte applikasjonsserver – det holder med Node simulatoren.

Se nøyaktig hva brukeren så på skjermen på et gitt tidspunkt

La oss se for oss dette scenarioet: Noen leverer en pakke i en automat, sender hente-kode over SMS, mottaker kommer og henter pakke:

Siden vi logger det meste som skjer på en automat, inkludert hvilke screens som sendes, så burde det jo være en smal sak å lese gjennom en hel loggfil, og så generere et hendelsesforløp over hva som har skjedd på en automat den siste tiden, og kunne spille av gamle screens på nytt.

Så vips, etter et par dager med utvikling:

Ikke bare kan vi spole frem og tilbake i hva brukeren opplevde i villkårlig tempo, vi kan også kopiere screens ut herfra for så å jobbe med dem i simulatoren.

Det skulle ikke mye kode for å få dette til: en loggparser og en side JavaScript. Den flotte timeline-widgeten er laget med dette Timeline-biblioteket.

Ennå kan vi ikke se hvor brukeren trykker på skjermen, men vi jobber i disse dager med å få til det også.

Nå forstår du kanskje hvorfor tittelen på denne posten er Event Sourcing.

Vanligvis så bruker man Event Sourcing for events i domenelaget, for ting som pakkesporing og sånnt, men det er sannelig utrolig mektig å bruke det i  GUI-laget også.

Konklusjon

Det er ikke sikkert at denne arkitekturen passer for alle applikasjoner, men vi har lekt med tanken av å bruke det videre i ekte web-applikasjoner, spesielt hvis det er interaksjon av typen veiviser/wizard eller lignende, slik som vi har på automatene våre.

Videre er det utrolig gøy å jobbe med denne teknologi-stacken. Det går knapt en sprint uten at vi jubler over hva vi har fått til, og hvor relativt enkelt det var å få til. En viktig del av det er nok at vi har skrudd sammen mesteparten selv, og ikke bundet oss fast i noe svært rammeverk.

Håper dette gir dere noe å tygge på i ferien. God jul og godt nyttår!

Bjørn Einar strømmer data [Luke 22, 2012]

Saturday, December 22nd, 2012
3 kommentarer

Bjørn Einar Bjartnes (@bjartnes) jobber sammen med Einar W. Høst (som dere møtte 3. desember) i Computas, og er en spennende fyr med spennende interesser. Du kan lese om hva han holder på med på bjartwolf.com.

I dagens luke, på lille, lille julaften, drar Bjørn Einar frem Node.js og sitt radiostyrte helikopter, og snakker om å strømme data.

beb

Hvem er du?
Automasjonsingeniør fra oljebransjen som har gått i land og begynt å kode

Hva er jobben din?
Overingeniør hos Computas AS, der jeg blant annet er fagnettverksleder for Samhandling og portaler. Utvikler stort sett SharePoint-løsninger for olje- og gassbransjen.

Hva kan du?
Jobber nå mest med SharePoint prosjekter, så jeg har mest fokus på å bruke SharePoint som produkt i samspill med andre typer webløsninger.

Hva liker du best med yrket ditt?
Vi er en gjeng på jobben som brenner for programmering litt utover det normale. Elsker å henge med dem og kode sære ting for moro skyld.


Omfavn strømmene!

Jeg har fått skikkelig dilla på strømmer i det siste.

Jeg har ingen helt presis definisjon på hva strømmer er, det kan bety litt forskjellige ting i forskjellige sammenhenger, alt fra IO-baserte strømmer til doven evaluering av sekvenser. Det har stort sett å gjøre med data som kommer over tid, i en strøm, som i en vannslange eller på et samlebånd. Jeg har hørt det omtalt som å jobbe med data i bevegelse, og synes det er en god beskrivelse.

Mange vil kjenne strømmer fra IO, der man leser og skriver strømmer til og fra konsollet og filer. Da jeg begynte å programmere tenkte jeg mest på strømmer som noe som kom i veien. Jeg skal egentlig bare ha innholdet i en fil, men er nødt til å forholde meg til en strøm av data. Hvorfor kan jeg ikke bare få innholdet fra filen? Etter at jeg begynte å leke med Node.js forstod jeg plutselig at strømmer egentlig er morsomme å jobbe med.

Istedenfor å bruke mye plass på å forklare forskjellige strømmer, så tenkte jeg heller å viste et eksempel som jeg liker.

Eksempelet er delt i to. I det første eksempelet vil jeg logge navigasjonsdata fra et quadcopter (eller drone på godt norsk) til en zip-fil. Det andre eksempelet leser tilbake fra zip-filen og skriver navigasjonsdata ut til konsollet. Hensikten min er å kunne logge data fra noen ekte flyturer og så kunne spille dem tilbake, så jeg kan teste ut forskjellige programmer til quadcopteret uten å faktisk fly det.

CycloneCloseup1

Helt tilfeldig valgt bilde av et Quadcopter

Noen ord om dette quadcopteret er på sin plass. Den kommer med et åpent API i C, som igjen har en Node.js modul som eksponerer APIet gjennom enkle funksjoner. Dronen setter opp sitt eget trådløse nett, som man kan koble seg til. Data fra dronen kommer som eventer i JavaScript. Disse eventene, navdata eventer, innholder data som status på dronen, verdier på måleinstrumenter osv. En kan også skrive kommandoer tilbake for å styre dronen, men det sparer jeg til en senere gang.

Strømme data til fil

Målet her er altså å gjøre om vanlige eventer til en strøm av data, slik at det kan pipes videre inn i moduler som forstår strømmer. Det artige er at dronen vil begynne å skrive til en zip-fil mens den fortsatt flyr. Det er dette jeg har illustrert i tegningen (nedenfor), der man ser eventene komme inn i den den gule roboten som symboliserer ar-drone-modulen; det serialiseres, pakkes og skrives til fil etterhvert som pakkene kommer inn.

100002010000034A00000254A369A25C 

var arDrone = require('ar-drone');
var gzip = require('zlib').createGzip();
var bacon = require('baconjs').Bacon;
var Stream = require('stream');
var fs = require('fs');
var drone = arDrone.createClient();

// Lager en strøm av eventer fra navdata
var eventStream = bacon.fromEventTarget(drone, 'navdata');

// Lager en strøm for å skrive ut JSON-data og ny linje for
// hvert navdata-event fra dronen
var jsonStream = new Stream();
jsonStream.readable = true;
eventStream.onValue(function(x) {
    jsonStream.emit('data', JSON.stringify(x) +"\n");
});

// Kobler alle strømmene sammen
jsonStream.pipe(gzip).pipe(fs.createWriteStream('logdata2.gz'));

15 linjer kode for å snakke med et quadcopter og skrive dataene i sann tid til en komprimert fil som JSON. Noen ganger er Node.js veldig moro.

Strømme data fra en fil

Hensikten med dette lille programmet er å spille tilbake logfilene i ønsket hastighet. Dette kan være nyttig om man ønsker å teste ut noe kode som skal lese fra helikopteret uten å fly, lade, kræsje etc. Planen er å lese tilbake ett og ett datapunkt hvert 10 ms.

Her tenkte jeg å vise noen spesielle mekanismer for strømmer. Det ene er å bremse strømmer – i Node.js gjøres dette via en mekanisme kalt backpressure. Det gjør at om en strøm ikke klarere å ta unna data fort nok, vil de strømmene som sender data til den opptatte strømmen vente til de får beskjed om at strømmen er klar til å ta imot mer. Alt som trengs er at en strøm returnerer false når den blir skrevet til for å signalisere at den ikke kan ta unna mer, og at den sender eventet drain når den er klar for mer.

Her kunne en tenke seg å bruke timestamps e.l, men for enkelhets skyld spiller jeg tilbake hvert 10 millisekund. Jeg laget den trege strømmen i en egen node-modul:

var Stream = require('stream');

// Her lager vi en stream som bremser hver gang den får data
// og venter litt med å si at den er klar igjen
// Jeg ser på det som et skikkelig trangt rør

var SlowStream =function (delayInMs) {
    var slowStream =new Stream();
    slowStream.writable =true;
    slowStream.write =function(val) {
        slowStream.emit('data', val);

        // Hvis en stream returnerer false, vil alle
        // oppstrøms vente til den emitter drain

        setTimeout(function () {
            slowStream.emit('drain');
        } , delayInMs);

        return false;
    };

    slowStream.end =function(val) { slowStream.emit('end'); };
    return slowStream;
}

module.exports = SlowStream;

En annen mekanisme jeg bruker er fra rammeverket bacon.js. Bacon lar meg lage eventstrømmer, og gir et sett av operasjoner jeg kan gjøre på disse strømmene. Her bruker jeg map og filter. Filter fungerer som filter ellers, ypperlig forklart i kjempekjekt-bloggen tidligere i år. Den tar en predikat, og kun data som tilfredstiller predikatet får slippe videre.

Map fungerer også som ellers, tar ett og ett element, transformerer det, og sender det videre. Den eneste forskjellen fra tradisjonell map og filter her er at det brukes på data som ennå ikke finnes. Filter bruker jeg for å kun hente ut de navdata-verdiene hvor dronen er i flymodus og høyden er over 1 cm. Jeg bruker map her for å hente ut noen få verdier; for hvert nav-data element, som inneholder ganske mye data, så vil jeg ha et nytt objekt som kun innneholder høyde og retning på dronen.

Jeg bruker også en strøm, linestream, som streamer filer linje for linje basert på en filstrøm. Totalt sett så er flyten i programmet som følger:

  1. Les fra fil
  2. Pipe det til unzippedstream
  3. Pipe det igjennom en linjestrøm
  4. Pipe det igjennom en treg strøm
  5. Map en JSON parser på strømmen
  6. Filtrer ut elementene der dronen flyr over 1 cm høyt
  7. Plukk ut (ved hjelp av map) et par verdier vi er interesserte i
  8. Skriv disse dataene ut til konsollet
var linestream = require('linestream');
var SlowStream = require('./slowStream.js');
var bacon = require('baconjs').Bacon;
var unzip = require('zlib').createGunzip();
var fs = require('fs');

var unzippedStream = fs.createReadStream('logdata.txt.gz').pipe(unzip);
var slowStream =new SlowStream(10);
linestream.create(unzippedStream).pipe(slowStream);

// Lager en eventstrøm av den treige strømmen
var navDataStream = bacon.fromEventTarget(slowStream, 'data');

// Strømmen må parses fra JSON daten i filen
// Det finnes biblioteker for å gjøre dette som en strøm, men de respekterer
// ikke backpressure riktig
var parsedStream = navDataStream.map( function (val) {
    return JSON.parse(val);
});

// Tar en strøm av navdata og returnerer kun data der dronen er i flymodus og
// høyden er over 1 cm
var flyingStream = parsedStream.filter(function (navdata) {
    return navdata.droneState.flying ===1&& navdata.demo.altitudeMeters >0.01;
});

// Tar inn navdata og returner et objekt med høyde og retning
var heightAndAltitudeStream = flyingStream.map(function (navdata) {
    return { height: navdata.demo.altitudeMeters,
             direction: navdata.demo.clockwiseDegrees };
});

heightAndAltitudeStream.onValue( function (x) { console.log(x); });

Quadcopter log from Torbjørn Marø on Vimeo.

Se også video av Bjørn Einar som flyr quadcopteret sitt.

En liten avslutning

Dessverre betaler ikke kundene mine meg særlig godt for å leke med quadcopteret mitt. Om du ikke allerede har en drone i hus, bør du selvfølgelig ønske deg en til jul. Kanskje klør du i fingrene etter noe å bruke strømmer av data til som er litt mer praktisk, og da er kanskje ikke dette det mest matnyttige eksempelet. I så fall kan jeg anbefale å ta en titt på Reactive Extensions for JavaScript eller .NET, eller kanskje en litt mer konkret anvendelse, som Reactive UI.

Måtte de harde pakkene strømme til et juletre nær deg.

God jul!


Noen flere lenker: Bjørn Einar’s reactive-drone prosjekt | Streams intro for Node | Gratis Rx-bok | Kjøp din egen drone | Node-modulen til AR dronen | Bra Rx-presentasjon på YouTube

Christian om funksjonell programmering [Luke 21, 2012]

Friday, December 21st, 2012
3 kommentarer

Christian Rødli Amble bør være kjent for de mest trofaste leserne av denne bloggen. Han har bidratt med mange gode kommentarer til blogpostene mine de siste årene, og da jeg skulle finne 24 gjestebloggere til å fylle julekalenderen i år, føltes det som en selvfølge å spørre om Christian ville være med.

Han har bidratt med en nokså avansert artikkel. Han snakker om den beryktede monaden, og jeg må innrømme at jeg synes det er vanskelig å henge med. Tar du utfordringen?

Amble

Hvem er du?
En som syntes «Real Programmers Don’t Use Pascal» var litt for kul for 6-7 år siden.

Hva er jobben din?
Programmerer ved Tingtun AS i Lillesand.

Hva kan du?
Funksjonell programmering og algoritmer.

Hva liker du best med yrket ditt?
Å finne den passende abstraksjonen for problemet for hånden, og bruke denne til å løse problemet på en effektiv måte.


Funksjonell programmering og hva det kan gjøre for deg.

Jeg har programmert i Haskell i et par år nå, og jeg mener dette er Veien, Lyset og Frelsen. Dette er et forsøk på å forklare hvorfor for en uinitiert.

Denne bloggposten inkluderer også min monadetutorial. Å skrive en slik en når en føler at en endelig har skjønt det er et lite “rite of passage” for Haskell-programmerere, og jeg har ikke fått laget en før nå.

Hva det er.

Haskell er et rent funksjonelt sterkt og statisk typet programmeringsspråk med ikke-striks semantikk.

Rent funksjonelt vil si at du ikke kan ha sideeffekter. En funksjon må alltid returnere det samme hvis den blir gitt de samme argumentene, og kan ikke gjøre noe annet enn å regne ut sitt resultat. Dvs. ingen input/output, ingen modifisering av variabler, og i hvert fall ingen GOTO-er.

Sterk og statisk typing regner jeg med de fleste skjønner, men Haskell har også typeinferens som gjør at en sjeldent trenger å nevne typene eksplisitt. Ellers er typesystemet ganske avansert med typefunksjoner, typeklasser, og mye annet snacks, som til sammen blir til et turingkomplett programmeringsspråk som ligger oppå programmet ditt og passer på at du ikke finner på noe tull. Hvis du absolutt vil kan du lage deg full aritmetikk på typenivå, og det finnes et eksempel på quicksort i det og.

Ikke-striks semantikk vil si at uttrykk reduseres fra utsiden og inn. Har du (a+(b*c)) reduseres først +, så a eller (b*c). Du vet ikke rekkefølgen argumentene blir evaluert i, og må programmere for å ta hensyn til dette. Dvs. har du (f(x) && g(y)) aner du ikke om f og g blir evaluert i den rekkefølgen, samtidig, eller i det hele tatt hvis en av dem gir False. Det er helt opp til kompilatoren og definisjonen av &&, selv om de som oftest tar et hint og gjør hva du tror. Siden f og g uansett ikke har lov til å ha sideeffekter som kan endre deres verdier så spiller ikke dette så stor rolle. De fleste Haskell-kompilatorer implementerer ikke-striks semantikk i form av lat evaluering, dvs. den evaluerer noe når den trenger det.

Så hvorfor?

Det over sier hva Haskell er ved å sammenligne med andre typer språk, og så si hva det ikke er. Du må hele tiden forholde deg til en steril og matematisk verden et godt stykke unna hvordan programmet ditt faktisk kjøres, og med typesystemet som hele tiden passer på deg blir BDSM-opplevelsen komplett. Så hvorfor vil jeg utsette meg for dette?

Sjekket evalueringsrekkefølge.

Du fjerner en del bugs ved å ikke tillate modifisering av variabler:

> list = [3,2,1]
> inPlaceSort list -- Dvs. inPlaceSort(list), Haskell utelater parenteser i funksjonskall.
> first = head list

Hvis du bytter plass på linje 2 og 3 vil programmet fortsatt fungere og ha riktige typer, men resultatet blir feil. Derimot:

> list = [3,2,1]
> sortedList = sort list
> first = head sortedList

Å bytte rekkefølge nå i et imperativt språk vil fortelle deg at du prøver å referere til variablen sortedList før den blir satt. Siden variabler uansett ikke kan endres i Haskell blir alle satt på en gang, og koden over gir riktig resultat selv med linje 2 og 3 byttet, mens det første eksemplet er umulig å skrive på en gyldig måte.

Høyere rangs funksjoner.

Se e.g. på disse funksjonene:

> sum xs = if null xs then 0 else head xs + sum (tail xs)
> product xs = if null xs then 1 else head xs * product (tail xs)

Her er sum-funksjonen i python hvis du har problemer med å skjønne syntaksen:

>>> def sum(xs):
>>>     if len(xs) == 0:
>>>         return 0
>>>     else:
>>>         return xs[0] + sum(xs[1:])

Det er et mønster som går igjen: de eneste bitene som skiller funksjonene er hva en returnerer hvis listen er tom, og hvilken operator en setter mellom elementene i listen. I Haskell er funksjoner førsteklasses objekter på lik linje med tall og strenger, og de kan sådan gis som argumenter til andre funksjoner, og en operator er bare en helt vanlig funksjon. Funksjoner som tar andre funksjoner som argumenter kalles høyere rangs funksjoner, og brukes ofte i funksjonelle språk, hvilket gir bedre modularitet.

Det mønsteret over kalles i Haskell for foldr, og er definert slik:

> foldr f z xs = if null xs then z else f (head xs) (foldr f z (tail xs))

sum og product kan da defineres ut fra foldr:

> sum xs = foldr (+) 0 xs
> product xs = foldr (*) 1 xs

foldr er tidligere omtalt på denne bloggen som reduce, sammen med eksempler på andre høyere rangs funksjoner.

Men i motsetning til de fleste andre språk så kan ikke funksjoner ha sideeffekter, og en er derfor garantert at de følger spillereglene.

Matematisk og elegant

(Advarsel: “litt” komplisert)

Haskell er som nevnt et veldig matematisk språk, og mange av konseptene i standardbibliotekene kommer fra kategoriteori. Du må ikke kunne kategoriteori for å kunne Haskell, men det hjelper litt da det er omtrent som ofte brukte designmønstre for funksjonelle språk, for ikke å nevne at det er veldig kult. Her tenkte jeg å forklare noen av de mest sentrale konseptene, functorer og monader.

Men først må vi innom typesystemet litt:

> 1 :: Int

:: vil si “har type”, dvs. her sier en eksplisitt at 1 er av type Int. Det på venstre side av :: er vanlig kode og det på høyre side er typesignaturen.

Konkrete typer og typekonstruktører begynner på en stor bokstav (utenom [] som også er en konkret typekonstruktør).

> [1,2,3] :: [Int]

[Int], en liste med heltall, kan også skrives litt mer eksplisitt som ([] Int). Her er [] en typekonstruktør som tar ett argument, en annen type (Int over), og returnerer en type ([Int]). Dette korresponderer til List<int> i C#.

Typer og typekonstruktører har også typer:

> [] :: * -> *
> [Int] :: *

Typen til en type eller en typefunksjon/typekonstruktør kalles for “kind.” * uttales som “type”, så det over sier at [] er en typekonstruktør som tar en type som argument og returnerer en type.

> map :: (a -> b) -> [a] -> [b]

a -> b vil si “er en funksjon som tar a som argument og returnerer b“. Når det er små bokstaver i en typesignatur er det typevariabler. Disse kan stå for hvilke som helst typer og typefunksjoner. Koden over beskrives slik: “map er en funksjon som tar en funksjon fra a til b som første argument, en liste med a-er som andre argument, og returnerer en liste med b-er, der a og b kan være hva som helst.”

> map (\x -> x + 1) [1,2,3] -- [2,3,4]

\ uttales lambda og lager en ny funksjon. I eksemplet over mappes en funksjon som tar x som argument og returnerer x + 1 over en liste.

Functor

En `Functor` er en generalisering av `map` over datatyper som tar ett argument. I kategoriteori heter det egentlig en endofunctor.

> fmap :: Functor f => (a -> b) -> f a -> f b

Det som kommer før => fungerer omtrent som en predikatfunksjon som begrenser domenet til f til å bare gjelde instanser av Functor. Functor her er en typeklasse.

> Functor :: (* -> *) -> Constraint

Dvs. Functor er en typefunksjon som tar en typekonstruktør fra type til type som argument og returnerer et Constraint, en begrensning på typevariablene i resten av signaturen. En instans av en typeklasse for en datatype lages ved å definere noen funksjoner, for Functor en fmap for en verdi av f.

Hvis en bytter ut f med [] i typen til fmap så blir typesignaturen den samme som for map. [] er en instans av Functor og fmap for lister gjør virkelig det samme som funksjonen map.

> fmap (\x -> x + 1) [1,2,3] -- [2,3,4]

Du kan selvsagt lage dine egne datatyper, og gjøre dem instanser av Functor ved å gi dem egne definisjoner av fmap.

IO

`IO` er en datatype som enkapsulerer en handling som skjer i kontekst av virkeligheten utenfor programmet. Inni en IO a er det en funksjon som tar en spesiell unik RealWorld-variabel som representerer virkeligheten og alt i den, og returnerer noe av type a og en oppdatert utgave av virkeligheten. Siden funksjoner i Haskell må returnere det samme så lenge argumentene til de er de samme tillater det at e.g. getLine, les en linje fra stdin, returnerer forskjellig streng hver gang siden den får en forskjellig virkelighet som argument å lese fra hver gang. Det er selvsagt bare teoretisk, men det har ikke hindret noen i å lage et bibliotek som manipulerer denne variabelen. Det virker dessverre ikke, selv om jeg er hellig overbevist om at gud skrev universet i Haskell.

> getLine :: IO String -- Les en String fra terminalen.
> print :: Show a => a -> IO () -- Ta noe det går an å vise og print det til terminalen. (Show er noe alla __repr__ i Python)

Alle Haskell-programmer har én slik `IO`-handling som kjøres når programmet starter, nemlig `main`-funksjonen av type IO (). (), uttalt `unit`, brukes ofte når en vil returnere ingen ting men har en typevariabel å fylle inn. IO () er altså en handling som bare utfører interaksjoner med virkeligheten.

Monad

Så langt kan vi nok til å lage et program med

> main = print 5

og få et flott 5-tall på skjermen når det kjøres. Men det er ikke så veldig oppmuntrende for vår alles kommende Haskell-karriere.

Hvis du har fulgt veldig godt med og er usedvanlig flink til å tolke hva jeg forsøker å kommunisere, tenker du kanskje “kan det være at `IO` er en `Functor`? Kan en da kanskje `fmap`-e `print` over `getLine` og få printet resultatet?”

I så fall, godt tenkt!, men ikke helt. Alle `Monad`er er også `Functor`er, og `IO`-instansen gjør det forventede, nemlig mapper funksjonen på resultatet av handlingen, men den resulterende typen er dessverre ikke helt hva en skulle håpe på:

> fmap print getLine :: IO (IO ())

Det er altså en `IO`-handling som returnerer en `IO`-handling. En trenger noe å flate det ut med, og det er her `Monad`er kommer inn:

> join :: Monad m => m (m a) -> m a
> join (fmap print getLine) :: IO ()

En `Monad`e er altså noe som setter sammen to like datatyper inni hverandre på en utflatende måte. For `IO` setter den det sammen i sekvensiell handling, først den ytterste så den innerste.

Lister er også `Monad`er, lite overraskende implementert som `concat`.

> join [[1,2,3],[4,5,6]] -- [1,2,3,4,5,6]

Å sende innholdet i en `Monad`e inn i en funksjon som `print` er såpass vanlig at det finnes en egen funksjon for det:

> a >>= f = join (fmap f a) -- >>= uttales "bind"
> (>>=) :: Monad m => m a -> (a -> m b) -> m b

Faktisk er det denne som definisjonen av en `Monad`e er gitt i termer av i Haskell, mens `join` er mer kategoriteoretisk. De kan uansett defineres ut fra hverandre, men jeg synes `join` gir en bedre og mer intuitiv forståelse av hva `Monad`er faktisk er, nemlig et monoid i kategorien av endofunctorer.

> main = getLine >>= print -- Les en linje og print den ut igjen.

Navnet `bind` kommer fra at en tenker en binder resultatet til en funksjon. Merk at `bind` for lister er det samme som concatMap (SelectMany i C#):

> words :: String -> [String] -- Splitt en streng på mellomrom.
> fmap words ["dette er", "en test"] -- [["dette","er"],["en","test"]]
> ["dette er", "en test"] >>= words -- ["dette", "er", "en", "test"]

Det blir ganske slitsomt å skrive en haug med >>=-er, og derfor har Haskell do-notasjon, som er syntaktisk sukker for akkurat dette:

> main = do putStrLn "Skriv navnet ditt:"
>           navn <- getLine
>           putStrLn ("Hei, " ++ navn ++ "!")

Er det samme som:

> main = putStrLn >>= (\_ -> getLine >>= (\navn -> putStrLn ("Hei, " ++ navn ++ "!")))

Poenget

Haskell-kode er på et høyere abstrakt nivå. Monader er et design-mønster en kan bruke til å blant annet lage en konkretisering av abstraksjonen som gjeninnfører imperativ programmering. Det er som om semikolonene i et vanlig programmeringsspråk kunne modifiseres til å gjøre hva en ville med uttrykkene på hver side. Det gir et utrolig kraftig verktøy, og myriaden av monadene som finnes gir en god indikasjon på det: Det kan brukes til å lage parsere, mange forskjellige DSL-er, GOTO, continuation passing style som ser ganske rett frem ut, korutiner, ikkedeterminisme, osv., osv.

Med noe som heter monadeomformere går det også an å kombinere funksjonalitet fra flere monader inn i en. Trenger du en blanding av parser og korutine som også kan kjøre IO? Ikke noe problem, bare stack dem oppå hverandre og løft funksjonene du bruker opp på riktig nivå.

Et eksempel: Allegro, bruk av fantomtyper til å holde orden på kontekst i en spesialisert monade.

Allegro er et bibliotek for å lage 2D-spill, med støtte for bildemanipulasjon, tekst-output, lyd, MIDI, og input fra tastatur og joysticker. Jeg begynte på et lite spill som brukte SDL, men kom meg ikke over at det ikke gikk an å blitte til transparente overflater, så da var det bare å finne på noe nytt, og Allegro så veldig egnet ut. Jeg har allerede laget rå bindinger til C-funksjonene, og pusler så smått med en litt mer Haskellifisert kokt utgave.

I C-API-et har en en funksjon som heter drawLine der du gir den noen kordinater, en farge og en tykkelse, og så tegner den en linje på hvaenn slags bitmap du forhåpentligvis har fortalt den at det skal tegne på. Har du ikke gjort dette? Segfault. Prøver du å kalle getJoystick uten først å kallt installJoystick for å initialisere systemet? Segfault. Noe som helst allegro-relatert uten å først kalle initialize? Segfault. Dette går igjen i hele API-et.

God Haskell-ånd er å gjøre runtime-feil om til kompileringsfeil med typesystemet. Idéen jeg har er å lage et tynt lag oppå `IO`-monaden som holder en fantomtype, dvs. som bare finnes i typesystemet. Denne fantomtypen inneholder hva som er i konteksten, og med disse typepredikatfunksjonene sørger en for at drawLine bare kan kalles når Bitmap er i der, dvs. det er noe å tegne på. Et par nye funksjoner som withBitmap kjører handlinger med en bitmap i konteksten. En funksjon som heter runAllegro henter ut handlingene og returnerer dem som IO. Denne er polymorfisk på forskjellige kontekster som har installX-funksjoner, og kaller automatisk disse i riktig rekkefølge så funksjonene kan brukes trygt.

Dermed blir det mindre en trenger å huske på, og samtlige ubehagelige segfaults fjernes.

Annet

Ting jeg ikke har tatt med, men som også er gode grunner til å lære Haskell er: Pattern matching, currying, automatisk parallellisering, Template Haskell (kode som skriver kode), god performance, haugevis av biblioteker i hackage, STM, og masse annet.

Lær deg Haskell!

Brodwall finner opp hjulet [Luke 20, 2012]

Thursday, December 20th, 2012
10 kommentarer

Johannes Brodwall (@jhannes) har lenge vært et kjent fjes i det smidige utviklermiljøet i Norge. Han er ildsjelen bak Oslo XP Meetup, har vært med å arrangere flere av smidig-konferansene, og holder kurs om smidig utvikling, TDD og arkitektur. Johannes uttaler seg stadig vekk i Computerworld, og har sin egen blogg han kaller Thinking Inside a Bigger Box. Du finner også endel innlegg fra ham på Sterk Blanding.

I dagens bloggpost snakker Johannes om no så rart som viktigheten av å finne opp hjulet på nytt, med et konkret eksempel i C#.

Johannes portrettbilde

Hvem er du?
Flakkende programmerer som var overbevist om at han aldri kunne lære å programmere før han startet på universitetet.

Hva er jobben din?
Chief scientist og code coach i Exilesoft i Norge, Sverige og Sri Lanka.

Hva kan du?
Få nysgjerrige programmerere til å lære nye teknikker for å programmere og for å forstå hva de skal programmere.

Hva liker du best med yrket ditt?
At jeg kan lære meg litt om mange forskjellige problemstillinger som gjennomsyrer samfunnet og at jeg må forene den kompromissløse og kalde verdenen til maskinen med den ubesluttsomme, nyanserte verdenen til mennesker.


Når jeg lærte matte i barneskolen grep jeg etter kalkulatoren. Men min far sa til meg: “Du får ikke bruke kalkulator før du kan klare deg uten”. Jeg syntes naturligvis dette var både urettferdig og upraktisk, men etter en stund fant jeg ut hvilken fordel det har å forstå grunnferdighetene før man griper etter et verktøy.

For mange utviklere dreier det neste kompetansevalget seg om hvilket fancy rammeverk skal man skal lære seg. Eller hvilket programmeringsspråk som ville løse alle våre problemer (med et glimt i øye til fjorårets julekalender her i programmeringsbloggen). Før vi griper etter verktøyene kan det være nyttig å lære seg hvordan de egentlig fungerer. Mitt motto er “jeg vil ikke bruke et rammeverk jeg ikke kunne laget selv”. Det ville naturligvis vært problematisk å lage et like komplett rammeverk som mange av de som er tilgjengelig, men jeg burde i det minste klare å løse alle problemene under normale omstendigheter selv.

Fordelen med å ha gjort det samme som et rammeverk gjør er at da skjønner jeg hvordan denne koden må være implementert. Jeg får en større intuitiv forståelse for hvordan jeg skal bruke rammeverket, jeg skjønner raskerer hva som er problemet når ting ikke fungere og ikke minst: Jeg skjønner når rammeverket gir meg mer problemer enn hjelp.

Et eksempel jeg har likt å bruke mye er å lage en webapplikasjon i Java uten webrammeverk. Jeg bruker gjerne et enkelt eksempel med en adressebok der man kan registrere kontakter og søke etter disse kontaktene. Til ære for C#-utviklere som leser denne bloggen har jeg løst den samme oppgaven i C#: Hvordan ville du laget en webapplikasjon som verken brukte MVC, ASP.NET eller en gang IIS?

Personlig er jeg veldig avhengig av TDD for å tenke. (Jeg har gjort et unntak fra den overnevnte kalkulatorregelen for tester og bruker SimpleBrowser.WebDriver, FluentAssertions og NUnit). For å starte, har jeg skrevet en test som demonstrerer hva webapplikasjonen skal gjøre:

[Test]
public void ShouldFindSavedPerson()
{
    // Start a web server INSIDE THE TEST :-D
    var server = new My.Application.WebServer();
    server.Start();

    var browser = new SimpleBrowser.WebDriver.SimpleBrowserDriver();
    browser.Url = server.BaseUrl;

    // Navigate to the "add contact" page
    browser.FindElement(By.LinkText("Add contact")).Click();

    // Add a new contact
    browser.FindElement(By.Name("fullName")).SendKeys("Darth Vader");
    browser.FindElement(By.Name("address")).SendKeys("Death Star");
    browser.FindElement(By.Name("saveContact")).Submit();

    // Navigate to the "find contact" page
    browser.FindElement(By.LinkText("Find contact")).Click();

    // Execute some queries:
    browser.FindElement(By.Name("nameQuery")).SendKeys("vader");
    browser.FindElement(By.Name("nameQuery")).Submit();
    browser.FindElement(By.CssSelector("#contacts li")).Text
           .Should().Be("Darth Vader (Death Star)");
    browser.FindElement(By.Name("nameQuery")).SendKeys("anakin");
    browser.FindElement(By.Name("nameQuery")).Submit();
    browser.FindElements(By.CssSelector("#contacts li"))
           .Should().BeEmpty();
}

Når jeg kjører denne testen første gang, vil den feile allerede på linjen browser.Url = server.BaseUrl, fordi det er ikke noen faktisk server.

For å implementere WebServer har jeg brukt en liten artig klasse som kommer med .NET: System.Net.HttpListener. Her er den essensielle koden:

class WebServer
{
    public void Start()
    {
        var listener = new System.Net.HttpListener();
        listener.Prefixes.Add(BaseUrl);
        listener.Start();
        new Thread(HttpThread).Start(listener);
    }

    private void HttpThread(object listenerObj)
    {
        HttpListener listener = (HttpListener)listenerObj;
        while (true)
        {
            var context = listener.GetContext();
            using (context.Response)
            {
            }
        }
    }
}

Jeg kjører testen igjen og kommer et skritt videre. Denne gangen får jeg beskjed om at testen ikke finner linken til “Add contact”. Ikke så rart, vi produserer ikke noe HTML! En liten endring i koden over:

var context = listener.GetContext();
using (context.Response)
{
    new AddressBookController().Service(context);
}

Og så må vi bare implementere AddressBookController.Service:

class AddressBookController
{
    internal void Service(HttpListenerContext context)
    {
        var html = "<html>" +
            "<p><a href='/contact/create'>Add contact</a></p>" +
            "<p><a href='/contact/'>Find contact</a></p>" +
            "</html>";
        var buffer = Encoding.UTF8.GetBytes(html);
        context.Response.OutputStream.Write(buffer, 0, buffer.Length);
    }
}

Testen kommer et skritt videre. Vi ser at vi får opp hovedsiden med valgene “Add Contact” og “Find contact”. Når vi klikker på “Add contact” finner vi naturligvis ikke feltet “fullName” fordi vi ikke har laget skjemaet enda. Metoden “HandleGetRequest” sjekker URL’en for å bestemme hvilken side som skal vises:

internal void Service(HttpListenerContext context)
{
    var html = HandleGetRequest(context.Request);
    var buffer = Encoding.UTF8.GetBytes(html);
    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
}

private string HandleGetRequest(HttpListenerRequest request)
{
    if (request.Url.LocalPath == "/contact/create")
    {
        return "<html>" +
            "<form method='post' action='/contact/create'>" +
            "<p><input type='text' name='fullName'/></p>" +
            "<p><input type='text' name='address'/></p>" +
            "<p><input type='submit' name='saveContact' value='Save'/></p>" +
            "</form>" +
            "</html>";
    }
    else
    {
        // som før
    }
}

Vi er nesten ferdig med å registrere kontakter. Nå finner vi ikke linken “Find contact” etter at vi har postet formen. Metoden “Service” må håndtere POST requester med redirect:

internal void Service(HttpListenerContext context)
{
    if (context.Request.HttpMethod == "GET")
    {
        var html = HandleGetRequest(context.Request);
        var buffer = Encoding.UTF8.GetBytes(html);
        context.Response.OutputStream.Write(buffer, 0, buffer.Length);
    }
    else
    {
        context.Response.Redirect(context.Request.Url.GetLeftPart(UriPartial.Authority));
    }
}

Nå mangler skjema for å søke etter personer. Her hjelper copy-paste pattern oss:

private string HandleGetRequest(HttpListenerRequest request)
{
    if (request.Url.LocalPath == "/contact/create") ...
    else if (request.Url.LocalPath == "/contact/")
    {
        return "<html>" +
            "<form method='get' action='/contact/'>" +
            "<p><input type='text' name='nameQuery'/></p>" +
            "<p><input type='submit' value='Find'/></p>" +
            "</form>" +
            "</html>";
    }
    else ...
}

Feilen nå er åpenbar: Vi har ikke med svaret med kontakter:

    class Contact
    {
        public string FullName { get; set; }
        public string Address { get; set; }
    }

    private static List<Contact> contacts = new List<Contact>();

    private string HandleGetRequest(HttpListenerRequest request)
    {
        else if (request.Url.LocalPath == "/contact/")
        {
            var contactsHtml = string.Join("",
                contacts.Select(c => "<li>" + c.FullName + " (" + c.Address + ")</li>")))
            return string.Format("<html>" + ...
                "<ul id='contacts'>{0}</ul>" +
                "</html>", contactsHtml);

        }

Så gjenstår det bare å ta vare på kontaktene når vi gjør en POST fra “Add contact” skjemaet:

internal void Service(HttpListenerContext context)
{
    if (context.Request.HttpMethod == "GET") ...
    else
    {
        // Read the parameters from the POST body (Request.InputStream)
        var request = context.Request;
        var encoding = context.Request.ContentEncoding;
        var reader = new StreamReader(context.Request.InputStream, encoding);
        var parameters = HttpUtility.ParseQueryString(reader.ReadToEnd(), encoding);

        context.Response.Redirect(HandlePostRequest(request, parameters));
    }
}

private string HandlePostRequest(HttpListenerRequest request, NameValueCollection parameters)
{
    contacts.Add(new Contact() { FullName = parameters["fullName"], Address = parameters["address"] });
    return request.Url.GetLeftPart(UriPartial.Authority);
}

En siste test feiler: Vi filterer ikke kontaktene basert på søket.

private string HandleGetRequest(HttpListenerRequest request)
{
    if (request.Url.LocalPath == "/contact/create") ...
    else if (request.Url.LocalPath == "/contact/")
    {
        var query = request.QueryString["nameQuery"];
        var contactsHtml = string.Join("",
            contacts
                .Where(c => query == null || c.FullName.ToLower().Contains(query.ToLower()))
                .Select(c => "<li>" + c.FullName + " (" + c.Address + ")</li>"));
        return string.Format("<html>" +
                ...
                "<ul id='contacts'>{0}</ul>" +
                "</html>", contactsHtml);
    }
    else ...
}

Nå gjenstår det bare å lagre personene til en ordentlig database og å rette den åpenbare sikkerhetssårbarheten ved vising av kontaktene. AddressBookWebServer bør også få en Main metode, slik at du faktisk kan kjøre koden. Men det overlater jeg til deg, kjære leser.

Og nå kommer vi til moralen

I denne artikkelen har du lært hvordan HTTP egentlig fungerer og hvordan rammeverk som ASP.NET MVC fungerer bak kulissene. Det er mange detaljer vi er glade for å slippe å håndtere, slik som konvertering mellom tegnsett og lesing av innholdet i en POST request. Og det er mange ting som slett ikke er så vanskelig, som for eksempel å gjøre ordentlig “redirect-on-post”. Jeg har på mer enn ett prosjekt innsett at etter at jeg hadde gjort et par dagers investering for å forstå den underleggende teknologien klarte jeg å levere prosjektet bedre uten de åpenbare, populære rammeverkene som alle anbefaler at man skal bruke.

Har jeg funnet opp hjulet på nytt her? Det er mulig å argumentere for det, men jeg ønsker å strekke “finne opp hjulet på nytt” metaforen så langt som den lar seg strekke:

Min erfaring er at det er mange “biler” i dag som har skjeive hjul der akslingen ikke er i midten. Det kan være at hjulet er dårlig eller det kan være at hjulet bare er montert feil. Så merker vi kanskje at bilen humper fordi vi har to hjul som begge har akslingen montert feil. Og så bruker vi masse innsats på å få justert disse to hjulene slik at de humper i takt.

Dersom vi har erfaring med i det minste å lage et “hjul” eller to, kan det hende vi klarer å identifisere de egentlige problemene med de “hjulene” som vi blir gitt, slik at vi kan finne ut hvilke “hjul” som er bra og hvilke som er dårlige, og ikke minst: Hvordan å bruke dem riktig.

Finn opp på nytt de hjulene du ikke forstår, ikke bruk et rammeverk du ikke kunne laget selv og ikke bruk en kalkulator før du forstår matematikken.

God (h)jul!

Kjersti om den geniale utvikleren [Luke 19, 2012]

Wednesday, December 19th, 2012
14 kommentarer

Kjersti Berg (@KjerstiBB) er en dyktig og engasjert utvikler som ikke er redd for å prøve nye ting. For et år siden forlot hun Java til fordel for C#, og for et halvt år siden byttet hun ut konsulentselskapet Miles med PSWinCom, og ble min kollega.

Kjersti står også bak Bergen CodingDojo, og er med og arrangerer BOOSTER-konferansen. I dag følger hun opp en kommentar hun kom med her på bloggen for litt siden…

fbda2c379515156ea6800018d708a8a5

Hvem er du?
En programmerer med sans for folk.

Hva er jobben din?
Utvikler i PSWinCom i Bergen.

Hva kan du?
Kan slette kode. Ingen kode – ingen bugs.

Hva liker du best med yrket ditt?
At jeg, på en god dag, kan lage noe som kan bidra tilk å gjøre livet litt enklere for noen andre.


Jakten på enhjørningen

tl;dr; Myten om den hyperproduktive utvikleren, er den egentlig sann? Og, bør vi bruke tid på å diskutere den?

Den dukker opp med mer eller mindre jevne mellomrom. Påstanden om at forskjellen i produktivitet blant utviklere er stor. Veldig stor. Større til og med enn blant andre yrkesgrupper. Sist i en artikkkel i Computerworld, der vi kan lese at Lyle M. Spencer rapporterer at de beste systemutviklere produserte 13 ganger så mye testet funksjonalitet per månedsverk som de gjennomsnittlige, og 32 ganger mer enn de dårligste. . Et kjapt søk på “programmer productivity” på Google gir over 12 millioner treff, og mange av dem gjentar denne påstanden i en eller annen form, med litt ulike kilder.

Jeg er i utgangspunktet skeptisk. Jeg får det nemlig ikke til å gå opp med min egen erfaring. Ikke at man skal trekke for bastante konklusjoner bare basert på egne erfaringer, a.k.a. anekdoter, men siden forskjellene er rapportert å være så store, skulle jeg ikke ha sett i det minste noen tegn til dette etter 10 år som utvikler? Flinkere folk enn meg har studert kildene, og utfordret påstandene og kritisert kildebruken. Klart det er forskjell på folk, men er disse forskjellene virkelig den viktigste faktoren for om et prosjekt skal lykkes eller ikke? Bør vi derfor ta konsekvensen av dette og lønne “dyktige” utviklere mer enn de “uproduktive”?

Etter å ha lest litt i noen av kildene som brukes for å underbygge påstanden er jeg ikke blitt mer overbevist. Ingen av de kildene jeg har lest klarer engang å forklare hva produktivitet i vår bransje egentlig er for noe, langt mindre hvordan vi meningsfylt skal kunne måle det. Ingen har klart å finne en objektiv metode for å skille de hyperproduktive fra de mindre produktive utviklerne. Hvorfor ikke? Kanskje fordi hele greien er en myte mer enn faktisk viten? Finnes det ikke egentlig noe som kan kalles utviklerproduktivitet i det hele tatt? Kanskje er produksjon helt feil analogi på det vi gjør?

Men, vil du kanskje si, jeg har nå vitterlig møtt endel idioter i jobben opp gjennom årene. Og noen av dem var ikke bare mindre produktive, de bidro faktisk negativt, fordi vi måtte bruke masse tid på å rydde opp i alt rotet de klarte å produsere. Neste gang du instinktivt tenker det; prøv å se om det finnes andre forklaringer enn bare den enkle; idioten er ikke en så flink og produktiv utvikler som meg. Kan det være at han er blitt tildelt en oppgave han ikke hadde forutsetninger for å løse alene, og fordi det ikke er en kultur for å spørre om hjelp i teamet? Kan det hende han er demotivert, fordi han har sittet med den samme typen oppgaver i flere år, og aldri fått mulighet til å utvikle seg? Har han kranglet med sjefen? Har han kranglet med kona?

I en studie om kollektiv intelligens, ble 192 team ble satt sammen på ulikt vis og gitt et sett oppgaver de skulle løse for å finne ut hvilke faktorer som påvirket gruppens score. Team med gruppemedlemmer med høy IQ gjorde det ikke bedre enn team der medlemmene hadde lavere IQ. Faktisk var heller ikke faktorer som motivasjon, samhold eller tilfredshet i gruppen avgjørende. Det som sto igjen som den viktigste faktoren var andel kvinner i gruppen! Som kvinne er det selvfølgelig deilig å kunne slå en mannlig kollega i hodet med slike resultater, men, som forskerne selv sier; poenget er nok ikke først og fremst å ha med mange kvinner, men å ha med folk som er flinke til å lytte til hverandre, som er sensitive til andres behov, som er åpne til andres idéer, egenskaper som de altså mener er mer fremtredende hos kvinner enn hos menn.

Hvis jeg har vært flink (sic!) nå, så er du kanskje overbevist om at utviklerproduktivitet ikke er et så veldig interessant fenomen å snakke om. Men, er det så farlig å gjøre det? Det er jo ikke på noen måte bevist at det ikke er så store forskjeller blant utvikleres evne til å løse problemer, selv om vi ikke klarer å måle det ordentlig ennå. Det føles jo ofte intuitivt riktig, selv om ikke forskningen gir ryggdekning for det. I tillegg er det jo deilig å føle seg som en del av en produktiv elite, ikke sant?

Jeg tror det gjør noe. Hva tenker du for eksempel om kollegaene dine? De som aldri drar på konferanser, og som ikke lærer seg et nytt programmeringsspråk hvert år. Eller han nye kisen som kommer rett fra skolen, og ennå ikke har lært seg hva som er «best» av TDD og BDD, eller ikke har sterke meninger om distribuert versjonskontroll og DI-rammeverk. Er du i stand til likevel å lære noe av ham? Lytter du ordentlig til det han sier, eller avviser du ham som en dinosaur eller noob, alt ettersom? Å lage software er helt grunnleggende en sosial aktivitet, men, når vi er for opptatt av å dyrke de beste, å forsøke å bli en av de beste, de hyperproduktive, geniale utviklerne, så er det lett å glemme det.

Med andre ord; jeg tror påstanden, eller egentlig, myten om den geniale utvikleren, ødelegger for vår evne til å vise respekt, høre på andre,og være ydmyk om egne evner. Jeg tror den gjør oss til større rævhol enn vi trenger å være. Og hvem vil vel egentlig være det?

Hvis du skal ansette noen utviklere, så for all del; gjør ditt beste i å finne noen som du tror er flinke og engsjerte, men enda viktigere; finn noen som du tror vil passe inn i organisasjonen din. Og, viktigst av alt, tør å analysere hvordan organisasjonen din hjelper eller hindrer de utviklerne som er der i å gjøre en så god jobb som de kan. Det er mye mer å hente der.

The talent myth assumes that people make organizations smart. More often than not, it’s the other way around. (Malcolm Gladwell, The talent myth, fra the New Yorker)

Cruickshanks presenterer Arquillian [Luke 18, 2012]

Tuesday, December 18th, 2012
1 kommentar

Kevin Cruickshanks (@kevincruick) er en kompis fra studietiden ved Universitetet i Bergen. Det er ikke så ofte vi ser hverandre lengre, men fra tid til annen stiller han opp på NNUG-møter, og da er det alltid hyggelig.

Arquillian er en spennende plattform for automatiserte integrasjonstester for Java-utviklere. I denne tutorialen git Kevin deg en grundig innføring.

IMG_3472

Hvem er du?
Utflytta stording som no er partner og systemutvikler i Machina AS i Bergen

Hva er jobben din?
Systemutvikler på Java og .NET plattform

Hva kan du?
Har mykje erfaring med utvikling av Java server og klienter

Hva liker du best med yrket ditt?
Det er utfordringer kvar einaste dag!


Integrasjonstesting med Arquillian

Noe jeg alltid har ønsket meg er et rammeverk for integrasjonstesting av Enterprise Java system (J2EE) i en kontainer. Arquillian er et slikt verktøy som gir deg mulighet for å teste applikasjonene dine rett i en kontainer, som for eksempel Jboss, uten bruk av mockups eller andre omveier for å få koden din testet.

Enhetstester bør du også lage, men det trenger du ikke Arquillian til. Dersom du vil se hvordan applikasjonen din oppfører seg i forskjellige kontainere er Arquillian nettopp noe for deg. Denne artikkelen viser deg hvordan du kan bygge en liten webapplikasjon som kan testes med Arquillian.

Stikkord

Her et utvalg av teknologiene som er i bruk i artikkelen:

  • Arquillian – Dette er selve testrammeverket som utfører testene dine.
  • Shrinkwrap – Blir benyttet til å opprette ferdig applikasjonspakker (EAR, WAR, JAR) som til slutt kjøres i applikasjonsserveren
  • JBoss – I eksempelapplikasjonen som er laget til denne artikkelen har jeg fokusert på Jboss versjon 7 som applikasjonsserver. Arquillian støtter en mengde andre kontainere, som Tomcat, Glassfish, Jetty, Weblogic og WebSphere i tillegg til flere eldre versjoner av Jboss. For full oversikt over hvilke kontainere som støttes kan du se her.
  • Netbeans – Jeg benytter meg av Netbeans i mitt daglige virke og eksempelapplikasjonen er kodet ved hjelp av den. Prosjektet er satt opp ved hjelp av Maven og kan åpnes og kjøres i andre programmer som IntelliJ og Eclipse.
  • Java Server Faces (JSF) v2.0 er benyttet som webrammeverk.
  • Java – Applikasjonen er bygd på Java versjon 7 og Java EE versjon 6

Oppsett av Arquillian

For å kunne kjøre Arquillian må vi første importere det. Min webapplikasjon er bygd opp rundt Maven og dermed kan jeg legge til alle eksterne bibliotekreferenser rett i POM-filen min og la Maven laste ned alle nødvendige filer. Arquillian krever en god del tilleggsbiblioteker og kan ta litt tid å laste ned første gang.

Under kan du se et utdrag av POM-filen som omhandler oppsettet for Arquillian. I dependencyManagement angir du hvilken versjon av Arquillian du vil benytte. Alle avhengigheter (dependencies) som ikke har angitt versjon benytter versjonen i dependencyManagement.

JSFUnit biblioteket er et tilleggsbibliotek som kan integreres med Arquillian. JSFUnit er et testrammeverk for JSF webapplikasjoner. Jeg benytter dette rammeverket til testing av websidene i eksempelapplikasjonen. Eksempel på bruk av JSFUnit er omtalt lenger ned i artikkelen.

Profiler
De to profilene som er angitt nederst i utsnittet under, viser hvilke kontainere som kan benyttes til testene. Profilen jbossas-7-remote blir brukt til testing mot eksterne instanser, mens jbossas-7-managed brukes til lokale instanser, dvs, at Arquillian starter og stopper instansen for deg for hver testklasse. Det er desverre ikke mulig å stare en instans av jboss i managed-modus og la den fungere på tvers av flere testklasser. Men dette vil nok bli støttet i senere versjoner av Arquillian.

...
    <properties>
        ...
        <jboss-version>7.1.1.Final</jboss-version>
        ...
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.0.1.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.descriptors</groupId>
            <artifactId>shrinkwrap-descriptors-impl-javaee</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-persistence-impl</artifactId>
            <version>1.0.0.Alpha5</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.jsfunit</groupId>
            <artifactId>jsfunit-arquillian</artifactId>
            <version>2.0.0.Beta3-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec</groupId>
            <artifactId>jboss-javaee-6.0</artifactId>
            <version>3.0.1.Final</version>
            <scope>provided</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
    <profiles>
        <profile>
            <id>jbossas-7-remote</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-remote</artifactId>
                    <version>${jboss-version}</version>
                    <scope>provided</scope>
                </dependency>
            </dependencies>
        </profile>
        <profile>
            <id>jbossas-7-managed</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-managed</artifactId>
                    <version>${jboss-version}</version>
                    <scope>provided</scope>
                </dependency>
            </dependencies>
        </profile>
    </profiles>
...

En webapplikasjon

Før vi går i gang med testing må vi få laget til en testbar webapplikasjon. Applikasjonen er bygd på en standard tre-lagsarkitektur med klient-, server- og databaselag. Til database kan du benytte en hvilken som helst SQLkompatibel database. Jeg har til testingen benyttet meg av den innebygde H2-databasen som følger med Jboss7 installasjonen.

JPA er brukt som rammeverk for kommunikasjon med databasen. For å kunne benytte oss av JPA trenger vi en Persistence Unit. Selve filen kan du legge i katalogen src/main/resources/META-INF/. Som tilbyder benytter jeg Hibernate som følger med Jboss. Du trenger ikke inkludere Hibernate i POM-filen din.

Du kan konfigurere og bruke forskjellige i testene dine, men jeg har kun laget til en som er konfigurert til å koble seg opp mot H2-databasen i Jboss.

<?xml version="1.0" encoding="UTF-8"?>
<persistence
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
    <persistence-unit name="budgettest">
        <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>no.ksoft.budget.ds.BudgetPost</class>
        <class>no.ksoft.budget.ds.Budget</class>
        <class>no.ksoft.budget.ds.BudgetPostTag</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

Entitetsbønner (entity bean)

Jeg har laget til tre entiter (disse legges til i persistence.xml) til denne applikasjonen som er et forsøk på å lage et lite budsjettprogram:

  • Budget
  • BudgetPost
  • BudgetPostTag

Under ser du hvordan entiteten Budget ser ut. Alle entitetene arver klassen Any som er en MappedSuperClass. Alle entiteter som arver Any vil få tilordnet alle egenskapene til Any, uten at den er å betrakte som en egen entitet i systemet.

Klassen Any:

@MappedSuperclass
@EqualsAndHashCode
public class Any {
    @SuppressWarnings("unused")
    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue
    @Getter
    @Setter
    protected int id;
}

Entiteten Budget:

@Entity
@Table(name = "budget", schema = "public")
@NamedQueries(value = {@NamedQuery(name = "Budget.findAll", query = "SELECT object(b) FROM Budget as b")})
public class Budget extends Any implements java.io.Serializable {

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "start_date", nullable = false, length = 29)
    private Date startDate;
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "end_date", nullable = false, length = 29)
    private Date endDate;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "budget")
    private Set budgetPosts = new HashSet(0);

    public Budget() {
    }

    public Budget(int id, Date startDate, Date endDate) {
        this.id = id;
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public Budget(int id, Date startDate, Date endDate, Set budgetPosts) {
        this.id = id;
        this.startDate = startDate;
        this.endDate = endDate;
        this.budgetPosts = budgetPosts;
    }
    ...
}

For hver entitet er det opprettet en egen sesjonsbønne (session bean):

  • BudgetServiceImpl
  • BudgetPostServiceImpl
  • BudgetPostTagServiceImpl

Hver bønne implementerer følgende standardmetoder for uthenting og lagring av data:

@Remote
public interface Service<T> {
    T findById(int id);
    T makePersistent(T entity);
    List<T> findAll();
}

En ferdigimplementert bønne ser slik ut:

@Stateless
public class BudgetServiceImpl implements BudgetService {
    @PersistenceContext
    private EntityManager em;

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public Budget findById(int id) {
        return em.find(Budget.class, id);
    }

    @Override
    public List findAll() {
        List l = em.createNamedQuery("Budget.findAll").getResultList();
        if (l == null) {
            return new ArrayList<>();
        }
        return (List) l;
    }

    @Override
    public Budget makePersistent(Budget entity) {
        em.persist(entity);
        return entity;
    }
}

Klientlaget

To enkle websider er lagt til. Sidene er kodet i xhtml og begge benytter seg av hver sin Managed bean for uthenting av relevant informasjon fra databasen. Index.xhtml viser et tall for antall budsjetter i databasen og inneholder en knapp som lenker til et vilkårlig budsjett. Den andre websiden, view-budget.xhtml, viser informasjon om et Budsjett som blir hentet ut ved hjelp av en ID paramater.

index.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
    <head><title>Budgets</title></head>
    <body>
        <f:view>
            <h:form id="viewBudgetForm">
                <h1><h:outputText value="Budgets"/></h1>
                <h:outputText id="totaltAmountOfBudgets" value="#{indexBean.allBudgets.size()}"/>
                <h:commandLink id="viewAbudget" value="View budget" action="view-budget.xhtml">
                    <f:param name="budgetId" value="#{indexBean.allBudgets.get(0).id}" />
                </h:commandLink>
            </h:form>
        </f:view>
    </body>
</html>
                                                        

view-budget.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
    <head>
        <title>Budgets</title>
    </head>
    <body>
        <f:view>
            <h:form id="viewSingleBudgetForm">
                <h:outputText value="Current budget ID: "/>
                <h:outputText id="currentBudgetId" value="#{viewBudgetBean.currentBudget.id}"/><br />
                <h:outputText value="Current budget start date"/>
                <h:outputText id="currentBudgetStartDate" value="#{viewBudgetBean.currentBudget.startDate}"/><br />
                <h:outputText value="Current budget end date"/>
                <h:outputText id="currentBudgetEndDate" value="#{viewBudgetBean.currentBudget.startDate}"/><br />
            </h:form>
        </f:view>
    </body>
</html>
                                                        

Det bakomforliggende bønnene (managed beans) benytter CDI for å legge inn referanser til sesjonsbønnene. Bønnen IndexBean benyttes i index.html, mens bønnen ViewBudgetBean benyttes i view-budget.xhtml. Ingen fantasifull navngivning av bønner dette, men navnet mer enn antyder hvor den aktuelle bønnen er i bruk. Du kan selvsagt bruke bønner på tvers av flere sider, men du kan fort ende opp i situasjoner som er vanskelig å håndtere etterhvert som applikasjonen vokser.

Bønnen IndexBean

@Named("indexBean")
@RequestScoped
public class IndexBean {

    @Inject
    BudgetService budgetService;

    public List getAllBudgets() {
        return budgetService.findAll();
    }
}

Bønnen ViewBudgetBean

@Named("viewBudgetBean")
@RequestScoped
public class ViewBudgetBean {

    @Inject
    BudgetService budgetService;
    @Getter
    @Setter
    @Inject
    @RequestParam("budgetId")
    Instance<Integer> budgetId;

    public Budget getCurrentBudget() {
        return budgetService.findById(getBudgetId().get().intValue());
    }
}

Testing av serverlaget med Arquillian

Til testing av serverlaget trenger vi først litt mer konfigurering av Arquillian. Konfigurasjonsfilen Arquillian.xml legges i <sti til prosjektet ditt>src/test/resources. Min fil ser slik ut:

<arquillian xmlns="http://jboss.org/schema/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
    <engine>
        <property name="deploymentExportPath">target/</property>
    </engine>
    <defaultProtocol type="Servlet 3.0" />
    <container qualifier="jbossas-7-managed" default="false">
        <configuration>
            <property name="jbossHome">X:\sti\til\jboss\jboss-as-7.1.1.Final</property>
        </configuration>
        <protocol type="jmx-as7">
            <property name="javaVmArguments">-Xmx512m -XX:MaxPermSize=128m -Xverify:none -XX:+UseFastAccessorMethods</property>
            <property name="executionType">REMOTE</property>
        </protocol>
    </container>
    <container qualifier="jbossas-7-remote" default="true">
        <configuration>
            <property name="managementAddress">192.168.1.195</property>
            <property name="managementPort">8080</property>
        </configuration>
        <protocol type="jmx-as7">
        </protocol>
    </container>
</arquillian>

Filen inneholder konfigurasjonselementer for begge profilene jeg oppgav i POM-filen (jbossas-7-managed og jbossas-7-remote). For remote-profilen oppgir jeg ip-adressen og portnummeret til den kjørende jbossinstansen, mens for managed, oppgir jeg den fulle stien til jbossinstallasjonen. Feilinformasjonen fra Arquillian er ikke alltid like intuitiv, men den gir nøyakig informasjon dersom deler av konfigurasjonen din er feil.

Du kan lage flere spektakulære tester med Arquillian, men jeg viser kun det grunnleggende her. Testen under viser det som trengs for å kjøre en enkelt testklasse med Arquillian.

Du trenger følgende tre ting for å lage en testklasse:

      1: En @RunWith(Arquillian.class) annotasjon for å angi at du vil la Arquillian behandle testklassen.
      2: En metode som er annotert med @Deployment som returnerer et ShrinkWrap arkiv.
      3: Minst en metode annotert med @Test som angir at det er en testmetode.

I testen under har jeg også lagt til klasseannotasjonen @Transactional med TransactionMode.ROLLBACK som parameter. Denne angir at databasen min blir rullet tilbake etter hver testmetode slik at jeg kan være sikker på at databasen er tom for hver test som kjøres. Denne annoteringen kan og ligge på metodenivå. Andre paramtetere som er tilgengelig for denne annotasjonen er COMMIT og DISABLE.

@UsingDataSet(“navn på dataset.xml fil”) annotasjonen gir deg mulighet for å populere databasen din med verdier før du kjører testene. Bruken av denne annotasjonen er smør på flesk i midt eksempel da den i praksis gir samme resultat som TransactionMode.ROLLBACK siden jeg benytter meg av et tomt dataset før hver test kjøres. Du kan lese mer om bruk av dataset i testene her.

@Deployment-metoden lager et ShrinkWrap arkiv som blir innstallert i kontainerinstansen under kjøring. I eksempelet er det en WAR-fil som blir opprettet. Alle klasser som er nødvendige for å kunne kjøre testen må legges inn i arkivet. Du trenger ikke ta med alle klassene i prosjektet ditt dersom de ikke er knyttet opp mot noen av klassene du trenger for å gjennomføre testene testene.

Det er også mulig å knytte opp hele prosjekt (F.eks.: et Java bibliotekprosjekt) inn i arkivet. Det vil med andre ord kunne være lurt å lage et eget Arquillian testprosjekt som kobler inn det nødvendige web-, ejb- og javabibliotekene du trenger og deretter utfører testene. Eneste forskjellen vil være et mer håndterbart arkiv som du kan benytte i testene dine.

@RunWith(Arquillian.class)
@UsingDataSet("empty.xml")
@Transactional(TransactionMode.ROLLBACK)
public class BudgetServiceTest {

    private static final Logger LOGGER = Logger.getLogger(BudgetServiceTest.class.getName());
    @Inject
    private BudgetService budgetService;

    public BudgetServiceTest() {
    }

    @Deployment
    public static Archive<?< createTestableDeployment() {
        final WebArchive war = ShrinkWrap.create(WebArchive.class, "service.war");
        war.addClass(Any.class);
        war.addClass(Budget.class);
        war.addClass(BudgetPost.class);
        war.addClass(BudgetPostTag.class);
        war.addClass(Service.class);
        war.addClass(BudgetService.class);
        war.addClass(BudgetServiceImpl.class);
        war.addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml");
        war.addAsWebInfResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml"));
        LOGGER.info(war.toString(Formatters.VERBOSE));
        return war;
    }

    @Test
    public void testFindAllBudgets() {
        List<Budget< all = budgetService.findAll();
        assertEquals(true, all.isEmpty());
        insertABudget();
        all = budgetService.findAll();
        assertEquals(1, all.size());
    }

    @Test
    public void testFindBudgetById() {
        final Budget expected = insertABudget();
        assertTrue(expected.getId() < 0);

        List<Budget< all = budgetService.findAll();
        assertEquals(1, all.size());

        Budget result = budgetService.findById(expected.getId());
        assertEquals(expected, result);
    }

    @Test
    public void testMakePersistent() {
        Budget expected = insertABudget();
        Budget test = budgetService.findById(expected.getId());
        assertEquals(expected, test);
    }

    private Budget insertABudget() {
        final Budget b = new Budget();
        b.setEndDate(new Date(System.currentTimeMillis()));
        b.setStartDate(new Date(System.currentTimeMillis()));
        return budgetService.makePersistent(b);
    }
}

JSFUnit

Til slutt vil jeg vise hvordan du kan få testet ut alle dei tre lagene i applikasjonen. Under vises et eksempel på en testklasse for å teste ut manipulering og navigering mellom to websider. Denne testing er mulig ved hjelp av JSFUnit som gir deg tilgang til en del nyttige metoder for å manipulere webapplikasjonen under kjøring i en kontainer.

I metoden testIndexBean(JSFServerSession server, JSFClientSession client) blir server og client injisert inn i metoden. JSFServerSession gir deg tilgang til deler av JSF API`et, mens JSFClientSession er en wrapper-klasse for HtmlUnit som imiterer nettleser interaksjon med en JSF-applikasjon.

@RunWith(Arquillian.class)
@InitialPage("/index.xhtml")
public class WebTest {

    private static final Logger LOGGER = Logger.getLogger(WebTest.class.getName());
    @Inject
    private BudgetService budgetService;

    @Deployment(testable = true)
    public static Archive>?< createTestableDeployment() {
        MavenDependencyResolver resolver = DependencyResolvers.use(
                MavenDependencyResolver.class).loadMetadataFromPom("pom.xml");

        final WebArchive war = ShrinkWrap.create(WebArchive.class, "budget.war");
        war.addClass(Any.class);
        war.addClass(Budget.class);
        war.addClass(BudgetPostTag.class);
        war.addClass(BudgetPost.class);
        war.addClass(Service.class);
        war.addClass(BudgetService.class);
        war.addClass(BudgetPostService.class);
        war.addClass(BudgetPostTagService.class);
        war.addClass(BudgetServiceImpl.class);
        war.addClass(BudgetPostServiceImpl.class);
        war.addClass(BudgetPostTagServiceImpl.class);
        war.addClass(IndexBean.class);
        war.addClass(ViewBudgetBean.class);
        war.addClass(FieldParameterNames.class);
        war.addAsWebResource(new File("src/main/webapp", "index.xhtml"));
        war.addAsWebResource(new File("src/main/webapp", "view-budget.xhtml"));
        war.setWebXML(new File("src/main/webapp/WEB-INF/web.xml"));
        war.addAsWebInfResource(new File("src/main/webapp/WEB-INF/faces-config.xml"), "faces-config.xml");
        war.addAsWebInfResource(new File("src/main/webapp/WEB-INF/jboss-web.xml"), "jboss-web.xml");
        war.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
        war.addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml");
        war.addAsLibraries(resolver.artifact("org.jboss.solder:solder-impl").resolveAsFiles());

        LOGGER.info(war.toString(Formatters.VERBOSE));
        return war;
    }

    @Before
    public void addSomeValues() {
        Budget b = new Budget();
        b.setEndDate(new Date(System.currentTimeMillis()));
        b.setStartDate(new Date(System.currentTimeMillis()));
        budgetService.makePersistent(b);
    }

    @Test
    @InitialPage("/index.xhtml")
    public void testIndexBean(JSFServerSession server, JSFClientSession client) throws IOException {
        //Sjekk at vi er på indexsiden
        assertEquals("/index.xhtml", server.getCurrentViewID()); 

        //Sjekk at IndexBean får hentet ut det ene budsjettet vi har opprettet
        assertEquals(1, server.getManagedBeanValue("#{indexBean.allBudgets.size()}")); 

        UIComponent totalAmountOfBudgetsOutput = server.findComponent("totaltAmountOfBudgets");

        //Sjekk at antall budsjett vises korrekt på indexsiden
        assertEquals("1", totalAmountOfBudgetsOutput.getAttributes().get("value").toString()); 

        //Klikk på vis budsjettknappen
        client.click("viewBudgetForm:viewAbudget"); 

        //Sjekk at vi har skiftet side til view-budget.xhtml
        assertTrue(server.getCurrentViewID().contains("view-budget"));
    }
}

Kildekode

Kildekoden til denne artikkelen er tilgjengelig som en github repository.

Mer informasjon

Ø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”.

Magnar genererer testet og dokumentasjon [Luke 16, 2012]

Sunday, December 16th, 2012
Ingen kommentarer

Magnar Sveen er en meget hyggelig fyr jeg har presentert en gang tidligere på denne bloggen. I sommer klarte han med sin editor-magi å nesten få meg til å konvertere fra Vim til Emacs. Som Emacs-hacker programmerer Magnar endel Lisp, og i dagens blogpost deler han av sin erfaring omkring dette. Du behøver ikke kunne Lisp for å få utbytte av artikkelen, kun et åpent sinn – Magnar forklarer det meste underveis.

magnar

Hvem er du?
En glad utvikler som knaster taster på toget mellom Oslo og Fredrikstad hver dag.

Hva er jobben din?
Framsieutvikler og partner i Kodemaker.

Hva kan du?
Trives godt i hele stacken – oftest solgt inn som frontend.

Hva liker du best med yrket ditt?
Å bygge ting.


Eksempler i Lisp: Kode som genererer tester og dokumentasjon

Jeg fikk til noe kult her om dagen.

Som du vet så er ikke dokumentasjon det morsomste å skrive. Og det er skremmende lett å glemme å oppdatere dokumentasjonen når du gjør endringer.

Så når jeg skulle lage et liste- og et string-bibliotek til Emacs, så fant jeg en fet løsning på akkurat det problemet.

Se her:

(defexamples s-dashed-words
  "some words" => "some-words"
  "under_scored_words" => "under-scored-words"
  "camelCasedWords" => "camel-cased-words")

Her definerer jeg noen eksempler på hvordan en funksjon fungerer. Det kule er at jeg ut i fra disse genererer både tester og dokumentasjon.

La oss starte med det første:

Generere tester

Slik må testen se ut for at testrammeverket skal forstå den:

(ert-deftest s-dashed-words ()
  (should (equal (s-dashed-words "some words") "some-words"))
  (should (equal (s-dashed-words "under_scored_words") "under-scored-words"))
  (should (equal (s-dashed-words "camelCasedWords") "camel-cased-words")))

Så nå skal vi ta eksempelkoden øverst og bygge om til denne testkoden programatisk. Det gjør vi med makroer.

Noen introduksjoner

  • Makroer bygger ny kode. Du gir dem argumenter som så brukes til å sette sammen den endelige koden før den kjøres.

  • Homoikonisitet betyr at språket er definert ved hjelp av sine datastrukturer. Altså kan kode både kjøres, men også undersøkes, manipuleres og bygges på samme måte som annen data i applikasjonen.

Homoikonisitet er essensielt for å få bunnsolide makroer.

I lisp får vi dette fordi et funksjonskall er en liste:

(* 2 4)

Dette er en liste med tre elementer (*, 2 og 4), og når den evalueres så blir det første elementet i listen kalt som en funksjon, og resten som parametere til den funksjonen – og vi får 8.

Anatomien til en makro

I lisp så er altså jobben til en makro å lage en liste som skal evalueres. Du kan lage listen akkurat slik du selv vil, men det finnes noen triks for å gjøre det lettere. La oss starte med et enkelt eksempel:

(defmacro multiply (a b)
  (list '* a b))

Denne tar i mot to argumenter, og bygger en liste på tre elementer. Hvis du stusser over fnutten foran * så er det en quote. Det er slik man sørger for at symbolet ikke blir evaluert, men får stå som det er.

Altså vil (multiply 2 4) bygge koden (* 2 4) som deretter evalueres til 8.

Vi kan også quote hele lister, slik:

'(* 2 4)

Denne blir ikke evaluert til 8, men fortsetter å være listen på tre elementer. Det kan vi bruke når vi skal lage makroer:

(defmacro multiply (a b)
  '(* a b))

men det fungerer ikke helt. Dette vil returnere lista akkurat slik, altså (* a b) – så når denne skal evalueres så er a og b ikke lengre bundet. Men det peker mot trikset:

(defmacro multiply (a b)
  `(* ,a ,b))

Backtick lar oss quote lista, men hvor vi kan evaluere deler av uttrykket. Kommaet escaper ut av quotingen, slik at verdiene i a og b settes inn. Tenk ruby eller python sin string interpolation.

Kanskje skulle multiply kunne ta flere argumenter? Vi prøver:

(defmacro multiply (&rest numbers)
  `(* ,numbers))

Nøkkelordet &rest betyr at resten av argumentene havner i lista numbers. Men dette gir ikke ønsket resultat. Vi ender opp med (* (2 4 6)).

Istedet skriver vi:

(defmacro multiply (&rest numbers)
  `(* ,@numbers))

Escapingen ,@ tar lista i numbers og splicer inn. Da får vi (* 2 4 6) som ønsket.

Og med det er vi klare for å generere testene:

Generere tester, take two

Vi vil altså at dette:

(defexamples s-dashed-words
  "some words" => "some-words"
  "under_scored_words" => "under-scored-words"
  "camelCasedWords" => "camel-cased-words")

skal transformeres til dette:

(ert-deftest s-dashed-words ()
  (should (equal (s-dashed-words "some words") "some-words"))
  (should (equal (s-dashed-words "under_scored_words") "under-scored-words"))
  (should (equal (s-dashed-words "camelCasedWords") "camel-cased-words")))

Og det gjør vi ved å definere defexamples slik:

(defmacro defexamples (fn &rest examples)
  `(ert-deftest ,fn ()
     ,@(map (partial 'example-to-should fn) (partition 3 examples))))

Den tar i mot funksjonen vi tester som fn, og en liste med eksempler. Jeg stykker opp examples i tripletter, som da ser slik ut:

0: "some words"
1: =>
2: "some-words"

Disse mapper jeg over en example-to-should som ved hjelp av partial har fått satt første parameter til fnallerede.

Her er den:

(defun example-to-should (fn example)
  (let ((actual (nth 0 example))
        (expected (nth 2 example)))
    `(should (equal (,fn ,actual) ,expected))))

Som du ser så tar den første og siste element i lista, og bygger opp should-uttrykket. => blir bare ignorert.

Ved hjelp av kun disse to, en liten funksjon example-to-should og en liten makro defexamples, så gjør jeg eksemplene om til tester og kjører dem.

Men det som gjør dette fett, er jo at de samme eksemplene brukes til å generere dokumentasjonen.

Generere dokumentasjon

Ta en titt på den genererte dokumentasjonen her. Slik ser én entry ut:


s-dashed-words (s)

Convert s to dashed-words.

(s-dashed-words "some words") ;; => "some-words"
(s-dashed-words "under_scored_words") ;; => "under-scored-words"
(s-dashed-words "camelCasedWords") ;; => "camel-cased-words"

Som du ser så er eksemplene der, men også funksjonens parameterliste og en forklarende tekst. Hvor kommer de fra?

Homoikonisitetens vidundere

Se her:

(symbol-function 's-dashed-words)

Slik får jeg tak i funksjonen som symbolet s-dashed-words peker på.

Jeg kan kalle den:

(funcall (symbol-function 's-dashed-words) "camelCase")

;; => "camel-case"

Men det sprø her er at funksjonen er en liste som jeg kan grave meg inn i. Slik ser den ut:

(lambda (s)
  "Convert S to dashed-words."
  (s-join "-" (map 'downcase (s-split-words s))))

Det jeg funcall‘er der oppe er ikke en peker til noe kompilert i minnet. Det er denne listen.

Så når jeg kaller

(nth 0 (symbol-function 's-dashed-words))

får jeg ut lambda. Og når jeg kaller

(nth 1 (symbol-function 's-dashed-words))

får jeg ut (s). Og når jeg kaller

(nth 2 (symbol-function 's-dashed-words))

får jeg ut "Convert S to dashed-words.".

Og det er ved å kombinere eksemplene, og ved å grave meg inn i datastrukturen som definerer funksjonene at jeg setter sammen dokumentasjonen.

Koden for det er litt mer omfattende, men hvis du er nysgjerrig så finner du den her på github.

Til slutt

Det aller kuleste er at dokumentasjonen aldri går ut på dato. Hvis navn eller signaturer endrer seg, så gjenspeiles det øyeblikkelig i dokumentasjonen. Og eksemplene er alltid riktig.

Konge.

Henning har arvet kode [Luke 15, 2012]

Saturday, December 15th, 2012
Ingen kommentarer

Jeg har hengt endel rundt på de programmeringsrelaterte forumene på diskusjon.no, og en av dem jeg har lagt merke til der er brukeren GeirGrusom. Han har vært medlem siden 2003, og har i skrivende stund over 12 tusen innlegg. Det verste er at det meste jeg har sett ham skrive faktisk er nyttig og relevant for diskusjonen.

Så jeg sporet opp denne GeirGrusom – eller Henning Moe, som han faktisk viste seg å hete. Og han ville gjerne hjelpe til med årets julekalender.

henning

Hvem er du?
Hobbyutvikler blitt proff for å slippe å jobbe som truckfører.

Hva er jobben din?
Jobber som IT konsulent hos OKB AS.

Hva kan du?
Jeg jobber primært med C# og .NET i backend og er sjeleglad så lenge jeg slipper å jobbe med UI.

Hva liker du best ved yrket ditt?
Det beste med yrket er egentlig å jobbe med noe som faktisk påvirker tusenvis av mennesker hver eneste dag. Jeg får bare håpe påvirkningen er positiv.


Legacy

De fleste prosjekter har det: En del av kodebasen som ingen vil røre!

Legacy kode og unit-testing av dette er det skrevet bøker om, fordi det er en skitten jobb, og svært få vil faktisk ha noe med det å gjøre. Som regel er dette teknisk gjeld som aldri blir tilbakebetalt fordi man ikke har tid, eller prosjektledelsen ikke har ressurser til å takle det. Koden benytter ofte gamle konsepter som enten er sett på som utdatert, eller som aldri burde vært der i utgangspunktet. Det kan være fra en svunnen æra hvor man benyttet et merkelig programmeringsspråk, eller man trodde at string alltid var den beste måten å behandle data med (såkalt “string typing”). Det kan være svære klasser som benytter seg utelukkende av singletons (som er sett på som et relativt dårlig konsept, men likefullt nyttig i sammenheng som abstract factory), eller fullstendig logikk med kun statiske funksjoner (som er det verste en kan røre i forbindelse med unit-testing) fordi forfatteren kanskje ikke forstod objektorientering når det ble skrevet.

Jeg skal bare fortelle litt om mine erfaringer med legacy kode.

Legacykode kan være skrevet i et gammelt språk, eller i en dialekt som ikke lenger brukes (eksempelvis C# 1.0 selv om prosjektet ellers er skrevet i C# 4.0). For C# sin del betyr dette veldig mye kode med svakt typede komponenter slik som ArrayList eller HashSet mot List<T> og Dictionary<TKey, TValue> og som for C# sin del både betyr nedsatt ytelse, dårlig lesbarhet og vanskelig å vedlikeholde. Ofte blir det bare liggende fordi koden ikke er dekket av tester, og fordi utvikleren da løper en risiko for å ødelegge produksjonskode dersom dette ikke blir dekket av tester før noe som helst annet blir gjort, noe det ikke alltid vil bli satt av tid til.

Det er noen ting å være obs på når man skriver unit-tester: det hender at det er feil i legacy-kode som ikke blir fanget opp. Faktisk er det slik at en må ta en avgjørelse om man skal rette disse, for det hender sluttbrukere har gjort seg vandt til disse problemene, eller at andre deler av systemet har jobbet seg rundt disse.

Det jeg har merket er ganske vanlig er at behandling av lokalisering ofte er gjort feil. Bytt tallsystem eller valuta på maskinen og koden feiler. Dette fordi det er blitt hardkodet inn datoformater eller antar at desimalskilletegn alltid er punktum eller komma. Flere steder har jeg sett at standardinnstilling er satt til USA (av en eller annen grunn), og da er det snikende feil med datoer da USA bruker kanskje verdens mest dustete måte å skrive datoer på.

Sikker trådbehandling er også en tilbakevendende feil med eldre komponenter som ikke blir lagt merke til. Mange utviklere vurderer ikke dette når programmet blir utviklet, og kanskje det som regel ikke er et problem i produksjon, eller oppleves som et fantomfeil som er umulig å gjenskape i test. Det var også noe som er blitt satt mer fokus på i de siste årene da det er langt mer vanlig å ha mange prosessorer på systemene, og det er da logisk å ha et bedre rammeverk for behandling av flere tråder. Bytt ut suspekte komponenter med trygge versjoner. Selv om komponenten fungerer uten å være trygg, så er det liten skade i å være sikker på at den er trygg. Se etter dette i singletons og statiske funksjoner. Singletons som inneholder en state er bortimot garantert å kunne føre til slike feil.

Som en anekdote…

… kan jeg fortelle at jeg nylig fikk en feil i produksjon som jeg forårsaket. Problemet var at jeg rettet en feil i koden som var validering av fakturalinjer. Dette ble gjort med et regulærtuttrykk som sjekket et merkelig karaktersett, men dessverre sjekket om dette gjentok seg 0 eller flere ganger uten noen anchors, eller at den engang sjekket om en match gikk over hele uttrykket. Jeg rettet dette slik at valideringen faktisk gjorde noe, og det kom ganske fort klager fra kunder om at de ikke lenger klarte å fylle inn i fakturaer dersom visse tegn befant seg i dem.

Dette med validering av slikt er noe jeg er litt imot. Aldri utfør valideringer som ikke har noe funksjonelt grunnlag. Jeg kan garantere at det dukker opp sluttbrukere som bruker ´ eller ` istedet for ‘ eller folk med merkelige tegn i e-posten sin. Ikke utfør valdering for noe som ikke er en teknisk begrensning. Ved siden av å kaste bort tiden din, så vil det føre til flere support-tilfeller. Her har jeg opplevd at de som skriver spesifikasjonene legger inn begrensninger i validering utelukkende fordi de har sett de begrensningene andre steder. I mitt tilfelle var dette at spesifikasjonen hevdet at passord ikke kunne ha mellomrom og kunne ikke være lenger enn 10 tegn. Ved å teste, så viste det seg at dette ikke var tekniske begrensninger.

Det er også ofte en god del overutviklede komponenter i legacy kode. Dette er ofte klasser som en utvikler har skrevet, og beholdt som et lite privatprosjekt der en klasse blir utvidet og utvidet til å takle alt mulig som strengt tatt ikke burde vært nødvendig, kanskje over flere år. Problemet med klassene er ikke at de ikke fungerer, men at dersom kravene til komponenten endres, så er den kanskje så omfattende at det enkleste og billigste er å bare forkaste hele komponenten til fordel for en ny, og enklere, noe som gjør at en komponent som det er brukt mange timer på, bare må kastes fordi ingen lenger har kontroll på hva den gjør, eller hvordan den fungerer. Det jeg har opplevd av dette er en utrolig omfattende querystring klasse, ORM system, og programflytmodeller.

For meg er kode fin når den er simpel, løst bundet, sterkt typet og enkel å bruke.Legacy kode kan ofte ikke tilhøre noen av disse kategoriene, noe som er frustrerende dersom en skal skrive unit-tester av eksisterende kode. En må bare starte fra bunnen og skrive en integrasjonstest, og dermed starte med refaktorering i andre enden.

Tredjepartskomponenter er det første som må løsrives. Disse er verdiløse i en unit-test, og i beste fall suspekte i en integrasjonstest, og kan skape store problemer. Finn et mock-objekt rammeverk. Det er ikke spesielt gunstig om kode inneholder en hel masse override klasser som ikke gjør noe som helst, og enda verre om de inneholder mock-klasser som gjør noe spesielt. Ikke skriv simulatorer, eller lignende. Det er en bjørnetjeneste som noen andre kanskje må rydde opp i senere. Dersom det er enkelt å lage mock-objekter, så lag en som utelukkende returnerer forventet verdi. Dersom denne verdien blir generert av noe annet enn konstanter, så er det i utgangspunktet utestet kode i en unit-test, og det har ikke noe der å gjøre.

Som en konklusjon…

… så er det viktig å bare ha i bakhodet når man utvikler at kode må leses og kanskje videreutvikles av andre. Det er ikke farlig å ha mange klasser, bare navn, forståelse og kontekst er enkelt å skjønne. En kan ikke ha for mange interfaces (innenfor rimelighetens grenser), og for enkelt å unit-teste gjør det ingenting om kjernefunksjonalitet i en klasse faktisk er bestemt av interfaces gjennom inversion of control. Eksempelvis kan private funksjoner erstattes av et interface og en privatindre klasse som implementerer dette.

En vil alltid oppleve at kode man har skrevet vil bli legacy, men det er fint å sørge for å minimere problemene knyttet til dette. Få også andre til å gå over koden din, og det burde være noen som ikke er redd for å påpeke slike problemer. Hvis koden din alltid blir godkjent av den andre utvikleren, burde du kanskje finne en annen som er litt mer kritisk til det du gjør.

Siste kommentarer

best seo services company
I'm not sure where you are getting your information, but good topic. I needs to spend some time learning more or understanding more. Thanks for wonder...
Louis Vuitton Outlet
30 years old Kalamazoo-born Vitalia totally likes it barbecuing bicycling. Last but not least she is intrigued by charters and flights as an example, ...
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)). ...
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!