Создание ракетного макроса, который генерирует лямбду - PullRequest
1 голос
/ 29 апреля 2019

Я пытаюсь создать небольшую оболочку для выполнения sql-подобных запросов к файлам csv (из любопытства и как попытка изучить Racket). Для этого я хотел реализовать макрос выбора с такой грубой структурой (где я планировал, чтобы x был столбцами БД, но пока что передал только одну строку):

(define-syntax select
  (syntax-rules (* from where)
    ((_ col1 ... from db where condition)
     (filter (lambda (x) condition) <minutiae>))))

(где детали - это файловый IO и код канала)

Область действия x не та, о которой я думал:

x: undefined;
 cannot reference an identifier before its definition

Я нашел этот пример макросов, похожих на буквы :

(define-syntax my-let*
  (syntax-rules ()
    ((_ ((binding expression) ...) body ...)
     (let ()
       (define binding expression) ...
       body ...))))

И затем я попытался просто произвести лямбду следующим образом:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ body)
     (lambda (x)
       body))))

А затем пытаемся имитировать структуру приведенного примера:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ body)
     (lambda (x_)
       (let ()
         (define x x_)
         body)))))

Оба они дали мне ту же ошибку при вызове ((my-lambda (+ x 1)) 0):

x: undefined;
 cannot reference an identifier before its definition

Согласно моим прочтениям, это из-за гигиены, но я не могу понять это достаточно хорошо, чтобы решить это самостоятельно. Что я делаю не так и как можно определить такие макросы? Почему работает пример let, а не лямбда?

Ответы [ 2 ]

3 голосов
/ 29 апреля 2019

Как вы уже догадались, проблема касается гигиены.

Пример let работает, потому что идентификаторы, используемые внутри данного тела, передаются макросу.

Но если вы попытаетесь определитьx идентификатор внутри тела, без того, чтобы человек, пишущий тело, на самом деле точно знал об этом , тогда вы нарушаете гигиену (вставляя произвольную привязку в область действия).

Чтокоторый вы хотите создать, называется макросом anaphoric .К счастью, у Racket есть то, что вам нужно для создания.

Синтаксический параметр

Если вы когда-либо использовали параметры Racket раньше, он работает немного так же, но для макросов.

(define-syntax-parameter <x>
  (lambda (stx)
    (raise-syntax-error '<x> "Used outside select macro." stx)))

Это определит параметр с именем <x>, который пользователи макроса смогут использовать внутри макроса select.Чтобы предотвратить его использование снаружи, по умолчанию этот параметр настроен на выдачу синтаксической ошибки.

Чтобы определить единственное место, в котором его можно использовать, вы вызываете syntax-parameterize:

(define-syntax select
  (syntax-rules (* from where)
    [(_ col1 ... from db where condition)
     (findf
       (lambda (x)
         (syntax-parameterize ([<x> (make-rename-transformer #'x)])
           condition))
       <minutiae>)]))

Это создаст новую область видимости вокруг condition, в которой <x> привязан к x в лямбде.

Затем вы можете вызвать свой макрос следующим образом:

(select * from db where (eq? <x> 'foo))

Если вы попытаетесь использовать <x> вне макроса, вы получите синтаксическую ошибку:

> (displayln <x>)
<x>: Used outside select macro.
  in: <x>

Полный код

#lang racket/base

(require
  (for-syntax racket/base)
  racket/stxparam)

(define-syntax-parameter <x>
  (lambda (stx)
    (raise-syntax-error '<x> "Used outside select macro." stx)))

(define-syntax select
  (syntax-rules (* from where)
    [(_ col1 ... from db where condition)
     (findf
       (lambda (x)
         (syntax-parameterize ([<x> (make-rename-transformer #'x)])
           condition))
       db)]))

(module+ test
  (require rackunit)

  (define db '(foo bar baz))

  (check-equal? (select * from db where (eq? <x> 'foo)) 'foo)
  (check-equal? (select * from db where (eq? <x> 'bar)) 'bar)
  (check-equal? (select * from db where (eq? <x> 'boop)) #f))
0 голосов
/ 29 апреля 2019

Это работает:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ x body)
     (lambda (x) body))))

(my-lambda x (+ x 1))

пока это не работает:

(define-syntax my-lambda*
  (syntax-rules ()
    ((_ body)
     (lambda (x) body))))

(my-lambda* (+ x 1))

Одна вещь, которая может помочь понять разницу, состоит в том, что Racket может переименовывать переменные, однако, он хочет, пока переименование является последовательным (на самом деле довольно сложно определить слово «согласованный» здесь, но интуитивно это должно иметь смысл для вас). Ракетка должна быть в состоянии переименовать, чтобы сохранить гигиену.

В первом случае вы звоните (my-lambda x (+ x 1)). Ракетка может переименовать его в (my-lambda x$0 (+ x$0 1)). Расширяя макрос, мы получаем (lambda (x$0) (+ x$0 1)), который действителен.

Во втором случае вы звоните (my-lambda* (+ x 1)). Ракетка может переименовать его в (my-lambda* (+ x$0 1)). Расширяя макрос, мы получаем (lambda (x) (+ x$0 1)), поэтому x$0 не связан.

(Обратите внимание, что я сильно упрощаю вещи. На самом деле Racket необходимо развернуть макрос в первом случае, чтобы знать, что ему нужно переименовать оба x одновременно.)

Чтобы получить то, что вы хотите, вам нужно нарушить гигиену. Вот один простой способ сделать это:

(require syntax/parse/define)

(define-simple-macro (my-lambda** body)
  #:with unhygiene-x (datum->syntax this-syntax 'x)
  (lambda (unhygiene-x) body))

(my-lambda** (+ x 1))

Также см. https://stackoverflow.com/a/55899542/718349 и комментарий ниже для альтернативного способа нарушения гигиены.

...