Skip to content

Instantly share code, notes, and snippets.

@tfeb
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tfeb/0b8531c94cf685824626 to your computer and use it in GitHub Desktop.
Save tfeb/0b8531c94cf685824626 to your computer and use it in GitHub Desktop.
Racket macro idiomaticity
#lang racket
(define (call/environment-variables thunk bindings)
;; This is a conventional call-with-whatever function: it calls thunk
;; with an environment augmented by bindings which is a list of pairs of
;; names and values
(let ([e (environment-variables-copy
(current-environment-variables))])
(for ([b bindings])
(match-let ([(list n v) b])
(environment-variables-set! e n v)))
(parameterize ([current-environment-variables e])
(thunk))))
;;; My original overcomplicated version
;;;
;(define-syntax (with-environment-variables stx)
; ;; This is the macro. The only significant thing here is that I want everything
; ;; to be evaluated: both names and values of variable (I am not sure why), so
; ;; (w-e-v ([n v] ...) ...) -> (call/ev (λ () ...) (list (list n v) ...))
; (syntax-case stx ()
; [(_ () body ...)
; #'(call/environment-variables (λ () body ...) '())]
; [(_ (binding ...) body ...)
; #`(call/environment-variables
; (λ () body ...)
; (list #,@(for/list ([b (syntax->list #'(binding ...))])
; (syntax-case b ()
; [(n v)
; #'(list n v)]))))]))
;;; bmastenbrook's pretty version
;;;
;(define-syntax with-environment-variables
; (syntax-rules ()
; [(_ ([name value] ...) body ...)
; (call/environment-variables
; (λ () body ...) (list (list name value) ...))]))
;;; My less-pretty but, I think, equivalent version
;;;
(define-syntax (with-environment-variables stx)
(syntax-case stx ()
[(_ ([name value] ...) body ...)
#'(call/environment-variables
(λ () body ...)
(list (list name value) ...))]))
(module+ test
(require rackunit)
(let ([n #"FOO"] [v #"BAR"] [n2 #"FISH"] [v2 #"BAT"])
(check-equal?
(call/environment-variables
(λ () (environment-variables-ref (current-environment-variables) n))
`([,n ,v]))
v)
(check-equal?
(with-environment-variables ([n v])
(environment-variables-ref (current-environment-variables) n))
v)
(check-equal?
(with-environment-variables ([n v] [n2 v2])
(let ([e (current-environment-variables)])
(map (λ (n) (environment-variables-ref e n)) `(,n ,n2))))
`(,v ,v2)))
(check-not-eq?
(with-environment-variables () (current-environment-variables))
(current-environment-variables)))
@tfeb
Copy link
Author

tfeb commented Jan 10, 2015

I'm a CL person, feeling my way around Racket's macro system. This works, but I'm wondering if

  • there is a more idiomatic way of doing something like this?
  • it actually is safe (hygenic I guess)?

@bmastenbrook
Copy link

Hello Tim! Nice to see you on the Racket side of the world.

What you're doing is hygienic; generally speaking, you have to write something involving datum->syntax to actually break hygiene. But it's unnecessarily procedural - what you've written can be expressed very simply just using patterns:

(define-syntax with-environment-variables
  (syntax-rules ()
    ((_ ([name value] ...) body ...)
     (call/environment-variables
      (lambda () body ...)
      (list (list name value) ...)))))

No base case is required either; a ... pattern implicitly means "zero or more". Generally speaking, complex macros are still best written procedurally (and syntax-parse is a very nice tool for doing so), but when it's just a trivial pattern like this, I tend to write it in syntax-rules.

@tfeb
Copy link
Author

tfeb commented Jan 12, 2015

Thank you! I had not understood two things I think: that ... is * not +, that it would work on compound expressions, and that the replacements could be hairy (ie it is smart enough to match ([name value] ...) to (list (list name value) ...)). Yes, that's three things: 'amongst our weapons are ...'.

The base case had also leaked in from an earlier macro, where I wanted the no-bindings case to transform into (begin ...) rather than calling a slightly-expensive function: I can't do that here since the environment is mutable so I need to make a new one anyway.

I've pushed a new version of the gist with your version and a (less good) version of mine which does the same matching I think: I presume that these are equivalent.

Anyway, thanks again: I've learned a thing.

@bmastenbrook
Copy link

No problem; sometimes an example is worth a thousand documents. Your version is definitely equivalent to mine; the use of quasisyntax (the #` form) is the same as syntax (#') because you don't have any unquote forms, and syntax-rules is always trivially convertible to syntax-case:

(require (for-syntax (rename-in racket [syntax-rules racket:syntax-rules])))

(begin-for-syntax
  (define-syntax syntax-rules
    (racket:syntax-rules ()
      [(_ literals (pattern expansion) ...)
       (lambda (s)
         (syntax-case s literals
           (pattern #'expansion) ...))])))

Have you seen Greg Hendershott's "Fear Of Macros"? It might help if you're interested in continuing down the Racket macrology path: http://www.greghendershott.com/fear-of-macros/

@tfeb
Copy link
Author

tfeb commented Jan 13, 2015

Thanks: it was meant to be #': the backquote got in by mistake.

I should look at "Fear Of Macros": I was aware of it but had (probably incorrectly) assumed it was aimed at people who aren't used to the idea of Lisp-family macros at all, rather than people trying to grok Racket's in particular.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment