Clojure:如何在函数内创建记录?

vie*_*bel 2 clojure

在clojure中,我想在函数内创建一个记录.

我试过了:

(defn foo []
  (defrecord MyRecord [a b])
  (let [b (MyRecord. "1" "2")]))
Run Code Online (Sandbox Code Playgroud)

但它会导致异常:

java.lang.IllegalArgumentException: Unable to resolve classname: MyRecord
Run Code Online (Sandbox Code Playgroud)

任何的想法?

Mic*_*zyk 11

关键点

你应该只defrecord在顶级使用.1

因此,如果您确实需要自定义记录类型,则应在foo(在代码中某些位置foo定义之前处理)之外定义它.

否则,您可以使用常规地图.特别是,如果foo要创建多个"类型"的实体(在概念层面),尝试为每个创建一个记录类型(Java类)可能没有意义; 自然的解决方案是使用持有:type密钥的地图来表示所代表的实体的种类.

为什么它不起作用

问题中的代码无法编译,因为Clojure的编译器会解析new在编译时作为表单的第一个参数提到的类名.(在宏扩展过程中(MyRecord. "1" "2")扩展为(new MyRecord "1" "2").)这里名称MyRecord尚未解析为适当的类,因为后者尚未定义(它将defrecordfoo首次调用之后由表单创建).

为了解决这个问题,你可以做一些可怕的事情

(defn foo []
  (eval '(defrecord MyRecord [x y]))
  (let [b (clojure.lang.Reflector/invokeConstructor
           ;; assuming MyRecord will get created in the user ns:
           (Class/forName "user.MyRecord")
           (into-array Object ["1" "2"]))]
    b))
Run Code Online (Sandbox Code Playgroud)

这导致小猫finalize通过反射调用其方法,导致可怕的死亡.

作为最后的评论,上面这个可怕的版本会起作用,但它也会在每次调用时以相同的名称创建一个新的记录类型.这会导致奇怪的事情发生.尝试以下示例来获得风味:

(defprotocol PFoo (-foo [this]))

(defrecord Foo [x]
  PFoo
  (-foo [this] :foo))

(def f1 (Foo. 1))

(defrecord Foo [x])

(extend-protocol PFoo
  Foo
  (-foo [this] :bar))

(def f2 (Foo. 2))

(defrecord Foo [x])

(def f3 (Foo. 3))

(-foo f1)
; => :foo
(-foo f2)
; => :bar
(-foo f3)
; breaks

;; and of course
(identical? (class f1) (class f2))
; => false
(instance? (class f1) f2)
; => false
Run Code Online (Sandbox Code Playgroud)

此时,(Class/forName "user.Foo")(再次假设所有这些都发生在user命名空间中)返回的类f3,其中既不是f1也不f2是实例.


1宏有时可能会输出一个defrecord包含在do其中的形式; do然而,顶级s是特殊的,因为它们表现得好像它们包装的表单在顶层单独处理.