1 2 Dagens föreläsning En konstruktion i Common Lisp som möjliggör att man kan skapa ny Lisp-kod, som i sin tur beräknas. - makrobegreppet - makrofunktioner - special form - makrodefintion - backquote - rekursivt definierade makrofunktioner - problem med namnkollision - vad kan definieras som makros: - ej beräkning av argumenten till en funktion, ex trace, defun - utvigning av språket - egna styrstrukturer, dvs argumenten beräknas speciellt, ex while, for - effektivisering, abstrakta datatyper Jämför föregående föreläsning: Funktion - interpretator/beräknare Makro - kompilator/översättare Makro-begreppet Begreppet förekommer i samband med progamspråk eller programmering, men kan ha en lite olika betydelse i olika sammanhang. I samband med programspråk innebär detta ofta en förbearbetning av programtext. En preprocessor. Till exempel att textsträngar byts ut mot andra. C med #define där konstant text kan ersättas: #define PROGRAM int main( void ) #define BEGIN { #define END ;} PROGRAM int main( void ) BEGIN { printf( "Hallo!\n" ) printf( "Hallo!\n" ) END ;} Man kan göra in-line av en funktion: #define sgn(a) (a< 0? -1 : 1) x = pow ( fabs ( x ), 2.0 ) * sgn ( x ); x = pow ( fabs ( x ), 2.0 ) * (x< 0? -1 : 1); Man kan styra med villkorliga makro med #if. 3 4 Makrofunktioner i Lisp Olika slags funktioner i Lisp Vi kan generalisera denna förprocessering till att utföras när programmert bearbetas, direkt under interpreteringen eller vid kompilering. Eftersom Lisp-program är representerade i språkets egna datstrukturer kan vi göra denna bearbetning på den strukturella - representations -nivån i stället för på text-nivån. Vi kan nu definiera makrofunktioner med hela Lispspråkets kraft. Vi behöver inget speciellt makrospråk. Med denna makro-facilitet är det enkelt att bygga ut språket med nya konstruktioner. Lisp-språket självt utnyttjar denna möjlighet att skapa nya kontrsuktioenr mycket omfattande. Normala funktioner Beräknar sina argument enligt call-by-value. Beräkna argumenten först, applicera sedan funktionen. Hanteras av Lisp s apply-funktion. Special form Funktioner som är hårdkodade direkt i Lisp-interpretatorn. Nya kan ej definieras! if, progn, quote, setq, return,... Makrofunktioner Funktioner i Common Lisp som vanligen beräknar sina argument på eget sätt, som kan överföras till normala funktioner eller special form. Nya kan definieras! cond, or, and, setf, dotimes
5 6 I Lisp-evaluatorn (minlisp från kap 21 och föregående föreläsning) kan vi observera följande: (defun mini-eval (form env) ; uttryck x symboltabell -> uttryck (cond ((constant? form) (evaluate-constant form)) ((variable? form) (lookup-value form env)) ((conditional? form) (eval-conditional (clauses form) env)) (t (mini-apply (function-part form) (evalargs (args-part form) env) env)))) special form Här bearbetas vanliga call-by-value - funktioner beräkna alla argumenten (defun mini-apply (fn args env) ; funktion x argumentlista x symboltabell -> uttryck (cond ((primitive-function? fn) (primitive-apply fn args)) ((function-symbol? fn) (mini-apply (lookup-definition fn) args env)) ((lambda? fn) (mini-eval (body fn) (extend-env (bind (formal-params fn) args) env))) (t (error "u.d.f - undefined function ~s" fn)))) Definition av egna makrofunktioner. (defmacro makronamn parameterlista makrokropp) Obs. Parametrarna binds till de oevaluerade aktuella parametrarna (ej som för vanliga funktioner, där argumenten först beräknas) Makroexpansion - tvåstegs process översättning och beräkning (makrofn arg1 arg2... argn) => makrofunktionen för makrofn appliceras på argumenten och översättning => ger som resultat ett nytt Lisp-uttryck => som i sin tur ges för beräkning Jämför från tidigare föreläsning: funktion - interpretator -> ger ett värde makro - kompilator -> ger programkod 7 8 När sker makroexpansion? vid interpretering direkt efter varandra: översättning beräkning vid kompilering två olika tillfällen: översättning vid kompilering beräkning vid exekveringstillfället (av den kompilerade koden) Detta betyder att man vid definition av en makro ej får förutsätta kännedom om variabler, funktioner mm som ej är tillgängliga vid makroexpansionstillfället. Därför kan man vanligen ej använda eval. Med interpretatorfunktionen eval kan vi beräkna ett Lisp-uttryck vi skapat. (eval (list first (list quote (a b c)))) -> (eval (first (quote (a b c)))) -> (first (quote (a b c))) => a Interpretatorfunktionen apply är en variant av funcall. (apply # (lambda (op args) (cons op args)) (+ (1 2 3)))) => (+ 1 2 3) lista med färdigberäknade argument Makroexpansion kan då i dessa interpretatorfunktioner beskrivas enligt följande: Beräkning av makrouttrycket makuttr = (makrofn arg1... argn): görs genom att Lisp gör följande: (eval (apply makrofunktionen för (first makroutr) (rest makuttr)))
9 10 Om vi på något sätt kan känna igen makrofunktioner, så kan vi implementera makroutveckling i minilisp enligt följande: (defun mini-eval (form env) ; uttryck x symboltabell -> uttryck (cond ((constant? form) (evaluate-constant form)) ((variable? form) (lookup-value form env)) ((conditional? form) (eval-conditional (clauses form) env)) ((macro-function? form) (mini-eval (mini-apply (lookup-macro-def (function-part form)) (args-part form) env) env)) ingen evaluering av argument makroutvecklingen jämför (t (mini-apply (function-part form) (evalargs (args-part form) env) env)))) Funktionen evalargs evaluerar argumenten i en argumentlista. Exempel: Vi önskar använda två-vägs cond-uttryck, som vi kallar cond-2. Språket tillhandahåller endast ett if-uttryck. (defun f (l) (cond-2 ((endp l) nil) (t (first l))) ) Definiera en makrofunktion för cond-2: - 2 fall (defmacro cond-2 (cl1 cl2) (list if (first cl1) (second cl1) (second cl2))) En test av funktionen f med ett argument: (f (a b c)) -> (cond-2 ((endp l)...)...) -> l binds till (a b c) makroutveckling av cond-2: -> (eval (apply # (lambda (cl1 cl2) (list if (first cl1) (second cl1) (second cl2))) (((endp l) nil) (t (first l)))) cl1 binds till ((endp l) nil) cl2 binds till (t (first l)) -> (eval (if (endp l) nil (first l))) => a kod blir argument 11 12 Backquote som quote, men uttryck inne i det backquote ade uttrycket kan beräknas. Mycket lämplig att använda i samband med att man definierar makrofunktioner. Uttryck efter kommatecken beräknas! (a,(+ 13 5) (x,(list a b))) => (a 18 (x (a b))) Jämför med quote: (a (+ 13 5) (x (list a b))) => (a (+ 13 5) (x (list a b))) (let ((l (a b c))) (a (,(second l) c d),(rest l)) => (a (b c d) (b c)) Backquote (a,(+ 13 5) (x,(cons a b))) är samma som (list a (+ 13 5) (list x (cons a b))) (backquote) är ett sk readmacro som utförs vid inläsningstillfället (samma som vanliga quote) Dessutom finns,@ som slice ar in en lista (let ((lista (x y))) (a,@lista b)) => (a x y b)
13 14 Makrodefinitionen av cond-2 (defmacro cond-2 (cl1 cl2) (list if (first cl1) (second cl1) (second cl2))) kan därför ersättas med: (defmacro cond-2 (cl1 cl2) (if,(first cl1),(second cl1),(second cl2))) Lisp använder makroutveckling Med funktionen macroexpand-1 kan man se resultatet av makroutvecklingen. Även cntrl-c m och menyn under ACL -> debug. (macroexpand-1 (cond (a b) (t c))) => (IF A (PROGN B) (COND (T C))) (macroexpand-1 (and a b c)) => (COND ((NOT A) NIL) ((NOT B) NIL) (T C)) (macroexpand-1 *) (1) => (IF (NOT A) (PROGN NIL) (COND ((NOT B) NIL) (T C))) (macroexpand-1 (or a b c)) => (COND (A) (B) (T C)) (macroexpand-1 *) (LET ((#:G6373 A)) (IF #:G6373 #:G6373 (COND (B) (T C)))) (1) I Common Lisp betyder på toppnivån * (som variabel) senaste utmatning. 15 16 Användning av makrofunktioner Oevaluerade argument Effektivisering Kompilering av primitiver, genom att ersätta anrop med definitionen och substituera in aktuella parametrar. (defun skapa-klockslag (min tim) (cons min tim)) kan då ersättas med makrofunktionen: (defmacro skapa-klockslag (min tim) (cons,min,tim)) Koden: (... (skapa-klockslag m (start-tim s))...) makroutvecklas till (... (cons m (start-tim s))...) som sedan kan kompileras till t. ex. maskinkod I många funktioner quote ar man ej argumenten, ex (trace remove) (defun f (x y) (list y x)) (setq kalle (+ 10 20)) Vi har definierat en boka-funktion där vi skriver: (boka 1 jan (10 30) (12 30) möte ) Antag vi vill ha en boka utan att behöva quote a argumenten: (boka 1 jan (10 30) (12 30) möte ) (defmacro boka (d m start slut akt) (boka-int,d,m,start,slut,akt)) där boka-int är den utsprungliga boka-funktionen
17 18 Annan evalueringsordning av argument (dvs ej call-by-value) Skriv en funktion om som fungerar som if. Fungerar (defun om (p s1 s2) (if p s1 s2)) Javisst! (om (> 10 5) (+ 10 2) (- 10 2)) => 12 Men? (defun fak (n) (om (= n 0) 1 (* n (fak (1- n))) )) (fak 3)? (fak 2) -> (om nil 1 (* 2 (fak 1))) -> (om nil 1 (* 2 (om nil 1 (* 1 (fak 0))))) -> (om nil 1 (* 2 (om nil 1 (* 1 (om t 1 (* 0 (fak -1)))) ))) -> (om nil 1 (* 2 (om nil 1 (* 1 (om t 1 (* 0 (om nil (* -1 (fak -2))) )))))) -> i oändlig repetition! Definiera om som en makro: (defmacro om (p s1 s2) (if,p,s1,s2)) 19 20 Annat exempel: Öka en global variabel med ett värde. (setq nr 10) (incr nr (+ 2 3)) - öka nr med 5, dvs -> (setq nr (+ nr (+ 2 3))) nr => 15 (defmacro incr (glob-var val-expr) (setq,glob-var (+,glob-var,val-expr)))) Detta problem kan lösas i vissa andra språk med en möjlighet att ha utparametrar och parameteröverföringsmodellen call-by-reference. I Ada kommer ni träffa på denna möjligheten. Finns ej i Java. Rekursivt definierat makro Givet en binär operator binplus. Skapa en operator plus som tar ett godtyckligt antal argument. Använd makro och gör en stegvis översättning av uttrycket till binplus. (plus) -> 0 (plus x) -> x (plus x y z 8) -> (binplus x (plus y z 8)) (defmacro plus (&rest args) (cond ((endp args) 0) ((endp (rest args)) (first args)) (t (binplus,(first args) (plus,@(rest args))) )))
21 22 Tillägg av nya styrstrukturer Exempelvis iterativa strukturer från Ada (while, for etc) (let ((i 10)) (while (> i 0) (print i) (setq i (- i 1)))) skriver ut 10, 9, 8, tom 1 Hitta motsvarande iterativa konstruktion och använd den som mall. (let ((I 10)) (loop (unless (> i 0) (return nil)) (print i) (setq i (- i 1))))) Makrodefinition av while (defmacro while (pred &rest exprs) (loop (unless,pred (return nil)),@exprs)) I Common Lisp Det finns en mycket omfattande loop-konstruktion, som tillåter alla tänkbara sätt att vilja beskriva en repetition. Dessa makroutvecklas. (Se t ex http://www.franz.com/support/documentation/8.2/ansicl/dictentr/loop.htm) (loop for i from 1 to 10 do (print i)) skriver ut 1, 2... 10 (macroexpand-1 (loop for i from 1 to 10 do (print i))) =>(utvecklas till lite förenklat) (block nil (let ((I 1)) (tagbody next-loop (print I) (setq I (+ i 1)) (when (> i 10) (go end-loop)) (go next-loop) end-loop))) 23 24 (defun f (max) (loop for n from 1 to max when (oddp n) collect n)) (f 10) => (1 3 5 7 9) (macroexpand-1 (loop for n from 1 to max when (oddp n) collect n)) => (utvecklas till förenklat) (block nil (let* ((n 1) Tekniken att bygga upp resultatet tas upp mer i nästa föreläsning (loop-list-head (list nil)) (loop-list-tail loop-list-head)) (tagbody next-loop (if (oddp n) (setf (cdr loop-list-tail) (setq loop-list-tail (list n)))) (setq n (+ n 1)) (when (> n max) (go end-loop)) (go next-loop) end-loop (return-from nil (cdr loop-list-head)))))) Domänspecifika programmeringsspråk (DSL) Ett specialiserat språk för en domän med sin egen syntax. På wikipedia via domänspercifika språk har vi ett scriptspråk för att beskriva ett spel. (Ett kommersilett bolag: http://en.wikipedia.org/wiki/ Zillions_of_Games#Scripting_language T ex tic-tac-toe.
25 26 Domänspecifika programmeringsspråk (DSL) Här använder man syntaxen för S-notation (dvs den vi använder i Lisp). Ett alternativ hade kunnat vara XML-taggar. (game (title "Tic-Tac-Toe") (description "...") (history "...") (strategy "...") (players X O) (turn-order X O) (board (image "images\tictactoe\tttbrd.bmp") (grid (start-rectangle 16 16 112 112) ; top-left position (dimensions ;3x3 ("top-/middle-/bottom-" (0 112)) ; rows ("left/middle/right" (112 0))) ; columns (directions (n -1 0) (e 0 1) (nw -1-1) (ne -1 1)))) (piece (name man) (help "Man: drops on any empty square") (image X "images\tictactoe\tttx.bmp" O "images\tictactoe\ttto.bmp") (drops (add-to-empty))) (board-setup (X (man off 5)) (O (man off 5))) (draw-condition (X O) stalemated) (win-condition (X O) (or (relative-config man n man n man) (relative-config man e man e man) (relative-config man ne man ne man) (relative-config man nw man nw man)))) Domänspecifika programmeringsspråk (DSL) Hur implementerar vi det domänspecifika språket? Vi kan skriva en interpretator alt. kompilator. Vi kan med hjälp av makroutveckling i Lisp kunna köra detta program (det är i S-notation) och för varje kommando skapa en makro som utvecklas till sådan kod som skapar och kör spelet. En del uttryck kan kanske direkt var körbara i Lisp (defmacro game (&rest commands)...) (defmacro title (name)...)... Vi har då genomfört en översättning av kod i det domänspecifika språket till Lisp-kod. 27 28 Problem att ta hänsyn till Ny lösning av problemet att öka global variabel med ett värde. (setq nr 10) (incr nr (+ 2 3)) skall översättas till: -> (setq nr (+ nr (+ 2 3))) felaktigt översatt till: -> (setq nr 15) (defmacro incr (glob-var val-expr) (list setq glob-var (+ (eval glob-var) (eval val-expr)))) Fungerar interpreterat men ej kompilerat! Se till att inte beräkna uttryck i onödan! Ibland blir det helt fel! Exempel: Som tidigare med incr, men öka två variabler med samma värde. (setq nr 10) (setq size 20) (incr2 nr size (+ 2 5)) - öka både nr och size med 2 nr => 17 size => 27 (defmacro incr2 (var1 var2 val-expr) (setq,var1 (+,var1,val-expr)) (setq,var2 (+,var2,val-expr)))) val-expr kommer att beräknas två gånger - tar extra tid - sidoeffekter utförs flera gånger (incr2 nr size (print 7))
29 30 Lösningen är att införa lokal variabel! (defmacro incr2 (var1 var2 val-expr) (let ((value,val-expr)) (setq,var1 (+,var1 value)) (setq,var2 (+,var2 value))))) (incr2 nr size (+ 2 (print 5))) -> (let ((value (+ 2 (print 5)))) (setq nr (+ nr value)) (setq size (+ size value)))) Utskriften av 5 kommer bara att ske en gång! Men det kan även bli variabelkollision då vi inför egen lokal variabel. Vad händer här? (setq nr 10) (setq value 100) (incr2 nr value (+ 2 5)) value =>? nr =>? (incr2 nr value (+ 2 5)) -> (let ((value (+ 2 5))) (setq nr (+ nr value)) (setq value (+ value value)))) 31 Lösning: Vi behöver ett unikt namn för den lokala variabeln. I Lisp finns en funktion gensym (generate symbol) som genererar en ny symbol. (gensym a ) => #:a11 (gensym a ) => #:a12 (defmacro incr2 (var1 var2 val-expr) (let ((local-var (gensym value ))) (let ((,local-var,val-expr)) (setq,var1 (+,var1,local-var)) (setq,var2 (+,var2,local-var)))))) (incr2 nr value (+ 2 5)) -> (let ((#:value7 (+ 2 5))) (setq nr (+ nr #:value7)) (setq value (+ value #:value7))))