在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,斯卡拉产生了一堆东西一样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的其他编译时不变的信息。(我们可以称其为受限类型的依赖类型吗?)
可以很好地利用这一点–一旦知道了使用什么数据构造函数创建值,就可以在其余流程中传播相应的类型,以增加类型安全性。例如,播放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
案例类,并手动提供数据构造器)。您可以看到Argonaut也存在我提到的Haskell编码问题。(除了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的编码错误。当然,它有时会阻碍类型推断,但是这似乎并不是一个足以使它错误的强大理由。我想念什么?