Scalaでは、代数的データ型はsealed
1レベルの型階層としてエンコードされます。例:
_-- 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。
ただし、重要な違いが1つあります–Scalaでは、「データコンストラクター」には独自のタイプがあります。たとえば、次の2つを比較します(それぞれの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>
_
実際、値に関して不変の追加のコンパイル時間が得られます。 (これを依存型の限定バージョンと呼ぶことができますか?)
これは有効に活用できます–値の作成に使用されたデータコンストラクターがわかれば、対応する型をフローの残りの部分に伝播して、型の安全性を高めることができます。たとえば、このScalaエンコーディングを使用するPlayJSONでは、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など残念ながら、Travis Brownによる この回答 とScala用の純粋に機能的なJSONライブラリである Argonaut を除いて、これらのいずれにもリンクはありません。
Argonaut 意識的に はHaskellアプローチを採用しています(ケースクラスをprivate
ingし、データコンストラクターを手動で提供することにより)。 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のエンコーディングが間違っている理由をまだ理解していません。確かにそれは時々型推論を妨げるが、それはそれを間違って宣言する十分な理由のようには思えない。何が足りないのですか?
私の知る限り、Scalaのケースクラスの慣用的なエンコーディングが悪い理由は2つあります。それは、型推論と型の特異性です。前者は構文上の利便性の問題であり、後者は推論の範囲の拡大の問題です。
サブタイプの問題は、比較的簡単に説明できます。
val x = Some(42)
x
のタイプはSome[Int]
であることが判明しましたが、これはおそらくあなたが望んでいたものではありません。他のより問題のある領域でも同様の問題が発生する可能性があります。
sealed trait ADT
case class Case1(x: Int) extends ADT
case class Case2(x: String) extends ADT
val xs = List(Case1(42), Case1(12))
xs
のタイプはList[Case1]
です。これは基本的に保証あなたが望むものではないことです。この問題を回避するには、List
のようなコンテナーのtypeパラメーターが共変である必要があります。残念ながら、共分散は問題のバケツ全体をもたらし、実際には特定の構成の健全性を低下させます(たとえば、Scalazは、共分散コンテナーを許可することにより、そのMonad
型といくつかのモナド変換子を妥協します。そう)。
したがって、この方法でADTをエンコードすると、コードに多少のウイルス効果があります。 ADT自体でサブタイプを処理する必要があるだけでなく、everyコンテナーを作成する場合は、不適切なタイミングでADTのサブタイプに到達しているという事実を考慮する必要があります。
パブリックケースクラスを使用してADTをエンコードしない2つ目の理由は、タイプスペースが「非タイプ」で乱雑にならないようにするためです。特定の観点から、ADTケースは実際にはタイプではなく、データです。この方法でADTについて推論する場合(これは間違いではありません!)、ADTケースごとにファーストクラスの型を使用すると、コードについて推論するために頭に入れておく必要のある一連の事項が増えます。
たとえば、上からのADT
代数について考えてみます。このADTを使用するコードについて推論したい場合は、「このタイプがCase1
の場合はどうなるか」について、常に考える必要があります。 Case1
はデータであるため、これは誰もが実際に尋ねる質問ではありませんneeds。これは、特定の副産物のケースのタグです。それで全部です。
個人的には、上記のことはあまり気にしません。つまり、共分散に関する不健全性の問題は現実のものですが、私は通常、コンテナーを不変にし、ユーザーに「それを吸い上げてタイプに注釈を付ける」ように指示することを好みます。それは不便でばかげていますが、多くの定型的な折り畳みと「小文字の」データコンストラクターである代替案よりも好ましいと思います。
ワイルドカードとして、この種の型の特異性に対する3番目の潜在的な欠点は、個々のADT型にケース固有の関数を配置するより「オブジェクト指向」のスタイルを促進する(というよりは許可する)ことです。このようにメタファー(ケースクラスとサブタイプポリモーフィズム)を混合することが悪いレシピであることに疑問の余地はほとんどないと思います。ただし、この結果が型指定されたケースのせいであるかどうかは、一種の未解決の問題です。