Arc Forumnew | comments | leaders | submitlogin
(coerce "foo" 'fn), programmable coercion, and ac-call
13 points by rntz 5636 days ago | 3 comments
Briefly: A hack (rntz.coerce.0 on anarki) which does the following: (coerce "foo" 'fn) gives you back a fn that does what "foo" does in functional position; this is internally used to handle non-fn things in functional position; and coercion is modifiable from arc, so you can modify this in arc. This allows you to define how to call objects of arbitrary types in arc; in particular, rntz.defcall.0 on anarki ports arc2-anarki's defcall to this backend.

It occurred to me that since I can use strings, tables, and lists as functions, I should be able to coerce them to 'fns. So I hacked this in:

    arc> (= foofn (coerce "foo" 'fn))
    #<procedure:...e/arc/arc/ac.scm:857:33>
    arc> (map foofn (range 0 2))
    (#\f #\o #\o)
However, in hacking this in, I though "wouldn't it be cool if coercion were modifiable from arc"? And then I realized that by combining these two ideas - coercion to 'fn, and modifiable coercion - I'd have a recipe for something from Anarki that I had really liked, namely, 'defcall - the ability to define how to call a given type of object. All I needed to do was ensure that calling a non-fn was equivalent to coercing it to a 'fn and calling that.

This idea in turn provides a really neat way to deal with the problem that 'ac-call faces: namely, that it defaults to using 'ar-apply, which conses up a list to apply the function (or string or table, etc) to, which causes a lot of time to be spent gc'ing these one-off lists. The solution currently used is to special-case all numbers of arguments up to four using functions 'ar-funcall{0..4}, and then fall back to 'ar-apply for five or more arguments. But if, instead of dealing with the whole thing in one chunk, we just compile the thing in functional position to (ar-coerce <foo> 'fn), we can do away with all of the consing.

If we do this, 'ac-call goes from being this monstrosity:

    (define (ac-call fn args env)
      (let ((macfn (ac-macro? fn)))
        (cond (macfn
               (ac-mac-call macfn args env))
              ((and (pair? fn) (eqv? (car fn) 'fn))
               `(,(ac fn env) ,@(ac-args (cadr fn) args env)))
              ((and direct-calls (symbol? fn) (not (lex? fn env)) (bound? fn)
                    (procedure? (namespace-variable-value (ac-global-name fn))))
               (ac-global-call fn args env))
              ((= (length args) 0)
               `(ar-funcall0 ,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 1)
               `(ar-funcall1 ,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 2)
               `(ar-funcall2 ,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 3)
               `(ar-funcall3 ,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
              ((= (length args) 4)
               `(ar-funcall4 ,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
              (#t
               `(ar-apply ,(ac fn env)
                          (list ,@(map (lambda (x) (ac x env)) args)))))))
To this:

    (define (ac-call fn args env)
      (let ((macfn (ac-macro? fn)))
        (cond (macfn
               (ac-mac-call macfn args env))
              ((and (pair? fn) (eqv? (car fn) 'fn))
               `(,(ac fn env) ,@(ac-args (cadr fn) args env)))
              ((and direct-calls (symbol? fn) (not (lex? fn env)) (bound? fn)
                    (procedure? (namespace-variable-value (ac-global-name fn))))
               (ac-global-call fn args env))
              (#t
               `((ar-coerce ,(ac fn env) 'fn)
                 ,@(map (lambda (x) (ac x env)) args))))))
Moreover, we get to eliminate 'ar-funcall{0..4}. So not only do we get the ability to coerce things to 'fns, programmable coercion, and eliminate argument consing; we actually reduce our codebase!

The programmable coercion is done by way of making coercion based on a double hashtable lookup. To (coerce 'foo 'string), we first look up 'string in coerce* , and then look up 'sym (the type of 'foo) in the resulting table, and call the procedure we get on 'foo (and any other args passed to coerce, as in (coerce "f" 'int 16) to interpret "f" as a hexadecimal integer). So to modify coercion from arc, you just add conversion functions to the appropriate spots in coerce* . This makes 'defcall easy:

    (def set-coercer (to from fun)
      (let cnv (or coerce*.to (= coerce*.to (table)))
        (= cnv.from fun)))
    
    (mac defcoerce (to from parms . body)
      `(set-coercer ',to ',from (fn ,parms ,@body)))
    
    (mac defcall (type-name parms . body)
      (w/uniq (fnobj args)
        `(defcoerce fn ,type-name (,fnobj)
           (fn ,args (let ,parms (cons (rep ,fnobj) ,args) ,@body)))))
As mentioned at the top, to grab this hack, grab rntz.coerce.0 from anarki; and to grab 'set-coercer, 'defcoerce, and 'defcall, grab rntz.defcall.0. Both have been merged into arc3.master.


1 point by shader 5635 days ago | link

Absolutely brilliant!

I had thought of making programmable coercion before, but never realized the connection between functional position and coercing the object to a fn.

Great job. I think it should be added to the master version, since you still have defcall and in theory nothings "changed" except an increase in performance and conceptual simplicity.

-----

1 point by rntz 5635 days ago | link

I have actually merged both rntz.coerce.0 and rntz.defcall.0 into arc3.master; I forgot to mention that in my original post. Unless you were asking about merging this into the arc2-based master branch, which I haven't done - I didn't really think about it; I've switched over entirely to arc3 at this point.

-----

1 point by shader 5635 days ago | link

Right, no point focusing much on "arc2 master" anymore.

How long before we move the old master to a separate branch and make arc3.master the new master branch?

-----