Re: Generate function defintions at compile time

Ryan Culpepper <[email protected]>
Newsgroups gmane.comp.lang.racket.user,gmane.lisp.scheme.plt
Message-ID <[email protected]>
On 08/22/2017 05:29 PM, [email protected] 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+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/[email protected]
For more options, visit https://groups.google.com/d/optout.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.