23

已编辑。我现在的问题是:在静态类型语言中,通常使用什么惯用的 Clojure 构造而不是 sum 类型?到目前为止的共识:如果行为可以统一,则使用协议,否则使用标记对/映射,在前置和后置条件中放置必要的断言。

Clojure 提供了多种表示产品类型的方法:向量、映射、记录……,但是如何表示总和类型,也称为标记联合和变体记录?类似于Either a bHaskell 或Either[+A, +B]Scala 中的东西。

我想到的第一件事是带有特殊标签的地图:{:tag :left :value a},但是所有代码都将被条件污染,(:tag value)如果不存在则处理特殊情况......我想确保的是那:tag总是存在的,它只能采用一个指定的值,并且对应的值始终具有相同的类型/行为并且不能是nil,并且有一种简单的方法可以看到我处理了代码中的所有情况。

我可以在 的行中想到一个宏defrecord,但对于 sum 类型:

; it creates a special record type and some helper functions
(defvariant Either
   left Foo
   right :bar)
; user.Either

(def x (left (Foo. "foo")))   ;; factory functions for every variant
; #user.Either{:variant :left :value #user.Foo{:name "foo"}}
(def y (right (Foo. "bar")))  ;; factory functions check types
; SomeException...
(def y (right ^{:type :bar} ()))
; #user.Either{:variant :right :value ()}

(variants x) ;; list of all possible options is intrinsic to the value
; [:left :right]

这样的事情是否已经存在?回答:没有)。

4

7 回答 7

21

您如何表示总和类型,也称为标记联合和变体记录?类似于Either a bHaskell 或 Either[+A, +B]Scala 中的东西。

Either有两个用途:返回两种类型之一的值,或者返回两个相同类型的值,它们应该基于标签具有不同的语义。

第一次使用仅在使用静态类型系统时才重要。 Either考虑到 Haskell 类型系统的约束,基本上是可能的最小解决方案。使用动态类型系统,您可以返回所需的任何类型的值。Either不需要。

第二种用途重要,但可以通过两种(或更多)方式非常简单地完成:

  1. {:tag :left :value 123} {:tag :right :value "hello"}
  2. {:left 123} {:right "hello"}

我想确保的是, :tag 始终存在,并且它只能采用指定值之一,并且相应的值始终具有相同的类型/行为并且不能为 nil,并且有一种简单的方法可以看到我处理了代码中的所有情况。

如果您想以静态方式确保这一点,Clojure 可能不是您的语言。原因很简单:表达式直到运行时才具有类型——直到它们返回一个值。

宏不起作用的原因是在宏扩展时,您没有运行时值——因此没有运行时类型。您有符号、原子、s 表达式等编译时构造。您可以eval使用它们,但eval由于多种原因,使用被认为是不好的做法。

但是,我们可以在运行时做得很好。

  • 我想确保的是 :tag 始终存在,
  • 并且只能取指定值之一
  • 并且相应的值始终具有相同的类型/行为
  • 并且不能为零
  • 并且有一种简单的方法可以看出我处理了代码中的所有情况。

我的策略是将所有通常是静态的(在 Haskell 中)转换为运行时。让我们写一些代码。

;; let us define a union "type" (static type to runtime value)
(def either-string-number {:left java.lang.String :right java.lang.Number})

;; a constructor for a given type
(defn mk-value-of-union [union-type tag value]  
  (assert (union-type tag)) ; tag is valid  
  (assert (instance? (union-type tag) value)) ; value is of correct type  
  (assert value)  
  {:tag tag :value value :union-type union-type}) 

;; "conditional" to ensure that all the cases are handled  
;; take a value and a map of tags to functions of one argument
;; if calls the function mapped to the appropriate tag
(defn union-case-fn [union-value tag-fn]
  ;; assert that we handle all cases
  (assert (= (set (keys tag-fn))
             (set (keys (:union-type union-value)))))
  ((tag-fn (:tag union-value)) (:value union-value)))

;; extra points for wrapping this in a macro

;; example
(def j (mk-value-of-union either-string-number :right 2))

(union-case-fn j {:left #(println "left: " %) :right #(println "right: " %)})
=> right: 2

(union-case-fn j {:left #(println "left: " %)})
=> AssertionError Assert failed: (= (set (keys tag-fn)) (set (keys (:union-type union-value))))

此代码使用以下惯用的 Clojure 构造:

  • 数据驱动编程:创建代表“类型”的数据结构。这个值是不可变的和一流的,你可以使用整个语言来实现逻辑。这是我不相信 Haskell 能做到的事情:在运行时操作类型。
  • 使用地图来表示值。
  • 高阶编程:将 fns 的映射传递给另一个函数。

如果您使用Either多态性,您可以选择使用协议。否则,如果您对标签感兴趣,那么某种形式的东西{:tag :left :value 123}是最惯用的。你会经常看到这样的东西:

;; let's say we have a function that may generate an error or succeed
(defn somefunction []
  ...
  (if (some error condition)
    {:status :error :message "Really bad error occurred."}
    {:status :success :result [1 2 3]}))

;; then you can check the status
(let [r (somefunction)]
  (case (:status r)
    :error
    (println "Error: " (:message r))
    :success
    (do-something-else (:result r))
    ;; default
    (println "Don't know what to do!")))
于 2012-06-08T14:40:49.347 回答
6

一般来说,动态类型语言中的 sum 类型表示为:

  • 标记对(例如,带有代表构造函数的标记的产品类型)
  • 在运行时对标签进行案例分析以进行调度

在静态类型语言中,大多数值都通过类型来区分——这意味着您无需进行运行时标记分析即可知道您是否拥有 anEither或 a Maybe——因此您只需查看标记即可知道它是 aLeft还是 a Right

在动态类型设置中,您必须先进行运行时类型分析(查看您拥有什么类型的值),然后对构造函数进行案例分析(查看您拥有哪种类型的值)。

一种方法是为每种类型的每个构造函数分配一个唯一标记。

在某种程度上,您可以将动态类型视为将所有值放入单个求和类型,将所有类型分析推迟到运行时测试。


我想确保的是, :tag 始终存在,并且它只能采用指定值之一,并且相应的值始终具有相同的类型/行为并且不能为 nil,并且有一种简单的方法可以看到我处理了代码中的所有情况。

顺便说一句,这几乎是对静态类型系统将做什么的描述。

于 2012-06-08T13:10:02.600 回答
4

如果没有完成像类型化 clojure这样令人兴奋的事情,我认为您无法避免运行时检查断言。

clojure 提供的一个鲜为人知的功能绝对有助于运行时检查,是前置条件和后置条件的实现(参见http://clojure.org/special_formsfogus 的博客文章)。我认为您甚至可以使用带有前置条件和后置条件的单个高阶包装函数来检查您对相关代码的所有断言。这很好地避免了运行时检查“污染问题”。

于 2012-06-14T14:44:03.693 回答
4

这在某些语言中运行良好的原因是您在结果上分派(通常按类型) - 即您使用结果的某些属性(通常是类型)来决定下一步做什么。

所以你需要看看在clojure中调度是如何发生的。

  1. nil 特殊情况- 该nil值在各个地方都是特殊情况,可以用作“Maybe”的“None”部分。例如,if-let非常有用。

  2. 模式匹配- 除了解构序列之外,基本 clojure 对此没有太多支持,但是有各种库可以做到。请参阅Clojure 替换 ADT 和模式匹配?[更新:在评论中 mnicky 说这已经过时了,你应该使用core.match ]

  3. OO 按类型- 按类型选择方法。因此您可以返回父级的不同子类并调用重载的方法来执行您想要的不同操作。如果你来自一个感觉很奇怪/笨拙的功能背景,但这是一个选择。

  4. 手动标记- 最后,您可以使用casecond与显式标记一起使用。更有用的是,您可以将它们包装在某种按您想要的方式工作的宏中。

于 2012-06-08T12:56:41.573 回答
4

作为一种动态类型语言,通常类型在 Clojure 中的相关性/重要性不如在 Haskell / Scala 中。您实际上并不需要显式定义它们- 例如,您已经可以将 A 类型或 B 类型的值存储在变量中。

所以这真的取决于你想用这些总和类型做什么。您可能真的对基于 type 的多态行为感兴趣,在这种情况下,定义一个协议和两种不同的记录类型,它们一起给出 sum 类型的多态行为可能是有意义的:

(defprotocol Fooable
  (foo [x]))

(defrecord AType [avalue]
  Fooable 
    (foo [x]
      (println (str "A value: " (:avalue x)))))

(defrecord BType [bvalue]
  Fooable 
    (foo [x]
      (println (str "B value: " (:bvalue x)))))

(foo (AType. "AAAAAA"))

=> A value: AAAAAA

我认为这将提供您可能希望从 sum 类型中获得的几乎所有好处。

这种方法的其他优点:

  • 记录和协议在 Clojure 中非常惯用
  • 出色的性能(因为协议调度被高度优化)
  • 您可以在协议中添加对 nil 的处理(通过extend-protocol
于 2012-06-14T09:11:23.050 回答
4

使用带有标签的向量作为向量中的第一个元素,并使用 core.match 来解构标记的数据。因此,对于上面的示例,“任一”数据将被编码为:

[:left 123]
[:right "hello"]

然后解构你需要参考core.match并使用:

(match either
  [:left num-val] (do-something-to-num num-val)
  [:right str-val] (do-something-to-str str-val))

这比其他答案更简洁。

这个 youtube talk更详细地解释了为什么向量对于在地图上编码变体是可取的。我的总结是,使用地图对变体进行编码是有问题的,因为您必须记住地图是“标记地图”而不是常规地图。要正确使用“标记地图”,您必须始终进行两阶段查找:首先是标记,然后是基于标记的数据。如果(何时)您忘记在映射编码的变体中查找标签,或者对标签或数据的键查找错误,您将得到一个难以追踪的空指针异常。

该视频还涵盖了矢量编码变体的这些方面:

  • 捕获非法标签。
  • 如果需要,使用Typed Clojure添加静态检查。
  • 将此数据存储在Datomic中。
于 2016-11-30T12:57:24.183 回答
2

不,到目前为止,clojure 中还没有这样的东西。虽然您可以实现它,但 IMO 这种类型似乎更适合静态类型语言,并且不会在 clojure 等动态环境中为您带来太多好处。

于 2012-06-08T12:00:02.420 回答