实施数据模型以防止常见错误。

7

在Clojure中,似乎有多种实现数据模型的方式:

  • 普通内置数据类型(maps/lists/sets/vectors)
  • 内置数据类型+元数据--例如:(type ^{:type ::mytype} {:fieldname 1})
  • 内置数据类型+特殊访问器函数(例如,从地图中获取不存在的键会抛出异常,而不是静默返回nil
  • deftype
  • defstruct
  • defrecord
  • defprotocol

我们已经到了地图/列表不再适用于我们的地步——我们遇到了大量错误,预先条件/后置条件很容易捕获,但是如果不这样做,追踪起来需要很长时间(并且难以编写接受嵌套地图/列表/向量的函数的有效前/post-conditions)——但我们不确定该选择上述哪一个。

我们有三个主要目标:

  • 编写惯用的Clojure代码
  • 避免花费大量时间寻找愚蠢的类型错误
  • 对于我们能够更改/重构代码而不会破坏任何东西,有信心

我们如何利用Clojure的力量来帮助我们?

2个回答

4
Clojure文化非常支持原始数据类型。这是有道理的。但明确的数据类型也可以很有用。当您的普通数据类型变得足够复杂和具体时,您实际上拥有了一个隐式数据类型而没有规范。
依赖构造函数。听起来有点不好,在面向对象的方式下,但构造函数只是一个安全,方便地创建数据类型的函数。纯数据结构的缺点是它们鼓励即时创建数据。所以,我尝试直接组合我的数据,而不是调用(myconstructor ...)。这样做存在很多潜在错误的可能性,并且在需要更改基础数据类型时会出现问题。
记录是最佳选择。在所有关于原始数据类型的争论中,很容易忘记记录可以做到像映射一样的事情。它们可以通过相同的方式访问。您可以在它们上面调用seq。您可以以相同的方式解构它们。绝大多数期望映射的函数也将接受记录。
元数据不能保护您。我对依赖元数据的主要反对意见是它不会在相等性中反映出来。
user> (= (with-meta [1 2 3] {:type :A})  (with-meta [1 2 3] {:type :B}))
true

无论您是否接受,我都会担心这会引入新的微妙错误。
其他数据类型选项:
  • deftype仅用于在创建新的基本或特殊目的数据结构时进行低级别工作。与defrecord不同,它并不带来所有Clojure的好处。对于大多数工作来说,这是不必要的或不可取的。
  • 建议废弃使用defstruct。当Rich Hickey引入类型和协议时,他基本上说应该永远优先选择defrecord。

  • 协议非常有用,尽管它们感觉有点偏离(函数+数据)范例。如果您发现自己正在创建记录,则应考虑定义协议。

    编辑:我发现普通数据类型的另一个优点,这一点之前对我来说还不明显:如果您正在进行Web编程,则可以有效且轻松地将普通数据类型转换为JSON。(用于执行此操作的库包括clojure.data.json、clj-json和我最喜欢的cheshire)。使用记录和数据类型,这项任务要繁琐得多。


    好的,所以我需要弄清楚defrecorddefprotocol,可以忽略defstruct,也不必过于担心deftype。在Clojure程序中,defrecord创建Java代码是否重要--我的意思是,我不想担心有一个Java类,但如果Clojure想私下使用一个,那就没问题了?非常好的答案,非常有帮助。 - Matt Fenwick
    好的,一个普通的map也是一个Java类,你可以在https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/APersistentMap.java中看到。 - Rob Lachlan
    所以,defrecord 创建一个 Java 类并不是什么大问题。从我们的角度来看,使用记录(record)并不会有太大区别——除了构造方式之外,它会完全感觉像 Clojure 数据。 - Rob Lachlan
    是的,一定要阅读关于defrecord、defprotocol和extend protocol的相关内容。在我看来,它们背后的逻辑非常精心设计。 - Rob Lachlan
    我的疑虑是我不确定这句话的确切含义:根据给定名称为类动态生成已编译字节码,在与当前命名空间同名的包中,包括给定字段和可选的协议和/或接口方法。(来自defrecord文档) - Matt Fenwick
    这意味着如果需要,您可以从Java中调用它。记录很好,因为它们在Clojure和Java中都可以成为一等公民。(编译的字节码与您在Clojure中编写的所有其他内容没有任何区别--它们都会被编译为字节码。其余部分只是了解从Java调用时要调用什么的相关内容。) - Rob Lachlan

    1

    能够编写适用于映射和列表的函数确实非常方便,如果通过切换到类和协议而失去这些功能,那将是一种遗憾。毕竟,在一个类型上拥有一百个函数要比在多个协议或记录上更好。切换到协议或记录可能会有点过重,例如在调试时防止(debug (map :rows (get-state))

    元数据是一种很好的方式,可以在需要的地方为数据添加“恰到好处的类型”,而不会失去代码库中其他部分的优势。我建议选择第二个选项:

    • '内置数据类型+元数据((type ^ {:type ::mytype} {:fieldname 1}))'

    这不是Alan Perlis关于一个数据类型上有一百个函数的说法吗?我重新格式化了标题中的示例——不小心放在了非LISP括号中! - Matt Fenwick
    没错,那正是我想要的引用。功劳要归于功臣! - Arthur Ulfeldt

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接