En Scala, les types de données algébriques sont codés sous forme de sealed
les hiérarchies de types à un niveau. Exemple :
-- 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]
Avec case class
es et case object
Scala génère un tas de choses comme equals
, hashCode
, unapply
(utilisé pour le filtrage), etc. qui nous apporte un grand nombre de propriétés et de caractéristiques clés des ADT traditionnelles.
Il y a cependant une différence essentielle - En Scala, les "constructeurs de données" ont leurs propres types. . Comparez les deux exemples suivants (copiés des REPLs respectifs).
// 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
J'ai toujours considéré que la variante Scala était du côté des avantages.
Après tout, il n'y a pas de perte d'informations sur le type . AppendIf[Int]
par exemple, est un sous-type de Positioning[Int]
.
scala> val subtypeProof = implicitly[AppendIf[Int] <:< Positioning[Int]]
subtypeProof: <:<[AppendIf[Int],Positioning[Int]] = <function1>
En fait, vous obtenez un invariant supplémentaire en temps de compilation concernant la valeur . (Pourrait-on appeler cela une version limitée du typage dépendant ?)
Cela peut être utilisé à bon escient - Une fois que vous savez quel constructeur de données a été utilisé pour créer une valeur, le type correspondant peut être propagé à travers le reste du flux pour ajouter plus de sécurité de type. Par exemple, Play JSON, qui utilise cet encodage Scala, vous permettra seulement d'extraire fields
de JsObject
et non d'un quelconque arbitraire 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])
En Haskell, fields
aurait probablement le type JsValue -> Set (String, JsValue)
. Ce qui signifie qu'il échouera au moment de l'exécution pour une JsArray
etc. Ce problème se manifeste également sous la forme des accesseurs d'enregistrements partiels bien connus.
L'opinion selon laquelle le traitement des constructeurs de données par Scala est erroné a été exprimée à de nombreuses reprises. - sur Twitter, les listes de diffusion, IRC, SO etc. Malheureusement, je n'ai pas de liens vers aucun d'entre eux, à l'exception de quelques-uns cette réponse par Travis Brown, et Argonaute une bibliothèque JSON purement fonctionnelle pour Scala.
Argonaute consciemment adopte l'approche Haskell (par private
les classes de cas, et la fourniture manuelle de constructeurs de données). Vous pouvez voir que le problème que j'ai mentionné avec l'encodage Haskell existe aussi avec Argonaut. (Sauf qu'il utilise Option
pour indiquer la partialité).
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
J'ai réfléchi à cette question pendant un certain temps, mais je ne comprends toujours pas pourquoi l'encodage de Scala est incorrect. Bien sûr, il entrave parfois l'inférence de type, mais cela ne semble pas être une raison suffisante pour le décréter mauvais. Qu'est-ce qui m'échappe ?