IFT359 – Programmation fonctionnelle Thème #8 Création de nouvelles formes syntaxiques 1
Utilisation des macros Modifier la méthode d’évaluation des opérandes et des sous- opérandes –i.e. créer un nouvel élément syntaxique –Ne pas évaluer une opérande –Évaluer plusieurs fois une opérande Inlining –À éviter, laissons travailler le « compilateur » Ne pas les utiliser si une procédure peut le faire –Ne sont pas des objets de première classe
Formes spéciales et macros On peut supposer que les formes spéciales λ, if, set! et begin sont définies dans le “compilateur” –voir “ Fully Expanded Programs » dans le guide de référence Fully Expanded Programs –idem pour les procédures primitives e.g. cons, car, cdr. Les autres formes spéciales (e.g. cond, case, when, let, and, or ) peuvent alors être définies grâce aux macros dans DrRacket –i.e. DrRacket est en partie implémenté en DrRacket à partir d’un petit nombre de structures de contrôle primitive (formes spéciales) et d’un petit nombre de procédures primitives.
Macros Les macros sont définis à l’aide de define-syntax. define-syntax retourne une fonction appelée syntax- transformer, –c’est un autre nom pour macro, mais plus explicite, cette fonction est utilisée pour transformer une expression syntaxique en une autre. –la transformation d’une expression syntaxique se fait à la phase expansion de l’exécution d’une programme. la première phase de l’exécution est celle du reader la seconde est l’expansion la troisième est l’exécution proprement dite (run time) Il y a deux formes syntaxiques qui peuvent nous aider à définir de nouvelles formes syntaxiques avec define-syntax –syntax-rules (simple, mais limité) –syntax-case (complexe, mais complète)
Macros du préprocesseur C Le C offre un préprocesseur, dont la directive #define permet de créer des macros #define max(a, b) (a > b) ? a : b Les #define manipulent directement le fichier source et non la structure du programme Les #define ne respectent pas la portée lexicale des variables Les #define ne supportent pas un nombre variable d’arguments (ANSI C)
Autres exemples de #define #define sqr(x) x * x #define swap(a, b) \ int temp = a; \ a = b; \ b = temp
Les problèmes avec #define int x = 5; int y = 4; max(x++, y); int i = 4; sqr(i + 1); int j = 5; int xx = 12; swap(j, xx); #define max(a, b) \ (a > b) ? a : b #define sqr(x) x * x #define swap(a, b) \ int xx = a; \ a = b; \ b = xx a++ est fait deux fois : (a++ > b) ? a++ : b priorité des opérateurs : i + 1 * i + 1 capture de variables : int xx = 5; \ j = 5; \ xx = xx
Les macros en DrRacket Elles respectent la portée lexicale –Hygiène Elles ne polluent pas l’environnement courant –Transparence référentielle Il n’y a pas de conflit avec l’environnement dans lequel elles sont définies Elles sont basées sur du pattern-matching plutôt que directement sur le texte du fichier source
syntax-rules (define-syntax id (syntax-rules (lit 0 … lit m ) [pattern 1 template 1 ] [pattern ns template ns ] … ) ) Les lit i et les ellipses ( … ) peuvent apparaître dans les patterns et les templates ; ne sont pas des identificateurs pouvant avoir une valeurs au moment de l’exécution; elles n’ont qu’un rôle syntaxique. Les lit i sont des éléments syntaxiques servant à reconnaître un pattern. Les … indiquent que l’expression qui le précède est répéter de 0 à n fois
exemple : make-procedure (define-syntax make-procedure (syntax-rules () [(_ (p...) body) (λ (p...) body)])) (make-procedure (x) (* x x)) # le _ signifie ici make-procedure
my-define-values (define-syntax my-define-values (syntax-rules () [(_ (id0 ids...) (v0 vs...) ) (begin (define id0 v0) (my-define-values (ids...) (vs...)))] [(_ () ()) (void)])) Comme il y a un seul template par pattern, begin est souvent utile.
when (define-syntax when (syntax-rules () [(_ test then0 then1...) (if test (begin then0 then1...) (void))]))
and (define-syntax and (syntax-rules () [(_) #t] [(_ e) e] [(_ e1 e2 e3...) (if e1 (and e2 e3...) #f)])) Pourquoi 3 patterns ?
cond (define-syntax cond (syntax-rules (else) [(_ [else exp1 exp2...]) (begin exp1 exp2...)] [( _ [test exp1 exp2...]) (when test (begin exp1 exp2...))] [( _ [test exp1 exp2...] cl1 cl2...) (if test (begin exp1 exp2...) (cond cl1 cl2...))])) Ce n’est pas le véritable cond on ne traite pas le => ; le cas ou une clause ne contient qu’un élément, i.e. le test.
let (define-syntax let (syntax-rules () [(_ ([x v]...) e1 e2...) ((λ (x...) e1 e2...) v...)] [(_ name ([x v]...) e1 e2...) (letrec ([name (λ (x...) e1 e2...)]) (name v...))])) On triche parce que letrec est défini avec let
Temps pour évaluer une expression (define-syntax time (syntax-rules () [(_ e) (let ([start (current-milliseconds)]) e (- (current-milliseconds) start ))]))
swap (define-syntax swap (syntax-rules () [(_ v1 v2) (let ([tmp v1]) (set! v1 v2) (set! v2 tmp))])) Pourquoi faut-il une macro ? (define swap (λ (v1 v2) (let ([tmp v1]) (set! v1 v2) (set! v2 tmp)))) (define x 1) (define y 2) (swap x y) x
Limitations de syntax-rules Ne permet pas de créer des identificateurs lors de la phase exécution –define-struct permet de créer plusieurs identificateurs ne sera probablement pas vu cette session Ne permet pas de violer l’hygiène au besoin –(aif (f 3) (display it) (display (error "!")) si (f 3) alors affiche le sinon affiche erreur Toujours en position d’opérateur
Exercice 19 (define-values* (a b c) (2 (+ a 4) (* a b))) a 2 b 6 c 12 (define-values* () ()) Définir define-values* une macro qui simule define-values. Vous pouvez utiliser define.
Exercice 20 (define-syntax define-values* (syntax-rules () [(_ (ids...) (exps...)) (begin (define ids exps)...)] )) Définir define-values* une macro qui simule define-values. Vous pouvez utiliser define. (define-syntax define-values* (syntax-rules () [(_ (id1 ids...) (exp1 exps...)) (begin (define id1 exp1) (define-values* (ids...) (exps...)))] [(_ () ()) (void)]))
Exercice 21 (define a 1) (define b 2) (define c 3) (define d 4) (define e 5) Définir rotate une macro qui modifie les liaisons de ses arguments de tel sorte que le premier prend la valeur du suivant... et le dernier prend la valeur du premier. Définissez swap pour vous aider. (rotate a b c d e) a 2 b 3 c 4 d 5 e 1
Exercice 22 (define-syntax swap (syntax-rules () [(_ x y) (let ([tmp x]) (set! x y) (set! y tmp))])) Définir rotate une macro qui modifie les liaisons de ses arguments de tel sorte que le premier prend la valeur du suivant... et le dernier prend la valeur du premier. Définissez swap pour vous aider. (define-syntax rotate (syntax-rules () [(rotate a) (void)] [(rotate a b c...) (begin (swap a b) (rotate b c...))]))
Exercice 23 (define memoize (λ (f) (let ([memo (make-hash)]) (λ args (cond [(hash-has-key? memo args) (hash-ref memo args #f)] [else (let ([result (apply f args)]) (hash-set! memo args result) result)]))))) Définir mλ une macro qui crée une fonction memoïzée à l’aide d’une liste de paramètres et des expressions formant le corps de la fonction. (define fib* (mλ (n) (cond [(= 0 n) 1] [(= 1 n) 1] [else (+ (fib* (- n 1)) (fib* (- n 2)))])))
Exercice 24 (define-syntax mλ (syntax-rules () [(_ (as...) exps...) (let ([memo (make-hash)] [fn (λ (as...) exps...)]) (λ args (cond [(hash-has-key? memo args) (hash-ref memo args)] [else (let ([val (apply fn args)]) (hash-set! memo args val) val)])))])) Définir mλ une macro qui crée une fonction memoïzée à l’aide d’une liste de paramètres et des expressions formant le corps de la fonction.
Exercice 25 (dotimes (j 1 5) (displayln j) (+ j 5)) Définir dotimes une macro qui prend dans une liste un identificateur i, une valeur initiale m et une valeur finale n et une suite d’expressions que nous appelons le corps et qui exécute ce corps pour des valeurs de i allant de m à n. La macro retourne la dernière exécution de ce corps et ne fait rien si m>n.
Exercice 26 (define-syntax dotimes (syntax-rules () [(dotimes (i m n) e0 e1...) (let loop ([i m]) (cond [(> i n) (void)] [(< i n) e0 e1... (loop (add1 i))] [else e0 e1...]))])) Définir dotimes une macro qui prend dans une liste un identificateur i, une valeur initiale m et une valeur finale n et une suite d’expressions que nous appelons le corps et qui exécute ce corps pour des valeurs de i allant de m à n. La macro retourne la dernière exécution de ce corps et ne fait rien si m>n.
Comment concevoir une macro 1.Écrivez le code que vous voulez que la macro construise. 2.Validez votre code. 3.Identifier les identificateurs et les sous-expressions qui varieront à chaque appel de macro. 4.Écrivez le code de la macro. 5.Utilisez le macro-stepper pour valider si votre macro produit bien le code que vous avez prévu. 6.Si vous avez des difficultés, essayez de diviser le problème en sous problèmes et reprenez les étapes 1..5 avec les sous- problèmes. 27