Programmerer.com

Keeping fun in the house

En deilig implementert and

Lite er så godt som en deilig implementert and.

Ta en titt på denne. Dette er en Clojure macro. Hvis du ikke kan Clojure, gjør ikke det noe, dette skal bli veldig forståelig likevel.

(defmacro and
"Evaluates exprs one at a time, from left to right. If a form
returns logical false (nil or false), and returns that value and
doesn't evaluate any of the other expressions, otherwise it returns
the value of the last expr. (and) returns true."
  {:added "1.0"}
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

(Hvis du vet hva en macro er i språket C, må du umiddelbart glemme den definisjonen du kan. En macro i et lisp-språk er ikke det samme.)

Macroen er verkøyet som gjør en vanlig, dødlig programmerer til Gud over Gudene.

Nå er det jo slik at ethvert programmeringsspråk, i hendene på Den Rette, er et gudommelig verktøy. Det lar deg Skape: Når du programmerer, tar du en tanke og gjør den til virkelighet.

Det er sterke krefter du besitter. Likevel: macroen hever deg til et nytt nivå. Et meta-nivå, i forhold til der du var.

For mens du før var bundet av mulighetene i det språket du programmerte, er du nå i stand til å forme selve språket.

Du kan skape muligheter som ikke fantes før.

Clojure selv et er godt eksempel, for Clojure er for en stor del skrevet i Clojure. Til og med noe så basalt som operatøren and, er skrevet i språket selv.

Det hadde ikke vært mulig uten macroer. (Prøv å implementere and i ditt språk, uten å bruke den innebygde operatoren. Prøv så å lage unless.)

Så la oss ta en titt på hvordan dette er gjort.

(defmacro and
"Evaluates exprs one at a time, from left to right. If a form
returns logical false (nil or false), and returns that value and
doesn't evaluate any of the other expressions, otherwise it returns
the value of the last expr. (and) returns true."
  {:added "1.0"}
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

Denne macroen heter and, og har tre forskjellige «inngangsporter». Den kan ta ingen, ett, eller flere argumenter, og bruker forskjellige «kropper» alt ettersom hvor mange argumenter den blir kalt med. Den første inngangsporten, har en enkel implementasjon:

  ([] true)

Dette sier: Hvis du kaller macroen med ingen argumenter [], så returner true.

Et kall uten argumenter ser slik ut: (and), og det returnerer true.

user> (and)
true

Hvis du kaller and med ett argument, får du verdien av det argumentet tilbake.

  ([x] x)

Det kan vi test ut:

user> (and true)
true
user> (and false)
false
user> (and 3)
3
user> (and wdcd)
CompilerException java.lang.RuntimeException: Unable to resolve symbol: wdcd in this context, compiling:(NO_SOURCE_PATH:1)

Aha. Symboler som har en verdi, i hvert fall, de får vi rett tilbake. (La oss glemme det siste forsøket her, det får være tema for en annen gang. (Hvis du virkelig ønsker å gå i dybden, ta en titt på min implementasjon av lisp, I Groovy, med forklaring.))

Så kommer vi til kjøttet i anda.

  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

Hvis vi sender inn et argument x, og flere argumenter (i en liste som nå heter) next, så skaper vi nå litt kode, on the fly. Clojures «backtick» operator

`

gjør noe vi er vant til fra web templating, men ikke i kode: Den sier at det som kommer nå, skal settes verbatim inn i koden til kalleren. Det betyr «quote», med andre ord.

Når vi ikke ønsker å quote, altså når vi faktisk ønsker oss verdien av noe inne i denne templaten vår, bruker vi unquote:

~

Så når kalleren kjører denne koden, skjer dette: Vi lar symbolet and# (vi bruker # for navngiving av variabler i macroer, for å unngå navnekonflikt med eventuelle variabler kalleren har i sin egen kode)… altså … vi lar symbolet and# være verdien av unqote x — altså x som ble sendt inn i makroen med kallet.

Hvis vi kalte (and true), så er variablen #and == true, nå.

Med dette gitt, kjører kalleren videre.

      (if and# (and ~@next) and#))))

Hvis det er slik at and# == true, så kaller vi macroen and igjen (med noen argumenter ~@next, se neste paragraf). Hvis and# !== true, så returnerer vi and# selv:

user> (and false true)
false
user> (and false 1)
false
user> (and false 3 2 1)
false

Ok, nå vet vi hva som skjer her:

      (if and# (and ~@next) and#))))

når and# er false (vi får den i retur). Hva når and# er true?

Siden det jo er den kjente and-operatoren vi driver og implementerer, så må jo også resten av argumentene være sanne, for at and skal returnere true, sånn alt i alt.

Husker du inngangsporten vår for denne versjonen av and-macroen?

  ([x & next]

Vi sender altså inn ett argument x, pluss en liste med de resterende argumentene, next.

Denne mystiske karen her:

~@next

er Clojure sin syntax for «unsplice». La oss for enkelhets skyld si at den tar hele listen av argumenter som ligger i next, og flater den ut. (Se http://clojuredocs.org/clojure_core/clojure.core/unquote-splicing for en mer nøyaktig beskrivelse av unquote splicing.)

Vi skal jo kalle and igjen, og vi ønsker ikke å sende hele listen med argumenter til x (og ingenting til next), for da blir

  ([x] x)

kalt (og da får vi bare returnert listen av argumenter, og det der jo dumt), istedenfor

  ([x & next]

som fortsetter evalueringen.

Det som er nesten magisk, og som gjør dette til en så vakker implementasjon av and, er at vi nå er ferdige.

Hvordan kan det ha seg?

Jo, Clojure and oppfører seg sånn:

user> (and true)
true
user> (and true true)
true
user> (and true 1 true)
true
user> (and true 1)
1

Altså: Hvis alle verdier er sanne, returner den siste verdien, ellers returner false.

Deilig, deilig, deilig. And.

Category: løsningsdesign