开发者

What are good examples of using 'binding' in clojure?

开发者 https://www.devze.com 2023-03-30 01:03 出处:网络
I understand that the binding form allows rebindable dynamic scoping in clojure. So far the only uses I\'ve seen it used for is for I/O such as with print where *out* is rebound to what ever writer yo

I understand that the binding form allows rebindable dynamic scoping in clojure. So far the only uses I've seen it used for is for I/O such as with print where *out* is rebound to what ever writer you would like at the time.

I would like to see examples that truly take advantage of the power of binding where other facilities really don't work. Personally I've only used it in cases where passing around a user supplied object to all the functions was real开发者_运维问答ly tedious. Basically a situation where I am trying to create a context that the helper functions uses. (Similar to this case When should one use the temporarily-rebind-a-special-var idiom in Clojure? ) To be more specific, I was relying on the user to create a dynamic binding to the *db* var to allow the database functions to know what to operate on. This was particularly useful when the user needs to write lots of nested calls to the database functions. Typically, I'm OK if I need to write macros to make things easier for myself, but to require the user to do so seems bad. That being said, I try to avoid doing so as much as possible.

What are some other good use cases for 'binding' that I can copy and incorporate into my code?


I use bindings for two reasons:

  1. running tests that override constants or other values of other symbols
  2. using "global" resources such as database connections or message broker channels

testing

I am working on a distributed system with several components that communicate by sending messages over message exchanges. These exchanges have global names, which I have defined like such:

(ns const)
(def JOB-EXCHANGE    "my.job.xchg")
(def CRUNCH-EXCHANGE "my.crunch.xchg")
;; ... more constants

These constants are used in a number of places to send messages to the right place. To test my code, part of my test suite runs code that uses the actual message exchanges. However, I don't want my testing to interfere with the actual system.

To solve this, I wrap my testing code in a binding call that overrides these constants:

;; in my testing code:
(binding [const/CRUNCH-EXCHANGE (str const/CRUNCH-EXCHANGE (gensym "-TEST-"))
          const/CRUNCH-TASK-QUEUE (str const/CRUNCH-TASK-QUEUE (gensym "-TEST-"))]
  ;; tests here
)

Inside of this binding function, I can call any code that uses the constants and it'll use the overridden values.

using global resources

Another way I use bindings is to "fix" the value of a global or singleton resource inside a particular scope. Here's an example of a RabbitMQ library I wrote, where I bind the value of a RabbitMQ Connection to the symbol *amqp-connection* so that my code can use it:

(with-connection (make-connection opts)
  ;; code that uses a RabbitMQ connection
)

The implementation of with-connection is quite simple:

(def ^{:dynamic true} *amqp-connection* nil)

(defmacro with-connection
  "Binds connection to a value you can retrieve
   with (current-connection) within body."
  [conn & body]
  `(binding [*amqp-connection* ~conn]
     ~@body))

Any code in my RabbitMQ library can use the connection in *amqp-connection* and assume that it is a valid, open Connection. Or use the (current-connection) function, which throws a descriptive exception when you forgot to wrap your RabbitMQ calls in a with-connection:

(defn current-connection
  "If used within (with-connection conn ...),
   returns the currently bound connection."
  []
  (if (current-connection?)
    *amqp-connection*
    (throw (RuntimeException.
      "No current connection. Use (with-connection conn ...) to bind a connection."))))


In VimClojure backend you might have several repls running in the same JVM. However since the connection between Vim and the backend is not continuous, you potentially get a new thread for each command. So you can't easily retain state between commands.

What VimClojure does, is the following. It sets up a binding with all interesting Vars like *warn-on-reflection*, *1, *2, and so on. Then it executes the command and afterwards stores away the potentially changed Vars from the binding in some bookeeping infrastructure.

So every command just says "I belong to repl 4711" and it will see the state of said repl. Without affecting the state of repl 0815.


binding functions can be really helpful in test code. This is one of the great advantages of storing functions in vars (as Clojure does by default).

an excerpt from a cryptography program I wrote.

(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]))]
     ~@exprs))

How do you unit test a key generator function? it's supposed to be unpredictable. You could thread (if testing ...) everywhere or use some sort of mocking framework. or you can use a macro that "dynamically mocks" the random number generator out and put this only in the test code leaving your production side free of cruft.

(deftest test-key-gen 
   (with-fake-prng 
         ....))
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号