Arc Forumnew | comments | leaders | submitlogin
A convenience layer over setforms
4 points by rocketnia 1981 days ago | 3 comments
In a recent thread[1], jsgrahamus was trying to program in a sort of pass-by-reference style.

Arc lets us define custom left-hand-side-of-assignment syntaxes, which should make it actually pretty easy to shorten this...

  (let (next-diagonals next-result next-total)
       (print-result diagonals result total)
    (= diagonals next-diagonals)
    (= result next-result)
    (= total next-total))
...into this:

  (= (list diagonals result total)
    (print-result diagonals result total))
Unfortunately, the process is pretty complicated. There are several examples of (defset ...) and (setforms ...) in arc.arc, but it's sort of a cumbersome and inconsistent interface. We have to pass around a list of three things:

- A list of variable bindings to establish first.

- A getter expression that can be evaluated under those bindings.

- Another expression that can be evaluated under those bindings, this time resulting in a setter function of one argument.

To begin with, managing these as three separate code snippets with a few variables in common is a recipe for hygiene problems, not to mention the intimidation factor of having multiple quasiquoted code snippets right next to each other. On top of that complexity, there are some slight quirks in the way these are actually used:

- Almost always, the bindings are put into an (atwiths ...). However, the (= ...) macro only uses (atwith ...), so it turns out we can't actually rely on later bindings being able to see earlier ones. Technically we can write code that's compatible with both contexts, but it might be easy to overlook one and mess up.

- For some reason, the setforms of a raw symbol, like (setforms 'foo), cause the variable to be looked up during the binding establishment process, even if the user's only trying to set it. This means we get errors when we're trying to set an unbound global variable. The (= ...) macro in arc.arc actually works around this by special-casing non-ssyntax symbols.

We could go hack at those quirks until they go away, but I prefer to think about it first in terms of the interface I'd actually like to use in the end. It's simple enough to write this interface as a convenience layer over the existing system.

Arc (and Racket) has a few built-in functions like (stdout ...) which act as getters if they're called with zero arguments and as setters if they're called with one argument. So I'll define (place ...) and (current ...) to convert back and forth between getter/setter expressions and getter/setter first-class functions.

As the centerpiece, (defplace ...) will replace (defset ...). Instead of a list of three code snippets, this just needs one expression that evaluates to a getter/setter function. This way I can think in terms of getter/setter functions all the time.

  (mac place (place)
  "Creates a getter/setter function based on a place syntax."
    (let (binds val setter)
         (case sym
           (list (list)
                 (w/uniq val
                   `(fn (,val)
                      (= ,place ,val))))
      (w/uniq arg
        `(with ,binds
           (fn ,arg
             (iflet (,arg) ,arg
               (,setter ,arg)
  (mac defplace (name args . body)
  "Defines a place syntax that expands like a macro into an expression
  that returns a getter/setter function."
    (w/uniq g-place
      `(defset ,name ,args
         (list (list ',g-place (do ,@body))
  (def current (p)
  "Gets the current value of a getter/setter function."
  (defplace current (place)
  "A place syntax that gets (or sets) the current value of a
  getter/setter function."
And here's a rough definition of (= (list ...) ...):

  (defplace list elems
    `(let places (list ,@(map [do `(place ,_)] elems))
       (fn args
         (iflet (val) args
           (each p places
             (= current.p pop.val))
           (map current places)))))
As mentioned above, this lets us write:

  (= (list diagonals result total)
    (print-result diagonals result total))
Using the same tools, it's now pretty easy to pass around "references" to local variables as long as we explicitly wrap and unwrap them using (place ...) and (current ...):

  (def square-by-reference (x)
    (zap [* _ _] current.x))
  (let x 10
    (square-by-reference place.x)

2 points by rocketnia 1981 days ago | link

Clickable links:



2 points by jsgrahamus 1980 days ago | link

This turned out to be a LOT more difficult than I imagined.

Thanks so much.


1 point by akkartik 1980 days ago | link

Well, it was also doable with existing defset, but rocketnia saw something a bit messy and decided to clean it up in the process :) "Done, and gets things smart." (