Dear Paul, I think adding dynamic (fluid) binding to Arc
would make code smaller. Consider a debugger: (local (help backtrace up down print eval-in goto-frame)
(def debugger (stack env)
(with (run t framenum 0 framecnt (1- (len stack)))
(while run
(print)
(let it (read)
(if (is it 'q) (set run nil)
(is it 'u) (up)
(is it 'd) (down)
(is it 'f) (goto-frame (read))
(is it 'e) (eval-in (read))
(is it 'b) (backtrace)
(pr "Unknown command: " it))
(def print ()
(pr "\n" framenum ":\t" (car (nth framenum stack))))
(def backtrace ()
(pr " Backtrace:\n")
(let n -1
(each frame stack
(prn (++ n) ":\t" (car frame)))))
(def up ()
(if (is framenum framecnt) (prn "\tAt top")
(++ framenum)))
...
What seems intuitive to me is that stack, env,
framenum and framecnt should be dynamically
bound. print is called in a (dynamic)
environment where framenum is defined, so it
should just be able to reference it.Possible fix #1: create fluid-with macro (fluid-with (run t framenum 0 framecnt (1- (len stack)))
This was suggested in
http://arclanguage.org/item?id=4339, but to make
this thread-safe you have to ensure that only
one thread at a time can run debugger.It doesn't make the code longer, but it also
doesn't allow multiple threads to run debugger
concurrently I don't see how this solution could
work. Possible fix #2: thread-local variables Create a thread-local macro that is like "with"
but makes all the variables thread-local. The
bang syntax could be used to make referencing
easier, but it still makes the code longer. (thread-local (run t framenum 0 framecnt (1- (len stack)))
...
(def print ()
(pr "\n" framenum ":\t" (car (nth tl!framenum tl!stack))))
Assume (tl 'framenum) gets the thread-local
value for framenum. Acceptable, but it makes
the code bigger.Possible fix #3: just pass arguments to functions (print framenum stack)
...
(def print (framenum stack)
(pr "\n" framenum ":\t" (car (nth framenum stack))))
This seems straight-forward. Not bad for print,
but up has to be called as: (is it 'u) (= framenum (up framenum framecnt))
Acceptable, but makes the code even bigger.Possible fix #4: macros Could define 'up' as a macro (mac up ()
(if (is framenum framecnt) (prn "\tAt top")
(++ framenum)))
Now it has access to those variables. The only
downside is that now I have write the code
bottom up. (mac up ()
(if (is framenum framecnt) (prn "\tAt top")
(++ framenum)))
...
(def debugger ...
I guess using macros to deal with variable
scoping is elegant enough. My only beef with
this is that I think the code is less readable
bottom-up. If I'm reading the debugger code.
Don't I want to read it top down? I can't
understand what up does until I read debugger.
So I don't like this solution either because
"Programs must be written for people to read,"
right?I ran into this again wanting to write a sql
module that could be used something like this: (with-open conn* (sql-conn jdbc_url username password)
(with-open stmt* (sql-stmt)
(let rs (sql-select "BlahID from BlahTable")
(while (next rs)
...
))))
sql-stmt uses conn* and sql-select uses stmt.Now alternative #2 (thread-local variables) is
even less appealing because I need a special
version of with-open that calls thread-local
instead let. Alternative #3 isn't bad, but does make the code bigger: (with-open conn* (sql-conn jdbc_url username password)
(with-open stmt* (sql-stmt conn*)
(let rs (sql-select stmt* "BlahID from BlahTable")
(while (next rs)
...
))))
Dynamic binding seems so intuitive to me. Why
isn't it the default binding? Is this another
onion in the varnish? The logic would seem to
be that it makes code more robust to only have
lexical binding. But that's just another way of
saying it makes it harder to write bad programs.
And Arc isn't designed to keep people from
writing bad programs, right?Have I missed an elegant solution that
doesn't make the code bigger, harder to read
or not thread-safe? |