On 08/22/2017 05:29 PM, hiph...@openmailbox.org wrote:
Hello,

I am writing a Racket library which will make it possible to control the
Neovim text editor using Racket. People will be able to use Racket to
control Neovim, as well as write plugins for Neovim in Racket.
https://gitlab.com/HiPhish/neovim.rkt

So far it looks good, but I am stuck on macros. There is a hash table
which serves as a specification for which functions to generate, it is
stored in the file 'nvim/api/specification.rkt'. This table has the key
"functions" which contains a vector of more hash tables.

I need this information to generate function definitions for users to be
able to call Neovim procedures from Racket. So instead of writing
something like

   (api-call "nvim_command" '#("echo 'hello world'"))

users should be able to write

   (require nvim/api)
   (command "echo 'hello world'")

For this I need to loop over the functions vector like this:

   (define functions (hash-ref api-info "functions"))
   (for ([function (in-vector functions)])
     (define name   (hash-ref function       "name"))
     (defien params (hash-ref function "parameters"))
     `(define (,(string->symbol name) ,@params)
        (api-call ,name (vector ,@params))))

Reality is a bit more complicated because the parameters are not a list
but a vector of vectors, but that's besides the point. The question is:
how do I do this? A regular for-loop at runtime will not work, and
wrapping this in begin-for-syntax doesn't produce actual module-level
definitions.

I tried doing it with a macro:

   (define-syntax (api->func-def stx)
     (syntax-case stx ()
       [(_ api-name api-params)
        (with-syntax ([name
                        (datum->syntax stx
                          (string->symbol
                            (function-name (eval (syntax->datum #'api-name)))))]
                      [arg-list
                        (datum->syntax stx
                          (vector->list
                            (vector-map string->symbol (eval (syntax->datum 
#'api-params)))))])
          #`(define (name #,@#'arg-list)
              (api-call api-name (vector #,@#'arg-list))))]))

This *seems* to generate the proper definition, although I'm not really
sure. But how can I write the loop now so that it covers all function
definitions? I don't need to re-use the macro, I only need it once, so
if there was an easier solution like my for-loop above that does it in
place it would be even better.

Using `eval` is almost always the wrong (or at least suboptimal) solution unless the problem is "I need to evaluate some code supplied in text form by someone else".

You need the names of the functions you want to define at compile time, because Racket requires bindings to be settled at compile time. IIUC, you are doing argument/result checking and conversion at run time. So, assuming you want to keep the information in a single hash table in a single module, you need to require that module once for-syntax and once normally.

Here's an example solution to a simplified version of the problem. I've simplified the specification to just the function name and its argument types:

  ;; file "api-spec.rkt"
  #lang racket/base
  (provide functions)

  ;; A Type is (U 'String 'Number)

  ;; functions : (Listof (List Symbol (Listof Type)))
  ;; List of function names and types of arguments.
  (define functions
    '((f (String String))
      (g (Number String))))

And here's the module that uses that information to define adapter functions:

  ;; file "api.rkt"
  #lang racket/base
  (require (for-syntax racket/base))

  ;; Need api-spec at run time to create adapter
  (require "api-spec.rkt")

  ;; type->predicate : Type -> (Any -> Boolean)
  (define (type->predicate type)
    (case type
      [(String) string?]
      [(Number) number?]))

  ;; make-adapter : Symbol -> Function
  (define (make-adapter function-name)
    (define arg-types
      (cond [(assq function-name functions) => cadr]
            [else (error 'make-adapter "unknown: ~s" function-name)]))
    (define arg-preds (map type->predicate arg-types))
    (define (wrapper . args)
      (for ([arg (in-list args)]
            [pred (in-list arg-preds)]
            [type (in-list arg-types)])
        (unless (pred arg)
          (error function-name "expected ~a, got: ~e" type arg)))
      (printf "called ~s on ~v\n" function-name args))
    (procedure-reduce-arity wrapper (length arg-types)))

  ;; Need api-spec at compile time for names to bind
  (require (for-syntax "api-spec.rkt"))

  (define-syntax (define-the-functions stx)
    (syntax-case stx ()
      [(define-all-functions)
       (with-syntax ([(function-id ...)
                      (for/list ([entry (in-list functions)])
                        (syntax-local-introduce
                         (datum->syntax #'here (car entry))))])
         #'(begin (define function-id (make-adapter 'function-id)) ...
                  (provide function-id ...)))]))

  (define-the-functions)

In the code above, all of the adapter work is done by interpreting run-time data structures. The macro only "knows" the names of the functions to bind---which it must, because binding must be settled at compile time.

Leaving as much work as possible to run time is a good place to start, and it is often a fine place to stop, too. In case you're interested, though, here's how to move a bit more knowledge and work to compile time. In the following version, the macro knows not only the names of the function but also their arities and argument types---but it leaves the interpretation of the argument types until run time.

  ;; file "api.rkt" v2
  #lang racket/base
  (require (for-syntax racket/base))

  ;; type->predicate : Type -> (Any -> Boolean)
  (define (type->predicate type)
    (case type
      [(String) string?]
      [(Number) number?]))

  ;; make-check-arg : Symbol Type -> (Any -> Any)
  (define (make-check-arg who type)
    (define pred (type->predicate type))
    (lambda (v)
      (unless (pred v)
        (error who "expected ~s, got: ~e" type v))))

  ;; Need api-spec at compile time for names to bind
  ;; and also arity/type information.
  (require (for-syntax "api-spec.rkt" racket/list))

  (define-syntax (define-the-functions stx)
    (syntax-case stx ()
      [(define-all-functions)
       (with-syntax ([(function-id ...)
                      (for/list ([entry (in-list functions)])
                        (syntax-local-introduce
                         (datum->syntax #'here (car entry))))]
                     [((arg-type ...) ...)
                      (map cadr functions)])
         #'(begin (define-function function-id (arg-type ...)) ...
                  (provide function-id ...)))]))

  (define-syntax (define-function stx)
    (syntax-case stx ()
      [(define-function function-id (arg-type ...))
       (with-syntax ([(check ...) ;; identifiers for arg checkers
                      (generate-temporaries #'(arg-type ...))]
                     [(arg ...) ;; identifiers for argument names
                      (generate-temporaries #'(arg-type ...))])
         #'(define function-id
             (let ([check (make-check-arg 'function-id 'arg-type)] ...)
               (lambda (arg ...)
                 (check arg) ...
                 (printf "called ~s on ~v\n"
                         'function-id (list arg ...))))))]))

  (define-the-functions)

The lists in the old `make-adapter` have become ellipses. The run-time require of "api-spec.rkt" is gone. We still have some run-time support code, though: `make-check-arg` is what actually interprets types, with the help of `type->predicate`. If you wanted, you could move the interpretation of types from run time to compile time, and those run-time functions would disappear---but you would need some compile-time helper functions to do similar work.

Ryan

--
You received this message because you are subscribed to the Google Groups "Racket 
Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to racket-users+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to