Hi Kevin, This is interesting! A number of people have wanted conveniences around `keyword-apply` and accepting the same keywords as some other function. (The trouble is, different people have different ideas of what those conveniences should be.)
To start with, I had a few small suggestions about the code you sent: - `keyword-apply/filter` doesn't need to be a macro, and your macro should use `fn` rather than `h`; - the second result of `procedure-keywords` can be `#f` if the given procedure accepts all keywords; - `for/lists` might be more convenient than `for/fold`; - I think some of the cases in `filter/kw` get the filtering backwards; - keywords can be `quote`-ed as values, so you don't need so many symbol-to-string-to-keyword conversions; - using `set!` this way will cause lots of problems; - there's no need for `list->vector` in `get-kw-val`; and - in `def`, it might be better to use syntax parameters than to break hygiene. You may know this, but all keyword-accepting functions in Racket are effectively implemented with `make-keyword-procedure`: the variants of `lambda` and `define` that use `kw-formals` are macros that expand to more primitive versions that don't know about keywords. The macros also do some optimization where possible. For inspiration, you might be interested in the implementation in https://github.com/racket/racket/blob/master/racket/collects/racket/private/kw.rkt For fun, I wrote a version that illustrates some of these suggestions and and tries to do more work at compile-time. Be warned that it is not thoroughly tested! I'm pasting it below, and I've also put it up as a Gist at https://gist.github.com/LiberalArtist/292b6e99421bc76315110a59c0ce2b0d -Philip #lang racket ;; License: Apache-2 (provide kw-pass-through-lambda local-keyword-apply local-kw-lst local-kw-val-lst (contract-out [keyword-apply/filter (-> procedure? (listof keyword?) list? list? any)])) (module+ test (require rackunit) (define (h #:c c . x) (list c x)) (define g (kw-pass-through-lambda (#:c [c 0] . args) (list c (local-keyword-apply h args)))) (define f (kw-pass-through-lambda (n p #:a [a 0] #:b [b 0] . ns) (list local-kw-lst local-kw-val-lst a b n p ns (local-keyword-apply g ns)))) (check-equal? (f 2 3 4 5 #:a 42 #:c 52) '((#:a #:c) (42 52) 42 0 2 3 (4 5) (52 (52 (4 5))))) ;; My implementation of "filtering" keywords has a different result, ;; but maybe I don't understand what you were trying to do. ;; Your version did this: ;; (check-exn #rx"procedure: h\n given keyword: #:z" ;; (λ () (f 2 3 4 5 #:z 42 #:c 52))) ;; Mine does this instead: (check-equal? (f 2 3 4 5 #:z 42 #:c 52) '((#:c #:z) (52 42) 0 0 2 3 (4 5) (52 (52 (4 5)))))) ;; potential further extensions: ;; - make keyword-apply/filter and local-keyword-apply ;; accept extra keyword and by-position args like keyword-apply ;; - implement a define version of kw-pass-through-lambda ;; - various performance optimizations (require syntax/parse/define racket/stxparam (for-syntax syntax/parse/lib/function-header racket/list racket/match racket/sequence syntax/transformer)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; runtime support (define (keyword-apply/filter proc kw-lst kw-val-lst by-pos-args) ;; like keyword-apply, but skips keywords that aren't allowed (define-values [required-kws allowed-kws] (procedure-keywords proc)) (match allowed-kws [#f ;; accepts all keywords (keyword-apply proc kw-lst kw-val-lst by-pos-args)] ['() ;; accepts no keywords (apply proc by-pos-args)] [_ (for/lists [kw-lst* kw-val-lst* #:result (keyword-apply proc kw-lst* kw-val-lst* by-pos-args)] ([kw (in-list kw-lst)] [val (in-list kw-val-lst)] #:when (memq kw allowed-kws)) (values kw val))])) (define (kw-arg-ref kw kw-lst kw-val-lst [fail-thunk ;; we'll use procedure-reduce-keyword-arity ;; to avoid getting here when required kws are missing (λ () (error 'kw-arg-ref "shouldn't get here"))]) (or (for/first ([this-kw (in-list kw-lst)] [val (in-list kw-val-lst)] #:break (keyword<? kw this-kw) #:when (eq? kw this-kw)) val) (fail-thunk))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; syntax layer (define-for-syntax (stxparam-uninitialized stx) (raise-syntax-error #f "only allowed inside kw-pass-through-lambda" stx)) (define-syntax-parameter local-kw-lst stxparam-uninitialized) (define-syntax-parameter local-kw-val-lst stxparam-uninitialized) (define-syntax-parameter local-keyword-apply stxparam-uninitialized) (define-simple-macro (lambda/name kw-formals #:name name:id body ...+) ;; a simple helper (w/ minimal cheking) ;; to give a function a good inferred name (let ([name (λ kw-formals body ...)]) name)) (define-for-syntax (check-required-not-after-optional names kws defaults) ;; required by-position arguments must come before ;; optional by-position arguments: ;; if any don't, return the first offending identifier (let*-values ([{names defaults} ;; ignore kw args (for/lists [names defaults] ([n (in-list names)] [d (in-list defaults)] [kw (in-list kws)] #:unless kw) (values n d))] [{names defaults} ;; drop leading required args (let loop ([names names] [defaults defaults]) (match defaults [(cons #f defaults) (loop (cdr names) defaults)] [_ (values names defaults)]))]) (for/first ([n (in-list names)] [d (in-list defaults)] #:unless d) n))) (define-syntax-parser kw-pass-through-lambda [(_ (arg:formal ... . (~or* rest-arg-name:id ())) body ...+) #:fail-when (check-duplicate-identifier (syntax->list #'(arg.name ... (~? rest-arg-name)))) "duplicate argument name" #:fail-when (check-duplicates (syntax->list #'((~? arg.kw) ...)) #:key syntax-e eq?) "duplicate keyword for argument" #:fail-when (check-required-not-after-optional (attribute arg.name) (attribute arg.kw) (attribute arg.default)) "default-value expression missing" ;; the error message λ gives ;; sort formals #:with ([by-pos-name:id (~optional by-pos-default:expr)] ...) (for/list ([stx (in-syntax #'([arg.name (~? arg.default)] ...))] [kw? (in-list (attribute arg.kw))] #:unless kw?) stx) #:with ((~alt [opt-kw:keyword opt-kw-name:id opt-kw-default:expr] [reqired-kw:keyword reqired-kw-name:id]) ...) #'((~? [arg.kw arg.name (~? arg.default)]) ...) #:with (by-pos-formal ...) #'((~? [by-pos-name by-pos-default] by-pos-name) ...) #:with inferred-name:id (or (syntax-local-name) #'kw-pass-through-procedure) #:with (core-arg-name:id ...) #'(by-pos-name ... reqired-kw-name ... opt-kw-name ... (~? rest-arg-name)) #'(let* ([core ;; w/ only required args (lambda/name (kw-lst kw-val-lst core-arg-name ...) #:name inferred-name (define (the-local-keyword-apply proc by-pos-args) (keyword-apply/filter proc kw-lst kw-val-lst by-pos-args)) (syntax-parameterize ([local-kw-lst (make-variable-like-transformer #'kw-lst)] [local-kw-val-lst (make-variable-like-transformer #'kw-val-lst)] [local-keyword-apply (make-variable-like-transformer #'the-local-keyword-apply)]) body ...))] [explicit-kws-proc ;; version that handles finding kw arg values and calls core ;; all by-pos args must be present (lambda/name (kw-lst kw-val-lst by-pos-name ... (~? rest-arg-name)) #:name inferred-name (let ([reqired-kw-name (kw-arg-ref 'reqired-kw kw-lst kw-val-lst)] ...) (let* ([opt-kw-name (kw-arg-ref 'opt-kw kw-lst kw-val-lst (λ () opt-kw-default))] ...) (core kw-lst kw-val-lst core-arg-name ...))))] [implicit-kw-proc ;; let λ handle optional by-position arguments and arity (make-keyword-procedure (lambda/name (kw-lst kw-val-lst by-pos-formal ... . (~? rest-arg-name ())) #:name inferred-name (explicit-kws-proc kw-lst kw-val-lst by-pos-name ... (~? rest-arg-name))) (lambda/name (by-pos-formal ... . (~? rest-arg-name ())) #:name inferred-name (explicit-kws-proc '() '() by-pos-name ... (~? rest-arg-name))))]) (procedure-reduce-keyword-arity-mask implicit-kw-proc ;; optimization: compute arity mask statically (procedure-arity-mask implicit-kw-proc) '(reqired-kw ...) ;; accept all keywords #f))]) On Thu, Aug 29, 2019 at 4:24 PM Kevin Forchione <lyss...@gmail.com> wrote: > Hi guys, > I’ve been working for a little while with the idea of being able to pass > keyword arguments through a function that doesn’t define them. Additionally > I wanted to allow the “pass-through” function to define its own keywords. > Additionally didn’t want to have to pre-specify what function might be on > the receiving end of the call. But finally, if both pass-through and called > functions define the same keywords I didn’t want to have to differentiate > between them. > > - This seems to involve some combination of make-keyword-procedure and > keyword-apply. > - Since make-keyword-procedure expects a “vanilla” function (one without > keywords specified) I decided to define a macro that would wrap the > function in a let that with default bindings for each keyword defined by > the pass-through. Inside the function I would then assign any values > provided by the function call to those variables. > - Additionally I would build a parameterized list of keywords defined by > the pass-through chain. These would be used in conjunction with the keyword > list produced by procedure-keywords and the keywords/values captured by the > function call to “filter” the lists used by keyword-apply. The idea being > to eliminate any keyword/values supplied to the pass-through and defined by > the pass-through that were not defined by the called function. This would > allow keywords not defined by either to be error by the called function. > > As you can see, it’s a convoluted approach and I’m not sure how robust it > actually. I’m presenting working code (for my test cases…) but also > wondering if someone hasn’t already crated that wheel. :) > > #lang racket > > (require (for-syntax syntax/parse > racket/syntax)) > > (define current-caller-kw (make-parameter '())) > > (define (get-kw-val w v kw kv) > (define key (string->keyword (symbol->string w))) > (define kws (list->vector kw)) > (define idx (vector-member key kws)) > (cond > [(false? idx) v] > [else (define kvs (list->vector kv)) > (vector-ref kvs idx)])) > > (define-syntax (def stx) > (syntax-parse stx > [(_ (f ((w v) ...) k ... . ks) body0 body ...) > (with-syntax ([kw (format-id #'f "kw")] > [kv (format-id #'f "kv")]) > #'(define f > (let ([w v] ...) > (make-keyword-procedure > (λ (kw kv k ... . ks) > (parameterize ([current-caller-kw > (append (current-caller-kw) > (map (λ (x) (string->keyword > (symbol->string x))) > (list 'w ...)))]) > (set! w (get-kw-val 'w w kw kv)) ... > body0 body ...))))))])) > > (define (filter/kw ckw fkw kw kv) > (cond > [(empty? ckw) (values kw kv)] > [(empty? (remove* fkw ckw)) (values kw kv)] > [else > (define diff (remove* fkw ckw)) > (define vkw (list->vector kw)) > (define vkv (list->vector kv)) > (for/fold ([wacc '()] [vacc '()]) > ([v kv] > [k kw] #:unless (member k diff)) > (values (append wacc (list k)) (append vacc (list v))))])) > > (define (h #:c c . x) (list c x)) > > (def (g ((c 0)) . args) > (define-values (rkw akw) (procedure-keywords h)) > (define-values (Δkw Δkv) (filter/kw (current-caller-kw) akw kw kv)) > (list c (keyword-apply h > Δkw > Δkv > args))) > (def (f ((a 0)(b 0)) n p . ns) (list kw kv a b n p ns (keyword-apply g > kw > kv > ns))) > > > ;=> '((#:a #:c) (42 52) 42 0 2 3 (4 5) (52 (52 (4 5)))) > (f 2 3 4 5 #:a 42 #:c 52) > ;=> application: procedure does not expect an argument with given keyword > ; procedure: h > ; given keyword: #:z > ; arguments...: > (f 2 3 4 5 #:z 42 #:c 52) > > Kevin > > -- > 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. > To view this discussion on the web visit > https://groups.google.com/d/msgid/racket-users/27D5D96D-F9D9-4ECB-9AE0-92FD1EB065C8%40gmail.com > . > -- 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. To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAH3z3gY-35%3Dxi3okpHrNpKKzsnf1-WMuGsg0Wzn3D5BPgso-Bw%40mail.gmail.com.