tirsdag 29. juni 2010 Clojure Kata
Et gjennomgående tema på denne bloggen er at utviklere selv må ta et aktivt ansvar for å å bli bedre. Blant annet må man trene på de ferdighetene man trenger i jobben sin, og her kommer kodekata'er inn som et viktig hjelpemiddel. Første gang jeg snakket om kodekata var i februar 2009.
Jeg har bl.a. én kata jeg har brukt flere ganger for å komme raskt inn i nye programmeringsspråk, og jeg viste hvordan jeg implementerte den med Erlang her. Det dreier seg om å lage et enkelt "kassaapparat" som et komandolinjeprogram. Det fine med denne oppgaven er at den kan begynne veldig enkelt, men at man kan utvide med ganske mye avansert funksjonalitet om man ønsker det. Man blir også nødt til å sette seg inn i hvordan man kommuniserer med kommandolinjen, noe som alltid er kjekt å beherske, og man må bruke noen enkle datastrukturer.
Jeg fokuserer ikke på automatiserte tester i denne kataen, men prinsippet om clean code er viktig. Når jeg lærer et nytt språk tenker jeg at det er viktig å raskt finne ut hvordan koden bør struktureres for å bli enkel å lese og refakturere.
Siden jeg nå holder på å lære meg Clojure har jeg selvfølgelig nettopp implementert et enkelt kassaapparat. Jeg tenkte det kunne brukes til å vise litt Clojure-kode til andre som kanskje har hørt litt om dette språket, og som vil se hvordan et lite program kan se ut. Så her er det komplette programmet, splittet opp i tre deler, med litt kommentarer.
Merk at Clojure-kode parses fra toppen av fila og nedover. Verdier og funksjoner må derfor deklareres før de kan brukes. Du vil dermed se detaljene først og "program-loopen" til slutt.
Gir det mening sålangt? Etter å ha skrevet Clojure-syntaks i en drøy uke er det plutselig ikke så lett å se om dette kan være vanskelig å tyde :) Jeg har nå definert et Map (tilsvarer Dictionary i .Net) med hvilke varer butikken har, og hva varene koster. Deretter har jeg definert noen funksjoner for å printe ut varene (med pris), for å finne prisen på en vare, og for å finne prisen på et gitt antall av en vare.
Og så kommer funksjonene får å kjøpe, vise innholdet av handlekurven, og for å gjennomføre betaling. Merk at handlekurven sendes inn som en parameter til alle disse funksjonene (i show-cart kalte jeg handlekurven for state, bare for å gjøre det vanskelig for deg).
Handlekurven er en array av arrays (eller Vector av Vectors, vi er tross alt i Java runtime nå), hvor hvert av de indre arrayene inneholder to elementer: navnet på en vare, og hvor mange det er av den. Buy, show-cart og checkout returnerer alle handlekurven – den samme eller en oppdatert kurv.
Og så kommer vi til siste del, som inkluderer en meny, input fra brukeren, og behandling av denne inputen.
De siste funkjonene her er kanskje det som er vanskeligst å skjønne. get-and-run-command prompter brukeren til å taste inn en kommando. Brukerens input brukes så til å velge en lambda-funkjon fra dispatch-table. Denne lambdaen kalles så med state som parameter, og returverdie fra lambdaen returneres som returverdi fra funksjonen. Alt dette skjer i linje 87.
Om det ikke finnes en lambda for det brukeren taster inn vil det bli kastet en NullPointerException, og da velger jeg en default lambda (linje 89).
run-program illustrerer hvordan tail-recursion fungerer i Clojure. I kallet til loop (linje 93) defineres det en verdi (variabel) kalt state, som i utgangspunktet er et tomt array – dette er handlevognen. Det gjøres så et kall til get-and-run-command, hvor vi sender inn state. Tilbake får vi returnert verdien vi kaller new-state, den oppdaterte handlevognen.
Til slutt kaller vi funkjonen recur, og sender inn denne nye handlevognen. Recur gjør at vi starter fra loop igjen, som om run-program ble kalt på nytt, men state får nå verdien som sendes inn i recur. Hele programmet vil altså stå og spinne inne i run-program (uten at stacken blir spist opp), og handlevognen oppdateres for hver iterasjon.
Denne måten å sende state/handlevognen rundt i et rekursivt kall var noe jeg lærte meg da jeg så på Erlang. Dette er en helt normal måte å holde på state innenfor funkjonell programmering, hvor man normalt ikke tillater (eller ønsker) variabler som muterer (endrer verdi). I stedet for å ha en global handlevogn som metodene aksesserer og endrer har vi nå metoder som sender handlevogner til hverandre.
(Faktisk implementerte jeg kassaapparatet uten denne teknikken først, for det er mulig å mutere verdier i Clojure. Seriøs refakturering inngår ofte som en del av kodekata'er.)
Ta også en titt på run-program-2, som gjør det samme som run-program, men med pipelining/threading. I linje 100 sendes state som input til metoden get-and-run-command. Resultatet fra det kallet sendes så som input til recur. Altså samme logikk, men enklere å se på :)
Jeg håper noen orket å lese denne blogposten, og ikke ble alt for avskrekket av alle parantesene. Ble du kanskje inspirert til å utføre eller komme opp med egne kataer? Del gjerne erfaringer eller dine egne ideer til lignende oppgaver med oss andre i kommentarfeltet.