Reputation: 302
I'm new to Clojure.
In Java I can do something like this extremely contrived example:
public abstract class Foo {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
public interface Fooable {
public void sayHello(String name);
}
public class Bar extends Foo implements Fooable, Barable {
...
}
public class Baz extends Foo implements Fooable, Barable {
...
}
So, here we have two classes, implementing the Fooable interface the same way (through their abstract base class parent) and (presumably) implementing the Barable interface two different ways.
In Clojure, I can use defrecord
to define types Bar and Baz, and have them implement protocols rather than interfaces (which, essentially, are what protocols are, from what I understand). I know how to do this in the most basic sense, but anything more complex is stumping me.
Given this:
(defrecord Bar [x])
(defrecord Baz [x y])
(defprotocol Foo (say-hello [this name]))
how would I recreate the abstract base class functionality above, i.e. have one protocol implemented the same way across multiple defrecord
types, without duplicating code? I could of course do this, but the code repetition makes me cringe:
(extend-type Bar
Foo
(say-hello [this name] (str "Hello, " name "!")))
(extend-type Baz
Foo
(say-hello [this name] (str "Hello, " name "!")))
There has to be a cleaner way of doing this. Again, I'm new to Clojure (and Lisp in general; I'm trying to learn Common Lisp concurrently), so macros are an entirely new paradigm for me, but I thought I'd try my hand at one. Not surprisingly, it fails, and I'm not sure why:
(defmacro extend-type-list [tlist proto fmap]
(doseq
[t tlist] (list 'extend t proto fmap)))
fmap
of course is a map of functions, i.e. {:say-hello (fn [item x] (str "Hello, " x "!"))}
The doseq
, applied to a concrete list of record types and a concrete protocol, does work. Within the macro, of course, it doesn't, macroexpand
calls return nil.
So, question 1, I guess, is "what's wrong with my macro?". Question 2 is, how else can I programmatically extend protocols for types without a lot of repetitive boilerplate code?
Upvotes: 3
Views: 801
Reputation: 181
It looks like the two questions you asked at the bottom have been pretty comprehensively answered, but you asked something interesting in the text of the question: "how would I recreate the abstract base class functionality above, i.e. have one protocol implemented the same way across multiple defrecord types, without duplicating code?"
If we look at the documentation for datatypes, two quotes jump out:
Clojure datatypes intentionally restrict some features of Java, for example concrete derivation. So I believe the answer to your question really is, "you shouldn't". Multimethods or def'ing the functionality outside the defrecord and calling into it (like DaoWen's answer) should be preferred.
But if you really, really want to do exactly what you're doing in Java, you can use gen-class to extend a class.
(gen-class :name Bar :extends Foo :implements [Fooable])
(gen-class :name Baz :extends Foo :implements [Fooable])
Note that this implementation is a bit of a hack (you can't test in the repl because gen-class only does something when compiling), and doesn't use the :gen-class key in the ns macro like you typically would if you really had to gen-class. But using gen-class at all is a bit of a hack.
Upvotes: 2
Reputation: 22258
Your macro is returning nil
because doseq
returns nil
.
A Clojure macro should generate a new form using a combination of syntax-quotes (`
), unquotes (~
), and unquote-splicing (~@
) reader macros.
(defmacro extend-type-list [types protocol fmap]
`(do ~@(map (fn [t]
`(extend ~t ~protocol ~fmap))
types)))
Without using a macro, you have a few options:
(defrecord Bar [x])
(defrecord Baz [x y])
Use a simple var to hold the function map:
(defprotocol Foo (say-hello [this name]))
(def base-implementation
{:say-hello (fn [this name] (str "Hello, " name "!"))})
(extend Bar
Foo
base-implementation)
(extend Baz
Foo
base-implementation)
(say-hello (Bar. 1) "Bar") ;; => "Hello, Bar!"
(say-hello (Baz. 1 2) "Baz") ;; => "Hello, Baz!"
If you move to multimethods, you can accomplish something similar with clojure.core/derive
(defmulti say-hello (fn [this name] (class this)))
(derive Bar ::base)
(derive Baz ::base)
(defmethod say-hello ::base [_ name]
(str "Hello, " name "!"))
(say-hello (Bar. 1) "Bar") ;; => "Hello, Bar!"
(say-hello (Baz. 1 2) "Baz") ;; => "Hello, Baz!"
Upvotes: 4
Reputation: 33019
Macros are just normal functions that are called at compile-time rather than run-time. If you look at the definition of defmacro
, it actually just defines a function with some special meta-data.
Macros return Clojure syntax, which is spliced into the code at the point of the macro call. This means that your macro should return (quoted) syntax that looks exactly like what you would have typed manually into the source file at that point.
I find that a good way to design complex macros is to first declare it with defn
, tweaking it until it returns my expected output. As with all Lisp development, the REPL is your friend! This approach requires you to manually quote any parameters being passed to your proto-macro function. If you declare it as a macro then all the arguments are treated as data (e.g. variable names are passed in as symbols), but if you call it as a function it'll try to actually evaluate the arguments if you don't quote them!
If you try this out with your macro you'll see that it doesn't actually return anything! This is because you're using doseq
, which is for side-effecting computations. You want to use for
in order to build up the syntax to do all of the extend-type
calls. You'll probably need to wrap them in a (do )
form since a macro must return a single form.
According to the documentation, you can actually implement multiple interfaces/protocols directly within inside the defrecord
macro. After your field declaration, just add the bodies of all the extend-type
forms you would have declared.
(defrecord Bar [x]
Foo1
(method1 [this y] 'do-something)
Foo2
(method2 [this y z] 'do-something-else))
Upvotes: 2