В 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>
Фактически, вы получаете дополнительный параметр времени компиляции относительно значения. (Можно ли назвать эту ограниченную версию зависимого набора?)
Это может быть полезно для использования. Как только вы знаете, какой конструктор данных использовался для создания значения, соответствующий тип может распространяться через остальную часть потока, чтобы добавить дополнительную безопасность типов. Например, Play JSON, который использует эту кодировку Scala, позволит вам извлечь 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
, вероятно, будет иметь тип JsValue -> Set (String, JsValue)
. Это означает, что он не сработает во время выполнения для JsArray
и т.д. Эта проблема также проявляется в виде хорошо известных аксессуаров для частичной записи.
То, что Scala обработка конструкторов данных неверна, неоднократно выражалась - на Twitter, списки рассылки, IRC, SO и т.д. К сожалению, у меня нет ссылок ни на один из этих, за исключением пары - этот ответ от Трэвиса Брауна и Argonaut, чисто функциональную библиотеку JSON для Scala.
Аргонавт сознательно использует подход Haskell (через private
классы case и предоставляет конструкторы данных вручную). Вы можете видеть, что проблема, которую я упомянул с кодировкой 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 неправильной. Уверен, что это иногда мешает типу вывода, но это не похоже на достаточно сильную причину, чтобы привести его в заблуждение. Что мне не хватает?