Clojure Dynamic Binding
Asked Answered
W

2

7

I realize the following is a bad idea for many reasons. I also realize that given I have a stackoverflow rep of 23, it's nature to assume that I'm a newb learning to program. However, please humor me, and focus on the "how can we do this" rather than "why do you want to do this / you don't want to do this" aspect.

What I want:

(def dog (Dog. ...))
(def cat (Cat. ...))

(with-animal dog
  (println (str "Dog: " (speak) "\n")))
(with-animal cat
  (println (str "Cat: " (speak) "\n")))

to output:

Dog: woof
Cat: meow

So basically, I want with-animal to be a macro s.t. all occurences of the "speak" function call gets mapped to the object I'm calling the block with.

In particular, I don't want to write:

(let-binding [speak (fn [] "woof")] ...)
(let-binding [speak (fn [] "meow")] ...)

Rather, I want the with-animal to make the speak function map to some method of the object I'm calling with.

Is there a clean way to do this in Clojure?

Thanks!

Watchman answered 8/10, 2012 at 20:56 Comment(1)
i liked the disclaimer :)Spermatophore
G
19

Dynamic binding exists for a reason and it has lots of great uses, so no worries about being flamed for seeking to understand it :-) There is some confusion floating around many older Clojure tutorials that pre-date the need for adding ^:dynamic metadata to vars that you expect to dynamically rebind.

This first example uses dynamic binding by rebinding an existing name. This removes the need for the macro to introduce a new symbol:


first make some animals, I just use maps in this example, many people will use some other type of Object:
(def dog {:sound #(str "wooooof")})
(def cat {:sound #(str "mewwww")})

define the function we will be rebinding to be dynamic (which allows rebinding)

(defn :^dynamic speak [] (println "eh?"))

write a basic template macro to bind speak to the function in the animal:

(defmacro with-animal [animal & body] 
    `(binding [speak (:sound ~animal)] 
       ~@body))

and test it:

(with-animal dog  
  (println (str "Dog: " (speak) "\n")))
Dog: wooooof                                                   


and now the "advanced version" which just introduces a symbol speak into the scope using a let with no need for dynamic binding. This is not to say that binding is bad in some way, it just more closely fits your desire to not write (let-binding [speak (fn [] "meow")] ...) This type of maco is called anaphoric (if you're into such fancy names):

the important part is the ~' before the speak symbol that explicitly introduces an un-qualified symbol into the scope:

user> (defmacro with-animal [animal & body]
    `(let [~'speak (:sound ~animal)] 
        ~@body))
#'user/with-animal

user> (with-animal dog 
        (println (str "Dog: " (speak) "\n")))
Dog: wooooof 

nil


I hope that the contrast between these two examples serves to answer your question about binding behavior from an object into a scope. The first example binds the value for the body of the maco AND anything that is called from that body. The second example introduces the name ONLY for the body of the macro.
Gettings answered 8/10, 2012 at 21:46 Comment(2)
I like this solution. I can see myself having one place that defines all functions that with-animal uses, then for each animal, defining it in that file.Watchman
speak doesn't need to be a function in either of the examples, does it? It could be just (def ^:dynamic speak "eh?"). All you're doing is concatenating it into a string anyway.Piggin
E
0

If you really want to make animal types talk idiomatically, use Clojure Protocols:

(defprotocol ISpeak
  (speak [animal] "make the type say it's thing"))

(deftype Dog []
  ISpeak
  (speak [this] "Woof!"))

(deftype Cat []
  ISpeak
  (speak [_] "Meow!!")) ;;you can "drop" the item if not used using _

(def a-dog (Dog.))
(speak a-dog)
;;=>"Woof!"

(def a-cat (Cat.))
(speak a-cat)
;;=>"Meow!!"

Please note you can extend any type (class) with the speak method.

(extend java.util.Random
  ISpeak
  {:speak (fn [_] "I'm throwing dices at you!")})

(speak (java.util.Random.))
;;=>"I'm throwing dices at you!"

The syntax is a bit different for Java classes, please see the Protocols documentation for more information.

Ecdysiast answered 17/7, 2015 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.