Meddelande
På gamla.pluggakuten.se kan du fortfarande läsa frågorna och svaren som ställts, men du kan inte skapa ett nytt konto eller nya trådar. Nya frågor och nytt konto skapar du på det nya forumet, välkommen dit!
Lisp-makron (en beskrivning)
- cancerman
- Medlem
Offline
- Registrerad: 2010-10-23
- Inlägg: 532
Lisp-makron (en beskrivning)
Jag tänkte försöka förklara vad som gör Lisp speciellt: makron.
För att kunna följa med i texten måste ni kunna lite Lisp, så jag tänker gå igenom det ni behöver veta innan jag beskriver makron (macros på engelska). Jag tänkte beskriva Clojure, för det är den Lisp-dialekt jag kan bäst, men det mesta jag säger gäller för andra Lisps med.
I Clojure så används prefixnotation. Det ser ut såhär (>
är en prompt, där man kan skriva kod):> (even? 2)
trueeven?
är namnet på en funktion. Frågetecknet ingår i namnet. I andra språk så hade motsvarande funktionsanrop sett ut såhär: even(2)
. Den största skillnaden är alltså att den första parentesen, (
, flyttas till vänter om funktionen. En annan skillnad är att komman inte används för att skilja argument från varandra, i stället används bara blanksteg. > (range 5 10)
(5 6 7 8 9)
Det är alltså inte så stor skillnad på funktionsanrop mellan Clojure och andra språk. En skillnad är dock att alla operationer ser ut på samma sätt; if-satser, aritmetiska operationer, o.s.v.> (if (even? 2) "ja" "nej")
"ja"
> (+ 1 2)
3
Operatorn kommer alltid först, och sedan argumenten, i en lista. Alla "anrop" är uttryck, även om operatorn inte är en funktion. Alla uttryck returnerar värden. I t.ex. Java, är vanliga if-satser inte uttryck.if (var) { statement1; } else { statement2; }
returnerar inget värde. Men, eftersom det ofta är bekvämt så finns i Java och C operatorn ?:
, som används på följande vis. var ? värde1 : värde2
Om var
är sant, får hela uttrycket värdet värde1
, annars värde2
. Om du burkar använda ?:
-operatorn i C eller Java så förstår du varför det är användbart med uttryck som returnerar värden.
Prefixnotation ser kanske lite underligt ut, men det medför en del fördelar. T.ex. så kan man ha fler tecken i namn på operatorer och "variabler" (det finns inte variabler i Clojure på samma sätt som i andra språk, men det är inte så viktigt just nu); t.ex. even?
, bit-and
, let*
, o.s.v. En annan fördel är att man kan ha flera argument till aritmetiska funktioner än i andra språk. > (+ 1 2 3 4 5)
15
Notera också att +
är en funktion som alla andra. I Java eller C så är de aritmetiska operatorerna speciella, men inte i Clojure. En nackdel med prefixnotation är att det ibland kan vara lite klumpigt för aritmetik. T.ex. så skrivs 1 + 2 * 3 - 1 * 4
såhär: (- (+ 1 (* 2 3) (* 1 4)))
. Men det kan man ändra på med makron, som vi snart ska beskriva, om man vill.
Varför vill man ha ett språk som ser ut såhär? Därför det är lättare att manipulera koden med program. Parenteserna är inte bara en syntaktisk regel, utan (even? 2)
är faktiskt en lista, precis som listan vi fick när vi evaluerade (range 5 10)
. Detta kan man förvissa sig om på följande sätt:> (list 1 2 3)
(1 2 3)
> (eval (list even? 2))
trueeval
kompilerar och evaluerar uttryck som man ger funktionen. Och det är exakt samma funktion som används när man kör kod från en källkodsfil. I en källkodsfil så finns dock text, medan eval tar datastrukturer, t.ex. listor, som indata. Det behövs nått där emellan för att göra om text till datastrukturer. För detta används den så kallade "reader:n".> (read-string "(even? 2)")
(even? 2)
> (eval (read-string "(even? 2)"))
true
Detta, tillsammans med att ett uttryck är ett träd, gör det enkelt att manipulera program. Listor och träd är ju programmerarens hemmaplan. Vårt aritmetiska uttryck (- (+ 1 (* 2 3) (* 1 4)))
t.ex. kan man skriva såhär, för att lättare se att det är ett träd:-
+
1
*
2
3
*
1
4
Hur drar man nytta av detta? Med makron. Det finns något i C man brukar kalla makron, och det finns vissa likheter, men Clojures makron är mycket mer kraftfulla. Delvis för att själva makromekanismen är bättre, men också för att det är mycket lättare att manipulera Clojurekod, eftersom den bara är träd av listor, där operatorn alltid är först.
Ett makro är nästan som en funktion, men med några viktiga skillnader. När man anropar en funktion så evalueras argumenten först, sedan blir värdena av argumenten indata till funktionen. Så är det inte med makron. Om vi text ska anropa(even? (+ 1 1))
så evalueras (+ 1 1)
först, och funktionen even?
får 2
som indata. Men om even?
hade varit ett makro, så hade inte (+ 1 1)
evaluerats, utan indata till makrot hade varit listan (+ 1 1).
I makrot kan man sedan göra godtyckliga transformationer med koden. Det som returneras av ett makro kommer att evalueras i stället för makrot. Typiskt är det som returneras av ett makro listor, d.v.s. kod.
T.ex. skulle man kunna göra ett makro som gör att man kan skriva koden baklänges.> (defmacro lisp-reverse [x]
(reverse x))
(x
är ett argument. []
är en annan datastruktur, en vektor.) Sedan kan man skriva kod såhär:> (lisp-reverse (1 2 +))
3
Om lisp-reverse
hade varit en funktion så hade eval
försökt evaluera (1 2 +)
först, vilket hade resulterat i ett fel. Varför är detta så bra? Tänk att det inte fanns for-loopar i Java, utan bara while-loopar. Man hade ju kunnat använda bara while-loopar, men ibland är det bekvämt med for-loopar. Som programmerare vet du hur du kan översätta en for-loop till en while-loop, men ändå så kan du inte få for-loopar i språket, för det finns inget sätt i Java att definiera nya syntaktiska konstruktioner. I Clojure, däremot, behöver man bara en typ av loop, så kan man själv göra alla andra. Om man tittar t.ex. på källkoden för while
i Clojure, så ser den ut såhär:(defmacro while [test & body]
`(loop []
(when ~test
~@body
(recur))))while
kan alltså skrivas i Clojure, och man behöver inte säga vad while
betyder i maskinkod i en kompilator, utan kan transformera ett while
-uttryck till något annat med Clojure. Man kan se vad för kod som egentligen kommer att evalueras när man skriver t.ex. (while (even? 2) (println "hej"))
genom att använda funktionen macroexpand
.> (macroexpand '(while (even? 2) (println "hej")))
(loop [] (when (even? 2) (println "hej") (recur)))
Man kanske kan tycka att man skulle lika gärna kunna lägga in while-loopar från början, så behöver man inte makros, men poängen är att programmeraren ska kunna utöka språket hur den vill.
Ett annat makro som är ganska häftigt är ->
. Ofta i Clojure händer det att man skriver uttryck som ser ut såhär: (reverse (range (get data 3)))
. Det kan vara jobbigt att läsa, dels p.g.a. de många parenteserna, och dels p.g.a. att det som kommer evalueras först är längst till höger. Men vi kan göra godtyckliga kodtransformationer med makron, så vi kan skriva ett makro som tar hand om just detta problem. Vi kan skriva om föregående uttryck såhär: (-> (get data 3) range reverse)
.> (macroexpand '(-> (get data 3) range reverse))
(reverse (-> (get data 3) range))
Notera att macroexpand bara expanderar makrot i den yttersta listan, och att ->
är definierat rekursivt. Det vi fick tillbaka är ju halvt transformerat. Men senare kommer eval
att stöta på makrot i den inre listan och expandera det med.
Jag tänkte visa ett exempel till: pattern matching. Pattern matching funkar såhär (i alla fall i Clojure): Du har några värden, säg x, y och z. Du vill returnera 1 om y är false, och z är true, men du bryr dig into om vad x är. Om x är false, y är true, z är vad som helst, så vill du returnera 2, o.s.v. Det kan man skriva såhär:(match [x y z]
[_ false true] 1
[false true _ ] 2 ;om x false, y true, z är vad som, returnera 2
[_ _ false] 3
[_ _ true] 4))
Jag hoppas ni förstår principen. Man skulle kunna göra samma sak med ett stort träd av if-uttryck. Först skulle man kunna testa vad x är, och sen, på båda sidor (vare sig det var true eller false) så gör man en ny if-sats och testar vad y är, o.s.v. Men det hade varit mycket jobbigare, och potentiellt oläsligt. Men med ett makro så kan vi beskriva hur Clojure ska transformera match-uttrycket ovan till ett sånt träd av if-satser.
Fast match
funkar även när värdena inte bara är true
eller false
(man skulle kunna testa om x var 1, 2, eller 3, t.ex.) så if-satser duger inte. Det som används är cond
, ett slags switch-case-liknande uttryck med flera alternativ än bara två.
Om man macroexpandar match-uttrycket ovan kan vi se att det blir ett träd av cond-uttryck, nämligen detta:
(cond
(= y false) (cond
(= z false) (let [] 3)
(= z true) (let [] 1)
:else (throw (java.lang.Exception. "No match found.")))
(= y true) (cond
(= x false) (let [] 2)
:else (cond
(= z false) 3
(= z true) 4
:else (throw
(java.lang.Exception.
"No match found."))))
:else (cond
(= z false) (let [] 3)
(= z true) (let [] 4)
:else (throw (java.lang.Exception. "No match found."))))
Notera att y testas först. Det är bara en optimering; y är den mest betydelsefulla variabeln i just detta match-uttryck.
Det skulle heller inte vara någon konst att skriva ett makro för infixnotation, så att man skulle kunna skriva> (infix 1 + 2 * 3)
7
Makron är alltså program som skriver program. Match-makrot är ett program som skriver träd av cond-uttryck.
Ett typiskt användsområde för makron är att abstrahera bort repetitiv kod. Clojure är inte objektorienterat, men i Common Lisp finns det ett objektsystem. När man definierar en klass gör man det med ett makro, och makrot genererar kod som definierar "getters och setters" för alla instansvariabler automatiskt. Ett annat exempel: Om man hade haft makron i Java så hade man inte behövt vänta så länge på foreach-loopar, och under tiden skrivafor (int i = 0; i < n; i++)
tusen gånger.
"Lisp is a programmable programming language." - John Foderaro, CACM, September 1991
Senast redigerat av cancerman (2011-12-05 18:02)
- cancerman
- Medlem
Offline
- Registrerad: 2010-10-23
- Inlägg: 532
Re: Lisp-makron (en beskrivning)
Hur många har läst egentligen? De som har läst, förstod ni? Eller är det nått som är oklart?
- wellwell
- Medlem
Offline
- Registrerad: 2010-04-02
- Inlägg: 374
Re: Lisp-makron (en beskrivning)
Bra initiativ; Pluggakuten behöver mer programmering. Jag läste inte så noga iom att jag inte kan lisp, det vore dock roligt om fler "tutorials" kunde komma (i lisp och i andra språk) så att de som var nyfikna på programmering kan pröva.
Keep up the good work
Jag gillar matte, typ.
- cancerman
- Medlem
Offline
- Registrerad: 2010-10-23
- Inlägg: 532
Re: Lisp-makron (en beskrivning)
Det var iofs meningen att de som inte kan Lisp ska kunna läsa.
- wellwell
- Medlem
Offline
- Registrerad: 2010-04-02
- Inlägg: 374
Re: Lisp-makron (en beskrivning)
okej får ta och kolla igenom det när jag har tid
Jag gillar matte, typ.
- Peppe L-G
- Medlem
Offline
- Registrerad: 2011-07-21
- Inlägg: 357
Re: Lisp-makron (en beskrivning)
Det är nog inget man hänger med på om man aldrig har programmerat innan, men jag är grymt nyfiken på vad som fick dig att skriva om makron bara sådär.
- cancerman
- Medlem
Offline
- Registrerad: 2010-10-23
- Inlägg: 532
Re: Lisp-makron (en beskrivning)
Grymt nyfiken? Jag vet att många här inte har så brett perspektiv när det gäller programmering. Dessutom ville jag bara skriva för jag har bytt tangentbordslayout och behöver öva. :p
Och ja, man bör nog kunna programmera i ett språk i alla fall.
Senast redigerat av cancerman (2011-08-22 10:40)