Hi.. long post.. it's a Request For Comment :) Clojure's "thread local binding facility" for Vars has always seemed like a useful (but of course misusable) feature to have available in our toolbox.. However it soon becomes apparent that Var bindings don't play nice with laziness - and since laziness can creep in all over the place (eg. using standard sequence functions, direct use of (lazy-seq ..), using (delay ..), perhaps from any (fn ..) you create) that renders them of much less value.. almost too unsafe to tangle with in my opinion. To quote Rich, "there is a fundamental tension between laziness and dynamic scope, combine with extreme caution."
The underlying problem is that when each (fn ..) gets created it doesn't capture the current dynamic environment so that it can subsequently be made available when it is eventually invoked. To quote Rich once more, "The overhead for capturing the dynamic context for every lazy seq op would be extreme, and would effectively render dynamics non-dynamic." To help alleviate the problem somewhat, a (bound-fn ..) helper macro has been created (https://www.assembla.com/spaces/clojure/tickets/170) but my guess is that its use would be impractical/ugly/risky.. it would need to be used "all over the place" and forgetting to use it in any of those places could introduce a bug. I've been thinking about an alternative to (bound-fn ..) and would like your opinions on the following tweak to Clojure: * Designate one "Var" (say clojure.core/*env*) as a special "environment" Var. It would either be bound to nil or something-non-nil (normally a map). * Modify clojure's implementation behind (fn ..) to do a light-weight version of what (bound-fn ..) does -- ie.on instantiation, capture the current value of *env*, and when invoked, wrap the execution in a bind/unbind of *env* with the captured value, but only if non-nil. * Create a (with-env {...} ...) helper macro. * Developers, just need to make sure not to wrap (with-env ..) around code that /loads/ their software (only around code that /runs/ their software). As a proof-of-concept, I implemented this the most simple, hackish way, but it seems to work quite well. The main details follow: -In RT.java add a public static 'ENV' field (similar to IN, OUT, etc) associated to clojure.core/*env* with root binding of nil -In both RestFn.java and AFn.java: add a new private 'env' field. set 'env' to the deref of RT.ENV in the constructor. rename *all* invoke() methods to invoke0(). add corresponding new invoke() methods for each invoke0() in order to intercept execution. Example: -public Object invoke(Object arg1, Object arg2, Object arg3) throws Exception{ - return throwArity(); -} --- +public Object invoke0(Object arg1, Object arg2, Object arg3) throws Exception{ + return throwArity(); +} +public Object invoke(Object arg1, Object arg2, Object arg3) throws Exception{ + try { + if (env != null) + <...something to push 'env' value onto RT.ENV...>; + return invoke0(arg1,arg2,arg3); + } + finally { + if (env != null) + <...something to pop 'env' value off RT.ENV...>; + } +} -In Compiler.java: make the following change so that (fn ..) objects override invoke0() instead of invoke(). - Method m = new Method(isVariadic() ? "doInvoke" : "invoke", + Method m = new Method(isVariadic() ? "doInvoke" : "invoke0", -------Example of it working------- user=> (def *other* {:addval 1}) #'user/*other* user=> (map #(+ % (:addval *other*)) [1 3 5 7 9]) (2 4 6 8 10) ;<---AS EXPECTED. user=> (binding [*other* {:addval 10}] (map #(+ % (:addval *other*)) [1 3 5 7 9])) (2 4 6 8 10) ;<---OOPS. BINDING DISAPPEARED. user=> *env* nil ;<---DEFAULTS TO nil user=> (with-env {:addval 10} (map #(+ % (:addval *env*)) [1 3 5 7 9])) (11 13 15 17 19) ;<---GREAT. BINDING WAS REMEMBERED. ------------------------------------- Now what about the overhead? Based on a little initial testing... when using a regular Var to implement our special *env*, when *env* is not utilized (ie.left bound to nil) the overhead appears to be negligible, but when bound it is quite significant.. Consuming this 30-million entry lazy list: (time (last (map identity (range 30000000)))) with *env* unbound = ~18 sec with *env* bound = ~65 sec However, if we choose to create clojure.core/*env* not referring to a Var but something else (unfortunately Clojure is extremely inextensible in this regard) -- we can instead invent and use a "lighter weight Var" because we something simple is adequate. I experimented by creating a VarLite class. It extends Var (had to change Var to be non-final) and manages the pushing/popping of its value with a simple stack (just for itself) with a bit of caching, and dispenses with Validators, etc. This reduces the overhead dramatically: (time (last (map identity (range 30000000)))) with *env* unbound = ~18 sec with *env* bound = ~22 sec With a better integrated, better designed implementation, I'm certain this could be improved further. In that case, would this be a worthwhile enhancement to Clojure? Seems like it could be a win-win situation, since it rescues (semi-)dynamic bindings from the gnashing jaws of laziness for those that want to use it, but shouldn't impact negatively upon those that don't? Or is there something fundamentally wrong with the idea? Thanks for reading, Jon --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en -~----------~----~----~----~------~----~------~--~---