When should one use the temporarily-rebind-a-speci

2019-06-22 00:32发布

问题:

I've noticed that some libraries such as clojure-twitter use special vars (the ones intended for dynamic binding that are surrounded by asterisks) for oauth authentication. You save your authentication in a var and then use (with-oauth myauth ..). I think this is a very nice solution to this sort of problem, because you can rebind the auth var for each user of the application.

I've taken a similar route in an email client I've been writing. I have a special var named session that I bind to a map with the current user's session, and user info, and there are various important functions that use information from that var. I wrote a macro, with-session to temporarily rebind it in the context of a set of forms passed to with-session. It turns out to be a pretty clean solution (to me).

So, my question is this: am I 'doin' it rite'? Is this a bad design decision, or is this one of the intended usages of special vars?

回答1:

You seem to be doing it exactly right. In fact, there's a number of built-in / contrib macros which work similarly, say with-out-str or clojure.contrib.sql/with-connection. The latter is a rather key part of present day Clojure infrastructure, so whatever idioms it uses have been scrutinised by a lot of people.

The important gotcha to keep in mind is that threads you launch while in scope of a bindings / with-bindings form do not inherit the rebound values for the vars in question; rather, they see the root bindings. If you want to propagate your bindings to worker threads / agents, either pass them on explicitly (as function arguments, say) or use bound-fn.



回答2:

Every time you make a global var that you plan to re-bind, you're adding an extra implicit argument to every function that accesses that variable. Unlike proper (explicit) arguments, this hidden argument doesn't show up in the functions's signature and there may be little indication that the function is using it. Your code become less "functional"; calling the same function with the same arguments may result in different return values based on the current state of these global dynamic vars.

The benefit of global vars is that you can easily specify a default value, and it lets you be lazy by not having to pass that var around to every function that uses it.

The downside is that your code is harder to read, test, use and debug. And your code becomes potentially more error-prone; it's easy to forget to bind or re-bind the var before you call the function that uses it, but it's not so easy to forget to pass in a session parameter when it's right in the arglist.

So you end up with mystery bugs, and weird implicit dependencies between functions. Consider this scenario:

user> (defn foo [] (when-not (:logged-in *session*) (throw (Exception. "Access denied!"))))
#'user/foo
user> (defn bar [] (foo))
#'user/bar
user> (defn quux [] (bar))
#'user/quux
user> (quux)
; Evaluation aborted.  ;; Access denied!

The behavior of quux depends implicitly on the session having a value, but you wouldn't know that unless you dug down through every function quux calls, and every function those functions call. Imagine a call chain 10 or 20 levels deep, with one function at the bottom depending on *session*. Have fun debugging that.

If instead you had (defn foo [session] ...), (defn bar [session] ...), (defn quux [session] ...), it would be immediately obvious to you that if you call quux, you'd better have a session ready.

Personally, I would use explicit arguments unless I had a strong, sane default value that tons of functions used, that I planned to very rarely or never rebind. (e.g. it would be silly to pass STDOUT around as an explicit argument to every function that wants to print anything.)



回答3:

Binding functions are great for test code.

I use rebinding wrapper functions extensively in my test code to do things like mock-up the random number genrator, use a fixed block size etc... so that I can actually test an encryption function against known output.

(defmacro with-fake-prng [ & exprs ]
  "replaces the prng with one that produces consisten results"
  `(binding [com.cryptovide.split/get-prng (fn [] (cycle [1 2 3]))
             com.cryptovide.modmath/mody 719
             com.cryptovide.modmath/field-size 10]
        ~@exprs))

(is (= (with-fake-prng (encrypt-string "asdf")) [23 54 13 63]))  

When using bindings its useful to remember that they only rebind for the current thread so when you fire something off in pmap which uses the thread pool you may loose your bindings. If you have some code that builds a string in parallel like this:

(with-out-str
    (pmap process-data input))

Using that innocent looping \p in front of the map will cause the binding to go away because it will run the process-data function in several threads from the thread-pool.

EDIT: Michał Marczyk points out the bound-fn macro which you can use to keep from loosing the bindings when using threads.