在Clojure中表示和型(或者是b)的惯用法

时间:2012-06-08 11:05:43

标签: types clojure discriminated-union

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

Clojure提供了许多表达产品类型的方法:向量,地图,记录......,但您如何表示sum types,也称为标记的联合和变体记录?类似于Haskell中的Either a b或Scala中的Either[+A, +B]

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

我可以在nil的行中考虑一个宏,但对于总和类型:

defrecord

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

7 个答案:

答案 0 :(得分:16)

  

你如何表示总和类型,也称为标记的联合和     变种记录?像Haskell中的Either a b或者     Scala中的Either[+A, +B]

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

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

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

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

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

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

    宏不起作用的原因是在宏扩展时,你 没有运行时值 - 因此没有运行时类型。你有 编译时构造如符号,原子,sexpressions等。你 可以{:left 123} {:right "hello"}他们,但使用eval被视为不良做法 原因有多少。

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

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

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

    eval

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

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

    如果您使用;; 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)))) 进行多态,则可以选择使用协议。否则,如果您对标签感兴趣,那么Either形式的东西就是最惯用的。你会经常看到这样的东西:

    {:tag :left :value 123}

答案 1 :(得分:6)

通常,动态类型语言中的和类型表示为:

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

在静态类型语言中,大多数值都按类型区分 - 这意味着您无需进行运行时标记分析即可知道您是Either还是Maybe - 所以您只需要查看代码,了解它是Left还是Right

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

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

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


  

我想要确保的是:标记始终存在,并且它只能使用其中一个指定值,并且相应的值始终具有相同的类型/行为且不能为零,并且存在很容易看到我在代码中处理了所有情况。

顺便说一下,这几乎是对静态类型系统的作用的描述。

答案 2 :(得分:4)

这在某些语言中如此有效的原因是你在结果上调度(通常是按类型) - 即你使用结果的某些属性(通常是类型)来决定下一步该做什么。

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

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

  2. 模式匹配 - 除了解构序列之外,基础clojure对此没有太多支持,但有各种库可以做到。请参阅Clojure replacement for ADTs and Pattern Matching? [更新:在评论中mnicky表示已过时,您应该使用core.match]

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

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

答案 3 :(得分:4)

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

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

(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

我认为这将提供您可能从和类型中获得的所有好处。

这种方法的其他好处:

  • 记录和协议在Clojure中非常惯用
  • 卓越的性能(因为协议调度得到了大量优化)
  • 您可以在协议中添加nil处理(通过extend-protocol

答案 4 :(得分:4)

如果没有完成像typed clojure那样令人兴奋的事情,我认为你不能避免运行时检查断言。

clojure提供的一个鲜为人知的特性肯定有助于运行时检查,它是条件前和条件的实现(参见http://clojure.org/special_formsa blog post by fogus)。我认为您甚至可以使用具有前置和后置条件的单个高阶包装函数来检查相关代码上的所有断言。这样可以很好地避免运行时检查“污染问题”。

答案 5 :(得分:2)

不,现在在clojure中没有这样的东西。虽然你可以实现它,但IMO这种类型似乎更适合静态类型语言,并且在动态环境(如clojure)中不会给你带来太多好处。

答案 6 :(得分:2)

使用带有标记的向量作为向量中的第一个元素,并使用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))

这比其他答案更简洁。

This youtube talk更详细地解释了为什么在地图上编码变体需要矢量。我的总结是使用地图编码变体是有问题的,因为你必须记住地图是一个标记的地图"不是常规地图。要使用带标记的地图"正确地,您必须始终执行两阶段查找:首先是标记,然后是基于标记的数据。如果(何时)您忘记在地图编码的变体中查找标记,或者对标记或数据的键查找错误,您将获得一个难以跟踪的空指针异常下来。

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

  • 诱捕非法标签。
  • 如果需要,使用Typed Clojure添加静态检查。
  • 将此数据存储在Datomic