Clojure - non-vanilla type extension

I am new to Clojure.

In Java, I can do something like this 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 in the same way (through their abstract parent base class) and (presumably) implementing the Barable interface in two different ways.

In Clojure, I can use defrecord

Bar and Baz to define types and have protocol implementations for them, not interfaces (which are, in fact, protocols, from what I understand). I know how to do it in the most basic sense, but something more complicated throws me off.

Considering this:

(defrecord Bar [x])
(defrecord Baz [x y])

(defprotocol Foo (say-hello [this name]))

      

how would i recreate the base class abstract functionality mentioned above i.e. one protocol implemented in the same way across multiple types defrecord

, without code duplication? I could of course do this, but repeating the code 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 must be a cleaner way to do this. Again, I'm new to Clojure (and Lisp in general, I'm trying to learn Common Lisp at the same time), so macros are a whole new paradigm for me, but I thought I'd try my hand at one. Unsurprisingly this fails, and I'm not sure why:

(defmacro extend-type-list [tlist proto fmap]
  (doseq
      [t tlist] (list 'extend t proto fmap)))

      

fmap

is of course a function mapping, i.e. {:say-hello (fn [item x] (str "Hello, " x "!"))}

Works doseq

applied to a specific list of post types and a specific protocol. Inside the macro, of course not, macroexpand

calls return nil.

So question 1, I am thinking "what happened to my macro?" Question 2: How else can I programmatically extend protocols for types without a lot of duplicate patterns?

+3


source to share


3 answers


Your macro returns nil

because it doseq

returns nil

.

A Clojure macro should generate a new form using a combination of syntax quotation marks ( `

), 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 several options:

(defrecord Bar [x])
(defrecord Baz [x y])

      



Use a simple variable to store 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 go to multimethods you can do something like this 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!"

      

+4


source


Question 1

Macros are regular functions that are called at compile time, not at run time. If you look at the definition defmacro

, it will actually just define a function with some special metadata.

Macros return Clojure syntax, which is spliced ​​into code at the point of the macro call. This means that your macro should return a (quoted) syntax that looks exactly like what you manually typed into the source file at this point.

I find a good way to design complex macros is to first declare it with defn

, tweak it until it returns my expected output. As with Lisp development, the REPL is your friend! This approach requires you to manually specify any parameters passed to your proto macro function. If you declare it as a macro then all arguments are treated as data (e.g. variable names are passed as characters), but if you call it as a function it will try to actually evaluate the arguments unless you quote them



If you try this with your macro, you will see that it returns nothing! It has something to do with what you are using doseq

, which is for side calculations. You want to use for

to create syntax for making all calls extend-type

. You will probably have to wrap them in a form (do )

as the macro must return one form.

Question 2

According to the documentation , you can actually implement multiple interfaces / protocols directly inside a macro defrecord

. After declaring the field, simply add the bodies of all the forms extend-type

you would declare.

(defrecord Bar [x]
  Foo1
  (method1 [this y] 'do-something)
  Foo2
  (method2 [this y z] 'do-something-else))

      

+2


source


It looks like the two questions you asked below were covered in some detail, but you asked something interesting in the text of the question: "How would I recreate the abstract functionality of the base class above, i.e. one protocol implemented the same in several defrecord types without duplicating code? "

If we look at the documentation for datatypes , two quotes come out:

  • Concrete output is bad
    • you cannot infer data types from concrete classes, only interfaces
  • Linking polymorphism to inheritance is bad.

Clojure datatypes deliberately restrict certain Java functionality, such as specific output. So I think the answer to your question is actually "you shouldn't." It is preferable to use multiple methods, or define functionality outside of defrecord and call into it (like DaoWen's answer).

But if you really want to do exactly what you do in Java, you can use gen-class to extend the class.

(gen-class :name Bar :extends Foo :implements [Fooable])
(gen-class :name Baz :extends Foo :implements [Fooable])

      

Note that this implementation is a little hacky (you can't test in repl because gen-class only does something when compiled) and doesn't use the gen-class key in the ns macro like you would normally if you really were in the gene class. But using gen-class is generally a bit of a hack.

+2


source







All Articles