sas*_*nin 19 types clojure discriminated-union
编辑.我现在的问题是:在静态类型语言中,通常使用什么惯用的Clojure结构而不是sum类型?到目前为止的共识:如果行为可以统一使用协议,否则使用标记对/映射,在前后条件下放置必要的断言.
Clojure提供了许多表达产品类型的方法:矢量,地图,记录......,但是如何表示总和类型,也称为标记的联合和变体记录?像Either a bHaskell或Either[+A, +B]Scala中的东西.
我想到的第一件事就是带有特殊标签的地图:{:tag :left :value a}但是(:tag value)如果不存在条件,那么所有的代码都将被污染并处理特殊情况......我想要确保的是,那:tag是永远存在的,而且只能取指定的值中的一个,和对应的值是一致的同类型/行为,不能nil,有一个简单的方法,看看我注意到在代码中所有的情况下照顾.
我可以想到一个宏defrecord,但是对于总和类型:
; 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]
Run Code Online (Sandbox Code Playgroud)
这样的事情已经存在吗?(回答:不).
Eri*_*and 16
你如何表示总和类型,也称为标记的联合和变体记录?像
Either a bHaskell或Either[+A, +B]Scala中的东西.
Either 有两种用途:返回两种类型之一的值,或返回两个相同类型的值,这两个值应具有基于标记的不同语义.
第一次使用仅在使用静态类型系统时很重要.
Either在Haskell类型系统的约束下,基本上是可能的最小解决方案.使用动态类型系统,您可以返回所需类型的值.Either不需要.
第二种用途很重要,但可以通过两种(或多种)方式简单地完成:
{:tag :left :value 123} {:tag :right :value "hello"} {:left 123} {:right "hello"}我想要确保的是:标记始终存在,并且它只能占用一个指定的值,并且相应的值始终具有相同的类型/行为且不能为零,并且有一种简单的方法可以看到我在代码中处理了所有情况.
如果您想静态地确保这一点,Clojure可能不是您的语言.原因很简单:表达式直到运行时才有类型 - 直到它们返回一个值.
宏不起作用的原因是在宏扩展时,您没有运行时值 - 因此没有运行时类型.你有编译时的结构,如符号,原子,sexpressions等.你可以使用eval它们,但eval由于许多原因,使用被认为是不好的做法.
但是,我们可以在运行时做得很好.
我的策略是将通常是静态的(在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))))
Run Code Online (Sandbox Code Playgroud)
此代码使用以下惯用的Clojure构造:
如果您使用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!")))
Run Code Online (Sandbox Code Playgroud)
通常,动态类型语言中的和类型表示为:
在静态类型语言中,大多数值都按类型区分 - 这意味着您无需进行运行时标记分析即可知道您是否具有- Either或Maybe- 因此您只需查看标记即可知道它是a Left还是a Right.
在动态类型设置中,您必须首先进行运行时类型分析(以查看您拥有的值的类型),然后进行构造函数的案例分析(以查看您拥有的值的风格).
一种方法是为每种类型的每个构造函数分配一个唯一标记.
在某种程度上,您可以将动态类型视为将所有值放入单个和类型中,将所有类型分析推迟到运行时测试.
我想要确保的是:标记始终存在,并且它只能占用一个指定的值,并且相应的值始终具有相同的类型/行为且不能为零,并且有一种简单的方法可以看到我在代码中处理了所有情况.
顺便说一句,这几乎是对静态类型系统的描述.
使用带有标签的向量作为向量中的第一个元素,并使用 core.match 来解构标记的数据。因此,对于上面的示例,“任一”数据将被编码为:
[:left 123]
[:right "hello"]
Run Code Online (Sandbox Code Playgroud)
然后要解构,您需要引用core.match并使用:
(match either
[:left num-val] (do-something-to-num num-val)
[:right str-val] (do-something-to-str str-val))
Run Code Online (Sandbox Code Playgroud)
这比其他答案更简洁。
这个 youtube 演讲更详细地解释了为什么向量适合编码地图上的变体。我的总结是,使用地图对变体进行编码是有问题的,因为您必须记住地图是“标记地图”而不是常规地图。要正确使用“标记地图”,您必须始终执行两阶段查找:首先是标记,然后是基于标记的数据。如果(当)您忘记在地图编码变体中查找标签,或者对标签或数据的关键查找错误,您将得到一个很难追踪的空指针异常。
该视频还涵盖了矢量编码变体的以下方面:
这在某些语言中效果如此良好的原因是您对结果进行分派(通常按类型) - 即您使用结果的某些属性(通常是类型)来决定下一步做什么。
所以你需要看看在 clojure 中调度是如何发生的。
nil 特殊情况- 该nil值在各个地方都有特殊情况,可以用作“Maybe”的“None”部分。例如,if-let非常有用。
模式匹配- 除了解构序列之外,基础 clojure 对此没有太多支持,但有各种库可以支持。请参阅Clojure 替代 ADT 和模式匹配?[更新:在评论中 mnicky 说这已经过时了,你应该使用core.match ]
by type with OO - 方法按类型选择。因此您可以返回父类的不同子类并调用重载的方法来执行您想要的不同操作。如果你来自职能背景,会感觉很奇怪/笨拙,但这是一个选择。
手动标记- 最后,您可以使用case或cond与显式标记。更有用的是,您可以将它们包装在某种按您想要的方式工作的宏中。
作为一种动态类型语言,类型在 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
Run Code Online (Sandbox Code Playgroud)
我认为这将提供您可能希望从总和类型中获得的几乎所有好处。
这种方法的其他优点:
extend-protocol)