schemerackettyped-racket

Typed Racket - dynamic function calls (string to procedure) revisited


About a year ago, @soegaard provided an answer to an interesting problem - how to take a string and return the procedure named in that string. The solution was simple and elegant.

Enter typed racket and a twist.

I can make it work in typed racket as long as it returns only functions with the same arity, for example (-> Number Number Number), but if I try to have it able to return functions with different arities, such as shown below, I cannot figure out how to make the require/typed call work.

Here is the modified file with my second function with a different arity.

#lang racket
(provide string->procedure add square)

(define (add x y)
  (+ x y))

(define (square x)
  (sqr x))

(define ns (variable-reference->namespace (#%variable-reference)))

(define (string->procedure s)
  (define sym (string->symbol s))
  (eval sym ns))

(string->procedure "add")

((string->procedure "add") 1 2)

((string->procedure "square") 5)

And here is the call that only works with the "add" fuction or any other function that takes two numbers and returns one number.

#lang typed/racket

(require/typed "string-procedure.rkt"
               [string->procedure
                (-> String (-> Number Number Number))]
               [add (-> Number Number Number)]
               [square (-> Number Number)])

I've tried using case-> and unions to no avail. Using case-> for the return type at least will run but then it fails all calls.

In case you think I'm nuts for trying this, what I'm trying to do is take the result of a database call, a string, and determine the correct procedure to call to access the appropriate data element in a struct. I can do it with a long case statement, but I was hoping for a more elegant solution.

Thank you.


Solution

  • I don't think you want to use eval, or to solve this problem in quite this way. Specifically: what if the database contains a string that refers to a function that doesn't exist, or a function that you didn't want to have called? This is how security problems arise.

    I would say that in this case, you'd probably be willing to specify the names of the procedures that are "legal", and you can probably do that easily with a macro that doesn't mangle hygiene too badly:

    #lang typed/racket
    
    ;; defines the 'db-callable' syntax. Put this in a library file...
    (define-syntax (db-callable stx)
      (syntax-case stx ()
        [(_ fun-name [id ...])
         (with-syntax ([(id-strs ...)
                        (for/list ([id-stx (in-list (syntax->list #'(id ...)))])
                          (symbol->string (syntax-e id-stx)))])
           #'(define (fun-name str)
               (match str
                 [id-str id] ...)))]))
    
    ;; here are some functions we want the DB to be able to call
    (define (f x) 3)
    (define (g x) 4)
    
    ;; here's the list of functions we want the db to be able to call:
    (db-callable getfun [f g])
    
    ((getfun "f") 9)
    ((getfun "g") 123)