Hva er en Monad?


fredag 13. august 2010 Monad Funksjonell programmering Clojure Ruby

yin_yang

Monad er et ord med mange betydninger; det brukes blant annet innenfor ulike filosofiske grener for å beskrive "universets essens", eller som et slags navn på Gud. Det kjente tegnet fra kinesisk filosofi og kosmologi, som kineserne kaller T'ai-Chi, kalles også the Great Monad i Vesten, og representerer den underliggende harmonien mellom motstridende krefter i universet (mann/kvinne, yin/yang, hard/myk, sol/måne). Men ordet har også en betydning i matematikken, og den er overført til informatikk, hvor det er mest kjent som en feature i programmeringsspråket Haskell.

Windows PowerShell er også kjent under kodenavnet Monad, men meg bekjent er dette ikke relatert til betydningen i de funksjonelle språkene.

Mange betrakter monads som det mest skumle og uforståelige med funksjonelle språk, og det tar tid å skjønne hva det er for noe. Jeg har akkurat begynt å forstå det selv, og dette er forklaringen jeg skulle ønske jeg hadde sett før jeg begynte å lese de mer teoretiske beskrivelsene. Forhåpentligvis kan dette være til hjelp for andre som sliter med det samme, men jeg gir ingen garantier.

Du skal ikke se bort fra at du har brukt monader allerede

Da jeg begynte å lese meg opp på F# lærte jeg at man har monads der også, men at man kaller dem computation expressions. Og så sa læreboken at LINQ i .net også er computation expressions. Tidligere har jeg også hørt at LINQ er en form for list comprehension, som jeg lærte meg da jeg begynte å bruke Erlang, og som jeg senere har funnet igjen i flere andre språk. (List comprehension er en slags spesialsyntaks for å generere lister.) Det viser seg at list comprehensions kan kalles bruk av en list monad, og monad comprehension er en mer generell variant av list comprehensions.

Ok, du ble kanskje ikke noe klokere av dette, men saken er at alle disse tingene henger sammen på et vis. Du vil forhåpentlig vis se lyset mot slutten av blogposten!

For å skape noen nye hjernekoblinger kan vi først ta tak i litt Ruby-kode. Følgende er en banal, liten kodesnutt som deklarerer et par variabler som brukes til å beregne og skrive ut en verdi:

n = 1337
m = n * 3.1415
puts m # Skriver ut: 4200.1855

Dette er imperativ kode, slik man for eksempel finner i C og alle etterfølgerne av det språket. I Ruby har vi derimot muligheten til å gjøre koden mer funksjonell ved å gjøre følgende:

def times_pi x
  yield x * 3.1415
end

times_pi(1337) {|m| puts m} # Skriver ut: 4200.1855

Legg merke til at det ikke er noen likhetstegn i denne koden – det deklareres ingen variabler. I stedet kaller man en metode (times_pi) hvor man sender med en anonym funksjon som et av argumentene (en kodeblokk). Denne kodeblokken kalles så i funksjonen ved å bruke nøkkelordet yield. Når metodekallet er ferdig finnes det ingen variabler – ingen state!

For å bli en dyktig Ruby-utvikler må man endre tankemønsteret man bruker fra kode som i det første, imperative eksempelet til kode som det man finner i det andre eksempelet hvor man bruker kodeblokker. Men dette er også et stort steg på veien mot å lære seg funskjonell programmering. Times_pi er nemlig en høyere-ordens funksjon (den tar en funksjon som et parameter), og koden har ingen variabler / state. Vi har snudd måten vi tenker på (en slags Inversion of Control).

Hva har dette med monad å gjøre?

Jeg skal snart vise deg en monad, men ta først en titt på følgende, skrekkelige eksempel på Clojure-kode:

; BAD BAD BAD - DON'T DO THIS EVER!!!
(def pi 3.1415)  ; pi = 3.1415
(def n 1337)     ; n = 1337
(def m (* n pi)) ; m = n * pi
(println m) ; Skriver ut: 4200.1855000000005

Her har jeg skrevet imperativ kode i Clojure, noe det ikke akkurat er laget for (men det fungerer). I virkeligheten ville jeg ha gjort det slik:

(let [pi 3.1415
      n  1337
      m  (* n pi)]
  (println m)) ; Skriver ut: 4200.1855000000005

Let lar meg definere et sett med verdier som så er tilgjengelig innenfor let-kallet. Når let-kallet er ferdig opphører verdiene å eksistere. Let er faktisk en monad – i Haskell er den kjent som Identity Monad. Det let gjør er å transformere argumentene sine til en kjede med funskjonskall, og resultatet blir noe sånn som dette:

((fn [pi]
     ((fn [n]
          ((fn [m]
               (println m)) 
           (* n pi))) 
      1337)) 
 3.1415) ; Skriver ut: 4200.1855000000005

Den koden er jo nærmest uleselig, og i alle fall ekstremt vanskelig å skrive (selv for dette lille eksempelet). Og det er hele poenget: Let-monaden lar meg på en enkel måte skrive imperativ-lignende kode – noe som ser ut som statements – som så blir omformet og kjedet sammen i en pipeline av funksjonskall (ren funskjonell kode).

Så hva er en monad?

Monader er altså i prinsippet, slik jeg ser det, syntaktisk sukker som omformer kode til funksjonskall etter bestemte regler. Tenk for eksempel på LINQ, som er en spørre-syntaks som under panseret blir omformet til funksjonskallene Select(), Where(), etc. Det er en formell teori knyttet til monad som snakker om bind og return, og forteller hvordan denne omformingen skal fungere. Dette er derimot ikke så viktig før man skal lage sine egne monads.., og jeg er ikke der enda. Det har derimot ikke hindret meg fra å bruke monads lenge uten å vite at det var det jeg gjorde i det hele tatt.

Monads er dermed ikke så skummelt lenger. List comprehensions og LINQ hjelper meg til å unngå løkker, og til å skrive bedre og mer deklerativ kode. Og let-monaden er helt uvurderlig i Clojure.

Andre monader

En annen, velkjent monad kalles Maybe-monad. Den fungerer i prinsippet slik at hvis én av handlingene i sekvensen/pipelinen returnerer null/ingenting så vil hele monaden returnerer det samme. På den måten kan monader forenkle kode hvor man normalt må sjekke for null. Det samme prinsippet kan brukes for å forenkle unntakshåndtering.

Ellers er vel ingen omtale av monad komplett uten å nevne I/O (input/output). Haskell bruker monads til dette, og det garanterer at I/O-handlinger (som jo har sideeffekter) kun utføres én gang, og i riktig rekkefølge. Det er altså ikke riktig at Haskell er et språk uten sideeffekter, slik jeg har blitt fortalt. Men språket krever at handlinger som har sideeffekter utføres i spesielle strukturer (altså monads) som håndterer sideeffektene på riktig måte. Dermed står man friere til å betrakte koden som "rent funksjonell", og man kan utføre andre deler av koden i "valgfrie" rekkefølger, benytte lazy evaluering effektivt etc.

Vel, det var mine betraktninger rundt monad, og en forklaring for dem som bare har litt erfaring med funksjonelle språk. Det er kun kort tid siden jeg følte at jeg begynte å forstå konseptet, så om du betrakter noe av dette som feil får du bare si fra. Min kunnskap er selvsagt langt fra komplett, så jeg vil gjerne høre andre synspunkter.


comments powered by Disqus