【问题标题】:clojure - extending types in non-vanilla waysclojure - 以非普通方式扩展类型
【发布时间】:2014-12-09 23:40:52
【问题描述】:

我是 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 调用不会返回 nil。

所以,我猜问题 1 是“我的宏有什么问题?”。问题 2 是,在没有大量重复样板代码的情况下,我还能如何以编程方式扩展类型的协议?

【问题讨论】:

    标签: macros clojure


    【解决方案1】:

    您的宏正在返回nil,因为doseq 返回nil

    Clojure 宏应使用语法引号 (`)、取消引号 (~) 和取消引号拼接 (~@) reader 宏的组合生成新表单。

    (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!"
    

    如果你转移到multimethods,你可以用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!"
    

    【讨论】:

    • 虽然它盯着我的脸,但我没有意识到extend 中的函数映射只是:具有适当函数值的映射。正如extend-type 伪装的那样,我认为它们是类似方法的东西,没有一流的地位。我设法忽略了the commentary函数映射是关键字化方法名称到普通 fns 的映射......这有助于轻松重用现有 fns 和映射,用于代码重用/混合,无需派生或组合。确实,简单变得简单。
    • 是的,我遇到了同样的问题,首先发现了extend-type;我没有正确考虑extendextend-type 的来源。我将不得不阅读阅读器宏语法以完全理解更正的宏,但我选择这个答案作为正确的答案,因为它不仅向我展示了更正的宏,而且还存在替代的有效方法来做我的事情我正在努力。
    【解决方案2】:

    问题 1

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

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

    我发现设计复杂宏的一个好方法是首先用defn 声明它,然后对其进行调整,直到它返回我预期的输出。与所有 Lisp 开发一样,REPL 是您的朋友!这种方法要求您手动引用传递给您的原型宏函数的任何参数。如果您将其声明为宏,则所有参数都将被视为数据(例如,变量名称作为符号传入),但如果您将其作为函数调用,如果您不引用它们,它将尝试实际评估参数!

    如果你用你的宏试试这个,你会发现它实际上并没有返回任何东西!这是因为您使用的是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))
    

    【讨论】:

    • 我没有意识到宏实际上只不过是在编译时调用的函数。它们对我来说仍然是新的,所以我想仍然有些神秘。不过,有一个问题要问你——我正在通过Clojure Programming 部分学习 Clojure;他们指出,除了优化之外,通常要避免实现内联协议,原因有很多(内联实现成为定义类型的一部分,因此如果不重新定义类型就不能在运行时更改等)这是否成立有你的经验吗?
    【解决方案3】:

    看起来您在底部提出的两个问题已经得到了相当全面的回答,但是您在问题的文本中提出了一些有趣的问题:“我将如何重新创建上面的抽象基类功能,即有一个协议实现跨多种 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,通常会这样做。但是完全使用 gen-class 有点麻烦。

    【讨论】:

      猜你喜欢
      • 2014-03-12
      • 2020-04-13
      • 1970-01-01
      • 1970-01-01
      • 2020-05-23
      • 2021-11-19
      • 2015-11-15
      • 2018-07-30
      • 2011-08-28
      相关资源
      最近更新 更多