在 Scala 中,代数数据类型被编码为sealed
一级类型层次结构。例子:
-- Haskell
data Positioning a = Append
| AppendIf (a -> Bool)
| Explicit ([a] -> [a])
// Scala
sealed trait Positioning[A]
case object Append extends Positioning[Nothing]
case class AppendIf[A](condition: A => Boolean) extends Positioning[A]
case class Explicit[A](f: Seq[A] => Seq[A]) extends Positioning[A]
使用case class
es 和case object
s,Scala 生成了一堆东西,比如equals
, hashCode
, unapply
(用于模式匹配)等,它们为我们带来了传统 ADT 的许多关键属性和特性。
不过有一个关键区别——在 Scala 中,“数据构造函数”有自己的类型。例如比较以下两个(从相应的 REPL 复制)。
// Scala
scala> :t Append
Append.type
scala> :t AppendIf[Int](Function const true)
AppendIf[Int]
-- Haskell
haskell> :t Append
Append :: Positioning a
haskell> :t AppendIf (const True)
AppendIf (const True) :: Positioning a
我一直认为 Scala 变体是有利的。
毕竟,没有类型信息的丢失。AppendIf[Int]
例如 是 的子类型Positioning[Int]
。
scala> val subtypeProof = implicitly[AppendIf[Int] <:< Positioning[Int]]
subtypeProof: <:<[AppendIf[Int],Positioning[Int]] = <function1>
实际上,您获得了关于 value 的额外编译时不变量。(我们可以称之为依赖类型的受限版本吗?)
这可以很好地利用——一旦您知道使用什么数据构造函数来创建一个值,相应的类型就可以通过流程的其余部分传播,以增加更多的类型安全性。例如,使用这种 Scala 编码的 Play JSON 将只允许您从 中提取fields
,JsObject
而不是从任意任意JsValue
.
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val obj = Json.obj("key" -> 3)
obj: play.api.libs.json.JsObject = {"key":3}
scala> obj.fields
res0: Seq[(String, play.api.libs.json.JsValue)] = ArrayBuffer((key,3))
scala> val arr = Json.arr(3, 4)
arr: play.api.libs.json.JsArray = [3,4]
scala> arr.fields
<console>:15: error: value fields is not a member of play.api.libs.json.JsArray
arr.fields
^
scala> val jsons = Set(obj, arr)
jsons: scala.collection.immutable.Set[Product with Serializable with play.api.libs.json.JsValue] = Set({"key":3}, [3,4])
在 Haskell 中,fields
可能会有 type JsValue -> Set (String, JsValue)
。这意味着它将在运行时失败JsArray
等。这个问题也以众所周知的部分记录访问器的形式表现出来。
Scala 对数据构造函数的处理是错误的观点已经被多次表达过——在 Twitter、邮件列表、IRC、SO 等上。不幸的是,除了一对之外,我没有任何链接——Travis Brown 的这个回答,和Argonaut,一个用于 Scala 的纯函数式 JSON 库。
Argonaut有意识地采用 Haskell 方法(通过private
案例类,并手动提供数据构造函数)。您可以看到我提到的有关 Haskell 编码的问题也存在于 Argonaut 中。(除了它Option
用来表示偏心。)
scala> import argonaut._, Argonaut._
import argonaut._
import Argonaut._
scala> val obj = Json.obj("k" := 3)
obj: argonaut.Json = {"k":3}
scala> obj.obj.map(_.toList)
res6: Option[List[(argonaut.Json.JsonField, argonaut.Json)]] = Some(List((k,3)))
scala> val arr = Json.array(jNumber(3), jNumber(4))
arr: argonaut.Json = [3,4]
scala> arr.obj.map(_.toList)
res7: Option[List[(argonaut.Json.JsonField, argonaut.Json)]] = None
我一直在思考这个问题,但仍然不明白是什么让 Scala 的编码错误。当然,它有时会妨碍类型推断,但这似乎不是一个足够有力的理由来判定它是错误的。我错过了什么?