wdkrnls
wdkrnls

Reputation: 4702

Racket Macro to auto-define functions given a list

I want to auto-generate a bunch of test functions from a list. The advantage being I can change the list (e.g. by reading in a CSV data table) and the program will auto-generate different tests on the next program execution.

For example, say I am trying to identify oxyanions in a string containing a chemical formula.

My list may be something like:

(define *oxyanion-tests*
  ;           name         cation
  (list (list "aluminate"  "Al")
        (list "borate"     "B")
        (list "gallate"    "Ga")
        (list "germanate"  "Ge")
        (list "phosphate"  "P")
        (list "sulfate"    "S")
        (list "silicate"   "Si")
        (list "titanate"   "Ti")
        (list "vanadate"   "V")
        (list "stannate"   "Sn")
        (list "carbonate"  "C")
        (list "molybdate"  "Mo")
        (list "tungstate"  "W")))

I'm reasonably confident that the chemical formula contains one of these oxyanions if there is a cation followed by an oxygen within parentheses (e.g. "(C O3)" ), or if the cation is followed by 2 or more oxygens (e.g. "C O3"). Note that this isn't perfect, since it will miss hypochlorite anions (e.g. "Cl O"), but it's good enough for my application.

(define ((*ate? elem) s-formula)
  (or (regexp-match? (regexp (string-append "\\(" elem "[0-9.]* O[0-9.]*\\)")) s-formula)
      (regexp-match? (regexp (string-append "(^| )" elem "[0-9.]* O[2-9][0-9.]*")) s-formula)))

I think I need a macro to do this, but I don't really understand how they work from reading the documentation. I'm asking here so that I have a good example to look at that is immediately useful to me.

Here is what I kind of think the macro should look like, but it doesn't work and I don't really have a mental model for figuring out how to fix it.

(require (for-syntax racket))
(define-syntax-rule (define-all/ate? oxyanion-tests)
  (for ([test oxyanion-tests])
    (match test
      [(list name cation) (syntax->datum (syntax (define ((string->symbol (string-append name "?")) s-formula)
                                    ((*ate? cation) s-formula))))])))

Thanks for any guidance you can give me!


P.S. Here are a few tests that should pass:

(define-all/ate? *oxyanion-tests*)
(module+ test
  (require rackunit)
  (check-true (borate? "B O3"))
  (check-true (carbonate? "C O3"))
  (check-true (silicate? "Si O4")))

Upvotes: 2

Views: 499

Answers (1)

stchang
stchang

Reputation: 2550

I see a couple of errors in your code:

  1. Your *oxyanion-tests* is a runtime value, but you need its values to use as function name identifiers, so it must be available at compile time.
  2. The syntax around the result of syntax-rules is implicit. So with syntax-rules, you only get the macro template language (see the docs for syntax for more info). Thus you can't do the datum->syntax that you are trying to do. You have to use syntax-case instead, which allows you to use all of Racket to compute the syntax objects you want.

Here's what I came up with:

#lang racket
(require (for-syntax racket/syntax)) ; for format-id

(define-for-syntax *oxyanion-tests*
  ;           name         cation
  (list (list "aluminate"  "Al")
        (list "borate"     "B")
        (list "gallate"    "Ga")
        (list "germanate"  "Ge")
        (list "phosphate"  "P")
        (list "sulfate"    "S")
        (list "silicate"   "Si")
        (list "titanate"   "Ti")
        (list "vanadate"   "V")
        (list "stannate"   "Sn")
        (list "carbonate"  "C")
        (list "molybdate"  "Mo")
        (list "tungstate"  "W")))

(define ((*ate? elem) s-formula)
  (or (regexp-match? 
       (regexp (string-append "\\(" elem "[0-9.]* O[0-9.]*\\)")) 
       s-formula)
      (regexp-match?
       (regexp (string-append "(^| )" elem "[0-9.]* O[2-9][0-9.]*")) 
       s-formula)))

(define-syntax (define-all/ate? stx)
  (syntax-case stx ()
    [(_)
     (let ([elem->fn-id 
            (λ (elem-str)
              (format-id 
               stx "~a?" 
               (datum->syntax stx (string->symbol elem-str))))])
       (with-syntax 
         ([((ate? cation) ...)
           (map 
            (λ (elem+cation)
              (define elem (car elem+cation))
              (define cation (cadr elem+cation))
              (list (elem->fn-id elem) cation))
            *oxyanion-tests*)])
         #`(begin
             (define (ate? sform) ((*ate? cation) sform))
             ...)))]))

(define-all/ate?)
(module+ test
  (require rackunit)
  (check-true (borate? "B O3"))
  (check-true (carbonate? "C O3"))
  (check-true (silicate? "Si O4")))

The key is the elem->fn-id function, which turns a string into a function identifier. It uses datum->syntax with stx as the context, meaning the defined function will be available in the context where the macro is invoked.

Upvotes: 2

Related Questions