我是Clojure的新手。
在Java中,我可以做一些像这个非常人为的例子:
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 {
...
}
所以,这里我们有两个类,以相同的方式实现Fooable接口(通过它们的抽象基类父级)和(可能)以两种不同的方式实现Barable接口。
在Clojure中,我可以使用defrecord
来定义类型Bar和Baz,让它们实现协议而不是接口(从本质上讲,它实际上是协议)。我知道如何在最基本的意义上做到这一点,但任何更复杂的事情都让我感到难过。
鉴于此:
(defrecord Bar [x])
(defrecord Baz [x y])
(defprotocol Foo (say-hello [this name]))
如何在上面重新创建抽象基类功能,即在多个defrecord
类型中以相同的方式实现一个协议,而不重复代码?我当然可以这样做,但代码重复让我感到畏缩:
(extend-type Bar
Foo
(say-hello [this name] (str "Hello, " name "!")))
(extend-type Baz
Foo
(say-hello [this name] (str "Hello, " name "!")))
必须有一种更清洁的方式来做到这一点。再一次,我是Clojure的新手(和Lisp一般;我试图同时学习Common Lisp),所以宏对我来说是一个全新的范例,但我想我会试试我的手在一个。毫不奇怪,它失败了,我不确定原因:
(defmacro extend-type-list [tlist proto fmap]
(doseq
[t tlist] (list 'extend t proto fmap)))
fmap
当然是一个函数映射,即{:say-hello (fn [item x] (str "Hello, " x "!"))}
doseq
,应用于具体的记录类型列表和具体协议,确实有效。当然,在宏中,macroexpand
调用返回为零。
所以,问题1,我猜,是"我的宏有什么问题?"。问题2是,如何在没有大量重复样板代码的情况下以编程方式扩展类型的协议?
答案 0 :(得分:4)
您的宏正在返回nil
,因为doseq
会返回nil
。
Clojure宏应使用语法引用(`
),非引号(~
)和非引号拼接(~@
){{3}的组合生成新表单}宏。
(defmacro extend-type-list [types protocol fmap]
`(do ~@(map (fn [t]
`(extend ~t ~protocol ~fmap))
types)))
不使用宏,您有几个选择:
(defrecord Bar [x])
(defrecord Baz [x y])
使用简单的var来保存函数贴图:
(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!"
如果您转到reader,则可以使用multimethods
完成类似的操作(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!"
答案 1 :(得分:2)
宏只是在编译时而不是运行时调用的普通函数。如果你看一下defmacro
的定义,它实际上只是定义了一个带有一些特殊元数据的函数。
宏返回 Clojure语法,它在宏调用点拼接到代码中。这意味着您的宏应返回(带引号)语法,该语法与您在此时手动输入到源文件中的语法完全相同。
我发现设计复杂宏的好方法是首先用defn
声明它,调整它直到它返回我的预期输出。与所有Lisp开发一样,REPL是您的朋友!此方法要求您手动引用传递给proto-macro函数的任何参数。如果你将它声明为一个宏,那么所有的参数都被视为数据(例如,变量名称作为符号传递),但是如果你把它作为一个函数来调用,它会尝试实际评估参数,如果你没有&#39引用它们!
如果您使用宏来尝试这一点,您会发现它实际上并没有返回任何内容!这是因为您正在使用doseq
,这是用于副作用的计算。您希望使用for
来构建语法来执行所有extend-type
调用。您可能需要将它们换成(do )
形式,因为宏必须返回单个表单。
根据the documentation,您实际上可以直接在defrecord
宏内部实现多个接口/协议。在您的字段声明之后,只需添加您已声明的所有extend-type
表单的正文。
(defrecord Bar [x]
Foo1
(method1 [this y] 'do-something)
Foo2
(method2 [this y z] 'do-something-else))
答案 2 :(得分:2)
看起来您在底部提出的两个问题已经得到了相当全面的回答,但您在问题文本中提出了一些有趣的问题:“我将如何重新创建上面的抽象基类功能,即有一个协议实现了多个defrecord类型的相同方式,没有重复代码?“
如果我们查看datatypes的文档,就会跳出两个引号:
Clojure数据类型故意限制Java的某些功能,例如具体的派生。所以我相信你的问题的答案真的是,“你不应该”。应该首选多种方法或者在defrecord之外定义功能并调用它(如DaoWen的答案)。
但是如果你真的想要用Java完成你正在做的事情,你可以使用gen-class扩展一个类。
(gen-class :name Bar :extends Foo :implements [Fooable])
(gen-class :name Baz :extends Foo :implements [Fooable])
请注意,这个实现有点乱(你不能在repl中测试因为gen-class只在编译时做了一些事情),并且不像你那样在ns宏中使用:gen-class键通常会,如果你真的不得不生成班级。但是根本使用gen-class是一种黑客攻击。