clojure - 以非香草方式扩展类型

时间:2014-10-15 02:05:45

标签: macros clojure

我是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是,如何在没有大量重复样板代码的情况下以编程方式扩展类型的协议?

3 个答案:

答案 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)

问题1

宏只是在编译时而不是运行时调用的普通函数。如果你看一下defmacro的定义,它实际上只是定义了一个带有一些特殊元数据的函数。

宏返回 Clojure语法,它在宏调用点拼接到代码中。这意味着您的宏应返回(带引号)语法,该语法与您在此时手动输入到源文件中的语法完全相同。

我发现设计复杂宏的好方法是首先用defn声明它,调整它直到它返回我的预期输出。与所有Lisp开发一样,REPL是您的朋友!此方法要求您手动引用传递给proto-macro函数的任何参数。如果你将它声明为一个宏,那么所有的参数都被视为数据(例如,变量名称作为符号传递),但是如果你把它作为一个函数来调用,它会尝试实际评估参数,如果你没有&#39引用它们!

如果您使用宏来尝试这一点,您会发现它实际上并没有返回任何内容!这是因为您正在使用doseq,这是用于副作用的计算。您希望使用for来构建语法来执行所有extend-type调用。您可能需要将它们换成(do )形式,因为宏必须返回单个表单。

问题2

根据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是一种黑客攻击。