Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When should one use the temporarily-rebind-a-special-var idiom in Clojure?

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?

like image 344
Rayne Avatar asked Feb 15 '10 03:02

Rayne


2 Answers

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.

like image 57
Michał Marczyk Avatar answered Sep 30 '22 07:09

Michał Marczyk


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.)

like image 29
Brian Carper Avatar answered Sep 30 '22 06:09

Brian Carper