Comment définir la «disjonction de type» (types d'union)?


181

Une façon qui a été suggérée pour traiter les doubles définitions des méthodes surchargées est de remplacer la surcharge par la correspondance de modèles:

object Bar {
   def foo(xs: Any*) = xs foreach { 
      case _:String => println("str")
      case _:Int => println("int")
      case _ => throw new UglyRuntimeException()
   }
}

Cette approche nécessite que nous abandonnions la vérification de type statique sur les arguments à foo. Ce serait beaucoup plus agréable de pouvoir écrire

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case _: String => println("str")
      case _: Int => println("int")
   }
}

Je peux me rapprocher Either, mais ça devient vite moche avec plus de deux types:

type or[L,R] = Either[L,R]

implicit def l2Or[L,R](l: L): L or R = Left(l)
implicit def r2Or[L,R](r: R): L or R = Right(r)

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case Left(l) => println("str")
      case Right(r) => println("int")
   }
}

Il ressemble à une solution générale (élégante, efficace) , il faudrait définir Either3, Either4.... Est-ce que quelqu'un sait d'une solution de rechange pour atteindre le même but? A ma connaissance, Scala n'a pas de "disjonction de type" intégrée. De plus, les conversions implicites définies ci-dessus se cachent-elles quelque part dans la bibliothèque standard afin que je puisse simplement les importer?

Réponses:


142

Eh bien, dans le cas spécifique de Any*, cette astuce ci-dessous ne fonctionnera pas, car elle n'acceptera pas les types mixtes. Cependant, comme les types mixtes ne fonctionneraient pas non plus avec la surcharge, c'est peut-être ce que vous voulez.

Tout d'abord, déclarez une classe avec les types que vous souhaitez accepter comme ci-dessous:

class StringOrInt[T]
object StringOrInt {
  implicit object IntWitness extends StringOrInt[Int]
  implicit object StringWitness extends StringOrInt[String]
}

Ensuite, déclarez foocomme ceci:

object Bar {
  def foo[T: StringOrInt](x: T) = x match {
    case _: String => println("str")
    case _: Int => println("int")
  }
}

Et c'est tout. Vous pouvez appeler foo(5)ou foo("abc"), et cela fonctionnera, mais essayez foo(true)et cela échouera. Cela pourrait être contourné par le code client en créant un StringOrInt[Boolean], sauf si, comme indiqué par Randall ci-dessous, vous faites StringOrIntunsealed classe.

Cela fonctionne parce que cela T: StringOrIntsignifie qu'il y a un paramètre implicite de type StringOrInt[T], et parce que Scala regarde à l'intérieur des objets compagnons d'un type pour voir s'il y a des implicites pour que le code demandant ce type fonctionne.


14
Si class StringOrInt[T]est fait sealed, la "fuite" à laquelle vous faites référence ("Bien sûr, cela pourrait être contourné par le code client en créant un StringOrInt[Boolean]") est bouchée, du moins si elle StringOrIntréside dans un fichier qui lui est propre. Ensuite, les objets témoins doivent être définis dans la même source que StringOrInt.
Randall Schulz

3
J'ai essayé de généraliser quelque peu cette solution (publiée comme réponse ci-dessous). Le principal inconvénient par rapport à l' Eitherapproche semble être que nous perdons beaucoup de support du compilateur pour vérifier la correspondance.
Aaron Novstrup

Joli tour! Cependant, même avec la classe scellée, vous pouvez toujours la contourner dans le code client soit en définissant un val implicite b = new StringOrInt [Boolean] dans la portée avec foo, soit en appelant explicitement foo (2.9) (new StringOrInt [Double]). Je pense que vous devez également rendre la classe abstraite.
Paolo Falabella

2
Oui; il serait probablement préférable d'utilisertrait StringOrInt ...
Escargot mécanique

7
Ps si vous souhaitez prendre en charge les sous-types, passez simplement StringOrInt[T]à StringOrInt[-T](voir stackoverflow.com/questions/24387701/… )
Eran Medan

178

Miles Sabin décrit une très belle façon d'obtenir le type d'union dans son récent article de blog Les types d'union Unboxed dans Scala via l'isomorphisme Curry-Howard :

Il définit d'abord la négation des types comme

type ¬[A] = A => Nothing

en utilisant la loi de De Morgan, cela lui permet de définir les types d'union

type[T, U] = ¬[¬[T] with ¬[U]]

Avec les constructions auxiliaires suivantes

type ¬¬[A] = ¬[¬[A]]
type ||[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

vous pouvez écrire des types d'union comme suit:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}

13
C'est l'une des choses les plus impressionnantes que j'ai vues.
Submonoïde


6
Le commentaire ci-dessus devrait être une réponse en soi. C'est juste une implémentation de l'idée de Miles, mais bien enveloppée dans un package sur Maven Central, et sans tous ces symboles Unicode qui pourraient (?) Poser un problème pour quelque chose dans un processus de construction quelque part.
Jim Pivarski

2
Ce drôle de personnage est la négation booléenne .
michid

1
Au départ, l'idée me paraissait beaucoup trop compliquée. En lisant presque tous les liens mentionnés dans ce fil, j'ai été surpris par l'idée et par la beauté de sa mise en œuvre :-) ... mais j'ai toujours l'impression que c'est quelque chose de compliqué ... maintenant juste parce qu'il n'est pas encore disponible directement loin de Scala. Comme le dit Miles: "Il ne nous reste plus qu'à harceler Martin et Adriaan pour le rendre directement accessible."
Richard Gomes

44

Dotty , un nouveau compilateur Scala expérimental, prend en charge les types d'union (écrits A | B), vous pouvez donc faire exactement ce que vous vouliez:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}

1
Un de ces jours.
Michael Ahlers

5
Au fait, Dotty sera le nouveau scala 3 (annoncé il y a quelques mois).
6infinity8

1
et sera disponible quelque part fin 2020
JulienD

31

Voici la manière Rex Kerr d'encoder les types d'union. Hétéro et simple!

scala> def f[A](a: A)(implicit ev: (Int with String) <:< A) = a match {
     |   case i: Int => i + 1
     |   case s: String => s.length
     | }
f: [A](a: A)(implicit ev: <:<[Int with String,A])Int

scala> f(3)
res0: Int = 4

scala> f("hello")
res1: Int = 5

scala> f(9.2)
<console>:9: error: Cannot prove that Int with String <:< Double.
       f(9.2)
        ^

Source: Commentaire n ° 27 sous cet excellent article de blog de Miles Sabin qui fournit une autre façon d'encoder les types d'union dans Scala.


6
Malheureusement, cet encodage peut être vaincu: scala> f(9.2: AnyVal)passe le vérificateur de type.
Kipton Barros

@Kipton: C'est triste. L'encodage de Miles Sabin souffre-t-il également de ce problème?
missingfaktor

9
Il existe une version légèrement plus simple du code de Miles; comme il utilise en fait l'implication inverse du paramètre contravariant de la fonction, et non un strict «non», vous pouvez utiliser trait Contra[-A] {}à la place de toutes les fonctions à rien. Donc, vous obtenez des trucs comme type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }utilisés comme def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(sans unicode sophistiqué).
Rex Kerr

Cela pourrait résoudre le problème d'héritage des types d'union? stackoverflow.com/questions/45255270/…
jhegedus

Hmm, je l'ai essayé, je ne peux pas créer de types de retour avec ces encodages, il ne semble donc pas possible d'implémenter le sous-typage stackoverflow.com/questions/45255270
...

18

Il est possible de généraliser la solution de Daniel comme suit:

sealed trait Or[A, B]

object Or {
   implicit def a2Or[A,B](a: A) = new Or[A, B] {}
   implicit def b2Or[A,B](b: B) = new Or[A, B] {}
}

object Bar {
   def foo[T <% String Or Int](x: T) = x match {
     case _: String => println("str")
     case _: Int => println("int")
   }
}

Les principaux inconvénients de cette approche sont

  • Comme Daniel l'a souligné, il ne gère pas les collections / varargs avec des types mixtes
  • Le compilateur n'émet pas d'avertissement si la correspondance n'est pas exhaustive
  • Le compilateur n'émet pas d'erreur si la correspondance inclut un cas impossible
  • Comme l' Eitherapproche, la généralisation plus , il faudrait définir analogue Or3, Or4etc. traits. Bien entendu, définir de tels traits serait beaucoup plus simple que définir les Eitherclasses correspondantes .

Mettre à jour:

Mitch Blevins démontre une approche très similaire et montre comment la généraliser à plus de deux types, en la surnommant le «bégaiement ou».


18

Je suis en quelque sorte tombé sur une implémentation relativement propre des types d'union n-aire en combinant la notion de listes de types avec une simplification du travail de Miles Sabin dans ce domaine , que quelqu'un mentionne dans une autre réponse.

Étant donné le type ¬[-A]qui est contravariant sur A, par définition étant donné que A <: Bnous pouvons écrire ¬[B] <: ¬[A], inverser l'ordre des types.

Étant donné les types A, Bet X, nous voulons exprimer X <: A || X <: B. En appliquant la contravariance, nous obtenons ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Cela peut à son tour être exprimé comme ¬[A] with ¬[B] <: ¬[X]dans lequel l'un de Aou Bdoit être un supertype de Xou Xlui - même (pensez aux arguments de fonction).

object Union {
  import scala.language.higherKinds

  sealed trait ¬[-A]

  sealed trait TSet {
    type Compound[A]
    type Map[F[_]] <: TSet
  }

  sealed traitextends TSet {
    type Compound[A] = A
    type Map[F[_]] =}

  // Note that this type is left-associative for the sake of concision.
  sealed trait[T <: TSet, H] extends TSet {
    // Given a type of the form `∅ ∨ A ∨ B ∨ ...` and parameter `X`, we want to produce the type
    // `¬[A] with ¬[B] with ... <:< ¬[X]`.
    type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

    // This could be generalized as a fold, but for concision we leave it as is.
    type Compound[A] = T#Compound[H with A]

    type Map[F[_]] = T#Map[F] ∨ F[H]
  }

  def foo[A : (∅ ∨ StringIntList[Int])#Member](a: A): String = a match {
    case s: String => "String"
    case i: Int => "Int"
    case l: List[_] => "List[Int]"
  }

  foo(42)
  foo("bar")
  foo(List(1, 2, 3))
  foo(42d) // error
  foo[Any](???) // error
}

J'ai passé un certain temps à essayer de combiner cette idée avec une limite supérieure sur les types de membres comme on le voit dans le TLists de harrah / up , mais la mise en œuvre de Mapwith type bounds s'est jusqu'à présent avérée difficile.


1
C'est génial, merci! J'ai essayé les approches précédentes, mais j'ai continué à avoir des problèmes en utilisant cela avec des types génériques dans le cadre de l'union. C'était la seule implémentation que je pouvais obtenir avec des types génériques.
Samer Adra

Malheureusement, mais probablement à prévoir, lorsque j'essaie d'utiliser une méthode Scala qui prend un type d'union à partir du code Java, cela ne fonctionne pas. Erreur: (40, 29) java: la méthode setValue dans la classe Config ne peut pas être appliquée à des types donnés; requis: X, scala.Predef. $ less $ deux-points $ moins <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> trouvé: java.lang.String raison: impossible de déduire type-variable (s) X (les listes d'arguments réels et formels diffèrent par la longueur)
Samer Adra

Pas encore tout à fait clair sur certains des détails de cette implémentation. Par exemple, l'article original définissait la négation comme "type ¬ [A] = A => Rien" mais dans cette version si juste a "trait scellé ¬ [-A]" et le trait n'est étendu nulle part. Comment cela marche-t-il?
Samer Adra

@Samer Adra Cela fonctionnerait de toute façon, l'article utilise Function1comme type contravariant existant. Vous n'avez pas besoin d'une implémentation, tout ce dont vous avez besoin est une preuve de conformité ( <:<).
J Cracknell

Une idée comment avoir un constructeur qui accepte un type d'union?
Samer Adra

13

Une solution de classe de type est probablement la meilleure façon de procéder ici, en utilisant des implicits. Ceci est similaire à l'approche monoïde mentionnée dans le livre Odersky / Spoon / Venners:

abstract class NameOf[T] {
  def get : String
}

implicit object NameOfStr extends NameOf[String] {
  def get = "str"
}

implicit object NameOfInt extends NameOf[Int] {
 def get = "int"
}

def printNameOf[T](t:T)(implicit name : NameOf[T]) = println(name.get)

Si vous exécutez ensuite ceci dans le REPL:

scala> printNameOf(1)
int

scala> printNameOf("sss")
str

scala> printNameOf(2.0f)
<console>:10: error: could not find implicit value for parameter nameOf: NameOf[
Float]
       printNameOf(2.0f)

              ^

Je peux me tromper, mais je ne pense pas que ce soit ce que le PO recherchait. OP posait des questions sur un type de données qui pourrait représenter une union disjointe de types, puis faire une analyse de cas dessus au moment de l' exécution pour voir ce que le type réel s'est avéré être. Les classes de types ne résoudront pas ce problème, car elles sont une construction purement à la compilation.
Tom Crockett

5
La vraie question posée était de savoir comment exposer différents comportements pour différents types, mais sans surcharge. Sans connaissance des classes de types (et peut-être une certaine exposition à C / C ++), un type union semble être la seule solution. Le Eithertype préexistant de Scala tend à renforcer cette croyance. Utiliser des classes de types via les implicits de Scala est une meilleure solution au problème sous-jacent, mais c'est un concept relativement nouveau et encore peu connu, c'est pourquoi l'OP ne savait même pas les considérer comme une alternative possible à un type d'union.
Kevin Wright

cela fonctionne-t-il avec le sous-typage? stackoverflow.com/questions/45255270/…
jhegedus

10

Nous aimerions un opérateur de type Or[U,V]qui puisse être utilisé pour contraindre un paramètre de type Xde telle sorte que soit X <: Uou X <: V. Voici une définition qui se rapproche le plus possible:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Voici comment il est utilisé:

// use

class A; class B extends A; class C extends B

def foo[X : (B Or String)#pf] = {}

foo[B]      // OK
foo[C]      // OK
foo[String] // OK
foo[A]      // ERROR!
foo[Number] // ERROR!

Cela utilise quelques astuces de type Scala. Le principal est l'utilisation de contraintes de type généralisées . Types donnésU et V, le compilateur Scala fournit une classe appelée U <:< V(et un objet implicite de cette classe) si et seulement si le compilateur Scala peut prouver qu'il Us'agit d'un sous-type de V. Voici un exemple plus simple utilisant des contraintes de type généralisées qui fonctionne dans certains cas:

def foo[X](implicit ev : (B with String) <:< X) = {}

Cet exemple fonctionne lorsqu'une Xinstance de classe B, a Stringou a un type qui n'est ni un supertype ni un sous-type de BouString . Dans les deux premiers cas, c'est vrai par la définition du withmot-clé que (B with String) <: Bet (B with String) <: String, donc Scala fournira un objet implicite qui sera passé en tant que ev: le compilateur Scala acceptera correctement foo[B]et foo[String].

Dans le dernier cas, je me fie au fait que si U with V <: X, alors U <: Xou V <: X. Cela semble intuitivement vrai, et je le suppose simplement. Il ressort clairement de cette hypothèse pourquoi cet exemple simple échoue quand Xest un supertype ou un sous-type de l'un Bou l' autre ou String: par exemple, dans l'exemple ci-dessus, foo[A]est incorrectement accepté etfoo[C] est incorrectement rejeté. Encore une fois, ce que nous voulons est une sorte de type expression sur les variables U, Vet Xqui est vrai exactement quand X <: UouX <: V .

La notion de contravariance de Scala peut aider ici. Vous vous souvenez du trait trait Inv[-X]? Parce qu'il est contravariant dans son paramètre de typeX , Inv[X] <: Inv[Y]si et seulement si Y <: X. Cela signifie que nous pouvons remplacer l'exemple ci-dessus par un exemple qui fonctionnera réellement:

trait Inv[-X]
def foo[X](implicit ev : (Inv[B] with Inv[String]) <:< Inv[X]) = {}

C'est parce que l'expression (Inv[U] with Inv[V]) <: Inv[X]est vraie, par la même hypothèse ci-dessus, exactement quandInv[U] <: Inv[X] ou Inv[V] <: Inv[X], et par la définition de contravariance, cela est vrai exactement quand X <: Uou X <: V.

Il est possible de rendre les choses un peu plus réutilisables en déclarant un type paramétrable BOrString[X] et en l'utilisant comme suit:

trait Inv[-X]
type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
def foo[X](implicit ev : BOrString[X]) = {}

Scala tentera maintenant de construire le type BOrString[X]pour tout ce Xqui fooest appelé avec, et le type sera construit précisément quand Xest un sous-type de l'un Bou l' autre String. Cela fonctionne, et il existe une notation abrégée. La syntaxe ci-dessous est équivalente (sauf qu'elle evdoit maintenant être référencée dans le corps de la méthode comme implicitly[BOrString[X]]plutôt que simplement ev) et utiliseBOrString comme type lié au contexte :

def foo[X : BOrString] = {}

Ce que nous aimerions vraiment, c'est une manière flexible de créer un type lié au contexte. Un contexte de type doit être un type paramétrable, et nous voulons un moyen paramétrable pour en créer un. On dirait que nous essayons de curry des fonctions sur des types tout comme nous curry des fonctions sur des valeurs. En d'autres termes, nous aimerions quelque chose comme ce qui suit:

type Or[U,T][X] = (Inv[U] with Inv[T]) <:< Inv[X]

Ce n'est pas directement possible dans Scala, mais il existe une astuce que nous pouvons utiliser pour nous en approcher. Cela nous amène à la définition deOr ci-dessus:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Ici, nous utilisons le typage structurel et l' opérateur dièse de Scala pour créer un type structurel Or[U,T]qui est garanti d'avoir un type interne. C'est une étrange bête. Pour donner un peu de contexte, la fonction def bar[X <: { type Y = Int }](x : X) = {}doit être appelée avec des sous-classes de AnyRefqui ont un type Ydéfini en eux:

bar(new AnyRef{ type Y = Int }) // works!

L'utilisation de l'opérateur dièse nous permet de faire référence au type interne Or[B, String]#pf, et en utilisant la notation infixe pour l'opérateur de type Or, nous arrivons à notre définition originale de foo:

def foo[X : (B Or String)#pf] = {}

Nous pouvons utiliser le fait que les types de fonction sont contravariants dans leur premier paramètre de type afin d'éviter de définir le trait Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 

Cela peut-il résoudre le A|B <: A|B|Cproblème? stackoverflow.com/questions/45255270/ ... Je ne peux pas dire.
jhegedus


7

Vous pourriez jeter un oeil à MetaScala , qui a quelque chose appelé OneOf. J'ai l'impression que cela ne fonctionne pas bien avec les matchinstructions mais que vous pouvez simuler la correspondance en utilisant des fonctions d'ordre supérieur. Jetez un œil à cet extrait de code , par exemple, mais notez que la partie "correspondance simulée" est commentée, peut-être parce qu'elle ne fonctionne pas encore tout à fait.

Passons maintenant à un éditorial: je ne pense pas qu'il y ait quoi que ce soit d'extraordinaire à définir Either3, Either4, etc. comme vous le décrivez. Il s'agit essentiellement du double des 22 types de tuple standard intégrés à Scala. Ce serait certainement bien si Scala avait des types disjonctifs intégrés, et peut-être une syntaxe intéressante pour eux comme {x, y, z}.


6

Je pense que le type disjoint de première classe est un supertype scellé, avec les sous-types alternatifs, et des conversions implicites vers / depuis les types souhaités de disjonction vers ces sous-types alternatifs.

Je suppose que cela répond aux commentaires 33 - 36 de la solution de Miles Sabin, donc le premier type de classe qui peut être utilisé sur le site d'utilisation, mais je ne l'ai pas testé.

sealed trait IntOrString
case class IntOfIntOrString( v:Int ) extends IntOrString
case class StringOfIntOrString( v:String ) extends IntOrString
implicit def IntToIntOfIntOrString( v:Int ) = new IntOfIntOrString(v)
implicit def StringToStringOfIntOrString( v:String ) = new StringOfIntOrString(v)

object Int {
   def unapply( t : IntOrString ) : Option[Int] = t match {
      case v : IntOfIntOrString => Some( v.v )
      case _ => None
   }
}

object String {
   def unapply( t : IntOrString ) : Option[String] = t match {
      case v : StringOfIntOrString => Some( v.v )
      case _ => None
   }
}

def size( t : IntOrString ) = t match {
    case Int(i) => i
    case String(s) => s.length
}

scala> size("test")
res0: Int = 4
scala> size(2)
res1: Int = 2

Un problème est que Scala n'emploiera pas dans le contexte de correspondance de cas, une conversion implicite de IntOfIntOrStringvers Int(et StringOfIntOrStringvers String), donc doit définir des extracteurs et utiliser à la case Int(i)place de case i : Int.


AJOUTER: J'ai répondu à Miles Sabin sur son blog comme suit. Peut-être y a-t-il plusieurs améliorations sur Soit:

  1. Il s'étend à plus de 2 types, sans aucun bruit supplémentaire sur le site d'utilisation ou de définition.
  2. Les arguments sont encadrés implicitement, par exemple n'ont pas besoin size(Left(2))ou size(Right("test")).
  3. La syntaxe de la correspondance de modèle est implicitement non boxée.
  4. Le boxing et le déballage peuvent être optimisés par le hotspot JVM.
  5. La syntaxe pourrait être celle adoptée par un futur type d'union de première classe, de sorte que la migration pourrait peut-être être transparente? Peut-être que pour le nom du type d'union, il serait préférable d'utiliser Vau lieu de Or, par exemple IntVString, ` Int |v| String`, ` Int or String` ou mon favori ` Int|String`?

MISE À JOUR: La négation logique de la disjonction pour le modèle ci-dessus suit, et j'ai ajouté un modèle alternatif (et probablement plus utile) sur le blog de Miles Sabin .

sealed trait `Int or String`
sealed trait `not an Int or String`
sealed trait `Int|String`[T,E]
case class `IntOf(Int|String)`( v:Int ) extends `Int|String`[Int,`Int or String`]
case class `StringOf(Int|String)`( v:String ) extends `Int|String`[String,`Int or String`]
case class `NotAn(Int|String)`[T]( v:T ) extends `Int|String`[T,`not an Int or String`]
implicit def `IntTo(IntOf(Int|String))`( v:Int ) = new `IntOf(Int|String)`(v)
implicit def `StringTo(StringOf(Int|String))`( v:String ) = new `StringOf(Int|String)`(v)
implicit def `AnyTo(NotAn(Int|String))`[T]( v:T ) = new `NotAn(Int|String)`[T](v)
def disjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `Int or String`) = x
def negationOfDisjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `not an Int or String`) = x

scala> disjunction(5)
res0: Int|String[Int,Int or String] = IntOf(Int|String)(5)

scala> disjunction("")
res1: Int|String[String,Int or String] = StringOf(Int|String)()

scala> disjunction(5.0)
error: could not find implicit value for parameter ev: =:=[not an Int or String,Int or String]
       disjunction(5.0)
                  ^

scala> negationOfDisjunction(5)
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction(5)
                            ^

scala> negationOfDisjunction("")
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction("")
                            ^
scala> negationOfDisjunction(5.0)
res5: Int|String[Double,not an Int or String] = NotAn(Int|String)(5.0)

UNE AUTRE MISE À JOUR: Concernant les commentaires 23 et 35 de la solution de Mile Sabin , voici un moyen de déclarer un type d'union sur le site d'utilisation. Notez qu'il est déballé après le premier niveau, c'est à dire qu'il a l'avantage d'être extensible à n'importe quel nombre de types dans la disjonction , alors que les Eitherbesoins de boxe imbriquée et le paradigme dans mon commentaire précédent 41 n'était pas extensible. En d'autres termes, a D[Int ∨ String]est attribuable à (c'est-à-dire est un sous-type de) a D[Int ∨ String ∨ Double].

type ¬[A] = (() => A) => A
type[T, U] = ¬[T] with ¬[U]
class D[-A](v: A) {
  def get[T](f: (() => T)) = v match {
    case x : ¬[T] => x(f)
  }
}
def size(t: D[IntString]) = t match {
  case x: D[¬[Int]] => x.get( () => 0 )
  case x: D[¬[String]] => x.get( () => "" )
  case x: D[¬[Double]] => x.get( () => 0.0 )
}
implicit def neg[A](x: A) = new D[¬[A]]( (f: (() => A)) => x )

scala> size(5)
res0: Any = 5

scala> size("")
error: type mismatch;
 found   : java.lang.String("")
 required: D[?[Int,String]]
       size("")
            ^

scala> size("hi" : D[¬[String]])
res2: Any = hi

scala> size(5.0 : D[¬[Double]])
error: type mismatch;
 found   : D[(() => Double) => Double]
 required: D[?[Int,String]]
       size(5.0 : D[?[Double]])
                ^

Apparemment, le compilateur Scala a trois bogues.

  1. Il ne choisira pas la fonction implicite correcte pour aucun type après le premier type dans la disjonction de destination.
  2. Cela n'exclut pas le D[¬[Double]]cas du match.

3.

scala> class D[-A](v: A) {
  def get[T](f: (() => T))(implicit e: A <:< ¬[T]) = v match {
    case x : ¬[T] => x(f)
  }
}
error: contravariant type A occurs in covariant position in
       type <:<[A,(() => T) => T] of value e
         def get[T](f: (() => T))(implicit e: A <:< ?[T]) = v match {
                                           ^

La méthode get n'est pas contrainte correctement sur le type d'entrée, car le compilateur n'autorisera pas Ala position covariante. On pourrait dire que c'est un bogue car tout ce que nous voulons, ce sont des preuves, nous n'accédons jamais aux preuves dans la fonction. Et j'ai fait le choix de ne pas tester case _dans la getméthode, donc je n'aurais pas à déballer un Optionin the matchin size().


5 mars 2012: la mise à jour précédente doit être améliorée. La solution de Miles Sabin fonctionnait correctement avec le sous-typage.

type ¬[A] = A => Nothing
type[T, U] = ¬[T] with ¬[U]
class Super
class Sub extends Super

scala> implicitly[(SuperString) <:< ¬[Super]]
res0: <:<[?[Super,String],(Super) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Sub]]
res2: <:<[?[Super,String],(Sub) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Any]]
error: could not find implicit value for parameter
       e: <:<[?[Super,String],(Any) => Nothing]
       implicitly[(Super ? String) <:< ?[Any]]
                 ^

La proposition de ma mise à jour précédente (pour un type d'union proche de première classe) a cassé le sous-typage.

 scala> implicitly[D[¬[Sub]] <:< D[(SuperString)]]
error: could not find implicit value for parameter
       e: <:<[D[(() => Sub) => Sub],D[?[Super,String]]]
       implicitly[D[?[Sub]] <:< D[(Super ? String)]]
                 ^

Le problème est que Adans (() => A) => Aapparaît à la fois dans les positions covariante (type de retour) et contravariante (entrée de fonction, ou dans ce cas une valeur de retour de fonction qui est une entrée de fonction), donc les substitutions ne peuvent être invariantes.

Notez que cela A => Nothingn'est nécessaire que parce que nous voulons être Aen position contravariante, de sorte que les supertypes de A ne sont pas des sous - types de D[¬[A]]ni D[¬[A] with ¬[U]]( voir aussi ). Comme nous n'avons besoin que d'une double contravariance, nous pouvons obtenir l'équivalent de la solution de Miles même si nous pouvons rejeter le ¬et .

trait D[-A]

scala> implicitly[D[D[Super]] <:< D[D[Super] with D[String]]]
res0: <:<[D[D[Super]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Sub]] <:< D[D[Super] with D[String]]]
res1: <:<[D[D[Sub]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
error: could not find implicit value for parameter
       e: <:<[D[D[Any]],D[D[Super] with D[String]]]
       implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
                 ^

Le correctif complet est donc.

class D[-A] (v: A) {
  def get[T <: A] = v match {
    case x: T => x
  }
}

implicit def neg[A](x: A) = new D[D[A]]( new D[A](x) )

def size(t: D[D[Int] with D[String]]) = t match {
  case x: D[D[Int]] => x.get[D[Int]].get[Int]
  case x: D[D[String]] => x.get[D[String]].get[String]
  case x: D[D[Double]] => x.get[D[Double]].get[Double]
}

Notez que les 2 bogues précédents dans Scala restent, mais le troisième est évité car il Test maintenant contraint d'être le sous-type de A.

Nous pouvons confirmer les travaux de sous-typage.

def size(t: D[D[Super] with D[String]]) = t match {
  case x: D[D[Super]] => x.get[D[Super]].get[Super]
  case x: D[D[String]] => x.get[D[String]].get[String]
}

scala> size( new Super )
res7: Any = Super@1272e52

scala> size( new Sub )
res8: Any = Sub@1d941d7

J'ai pensé que les types d'intersection de première classe sont très importants, à la fois pour les raisons pour lesquelles Ceylan les a , et parce qu'au lieu de subsumer à Anyce qui signifie que le déballage avec un matchtype attendu peut générer une erreur d'exécution, le déballage d'une ( collection hétérogène contenant a) la disjonction peut être vérifiée (Scala doit corriger les bogues que j'ai notés). Les unions sont plus simples que la complexité de l'utilisation de la HList expérimentale de metascala pour des collections hétérogènes.


L'élément n ° 3 ci-dessus n'est pas un bogue dans le compilateur Scala . Notez que je ne l'avais pas numéroté à l'origine comme un bogue, puis j'ai fait une modification négligemment aujourd'hui et je l'ai fait (en oubliant ma raison d'origine pour ne pas dire que c'était un bogue). Je n'ai plus modifié le message, car je suis à la limite de 7 modifications.
Shelby Moore III

Le bogue n ° 1 ci-dessus peut être évité avec une formulation différente de la sizefonction .
Shelby Moore III

L'élément n ° 2 n'est pas un bug. Scala ne peut pas exprimer pleinement un type d'union . Le document lié fournit une autre version du code, de sorte qu'il sizen'accepte plus D[Any]comme entrée.
Shelby Moore III

Je ne comprends pas tout à fait cette réponse, est-ce aussi une réponse à cette question: stackoverflow.com/questions/45255270/…
jhegedus

5

Il existe une autre manière qui est un peu plus facile à comprendre si vous ne grokez pas Curry-Howard:

type v[A,B] = Either[Option[A], Option[B]]

private def L[A,B](a: A): v[A,B] = Left(Some(a))
private def R[A,B](b: B): v[A,B] = Right(Some(b))  
// TODO: for more use scala macro to generate this for up to 22 types?
implicit def a2[A,B](a: A): v[A,B] = L(a)
implicit def b2[A,B](b: B): v[A,B] = R(b)
implicit def a3[A,B,C](a: A): v[v[A,B],C] = L(a2(a))
implicit def b3[A,B,C](b: B): v[v[A,B],C] = L(b2(b))
implicit def a4[A,B,C,D](a: A): v[v[v[A,B],C],D] = L(a3(a))
implicit def b4[A,B,C,D](b: B): v[v[v[A,B],C],D] = L(b3(b))    
implicit def a5[A,B,C,D,E](a: A): v[v[v[v[A,B],C],D],E] = L(a4(a))
implicit def b5[A,B,C,D,E](b: B): v[v[v[v[A,B],C],D],E] = L(b4(b))

type JsonPrimtives = (String v Int v Double)
type ValidJsonPrimitive[A] = A => JsonPrimtives

def test[A : ValidJsonPrimitive](x: A): A = x 

test("hi")
test(9)
// test(true)   // does not compile

J'utilise une technique similaire à Dijon


Cela peut-il fonctionner avec le sous-typage? Mon instinct: non, mais je me trompe peut-être. stackoverflow.com/questions/45255270
...

1

Eh bien, tout cela est très intelligent, mais je suis presque sûr que vous savez déjà que les réponses à vos principales questions sont différentes sortes de «non». Scala gère la surcharge différemment et, il faut bien l'admettre, un peu moins élégamment que ce que vous décrivez. Une partie est due à l'interopérabilité Java, une partie est due au fait de ne pas vouloir frapper les cas délicats de l'algorithme d'inférence de type, et une partie est due au fait qu'il ne s'agit simplement pas de Haskell.


5
Bien que j'utilise Scala depuis un certain temps, je ne suis ni aussi compétent ni aussi intelligent que vous semblez le penser. Dans cet exemple, je peux voir comment une bibliothèque pourrait fournir la solution. Il est logique de se demander ensuite si une telle bibliothèque existe (ou une alternative).
Aaron Novstrup

1

Ajout aux réponses déjà excellentes ici. Voici un résumé qui s'appuie sur les types d'union de Miles Sabin (et les idées de Josh) mais les définit également de manière récursive, vous pouvez donc avoir> 2 types dans l'union ( def foo[A : UNil Or Int Or String Or List[String])

https://gist.github.com/aishfenton/2bb3bfa12e0321acfc904a71dda9bfbb

NB: Je dois ajouter qu'après avoir joué avec ce qui précède pour un projet, j'ai fini par revenir à des types de somme simples (ie trait scellé avec des sous-classes). Les types d'union Miles Sabin sont parfaits pour restreindre le paramètre de type, mais si vous devez renvoyer un type d'union, cela n'offre pas grand-chose.


Cela peut-il résoudre le A|C <: A|B|Cproblème de sous - typage? stackoverflow.com/questions/45255270/... Mon instinct se sent NON parce que cela signifierait que cela A or Cdevrait être le sous-type de (A or B) or Cmais qui ne contient pas le type, A or Cdonc il n'y a aucun espoir de créer A or Cun sous-type de A or B or Cavec ce codage au moins .. . Qu'est-ce que tu penses ?
jhegedus

0

À partir de la documentation , avec l'ajout de sealed:

sealed class Expr
case class Var   (x: String)          extends Expr
case class Apply (f: Expr, e: Expr)   extends Expr
case class Lambda(x: String, e: Expr) extends Expr

Concernant la sealedpièce:

Il est possible de définir d'autres classes de cas qui étendent le type Expr dans d'autres parties du programme (...). Cette forme d'extensibilité peut être exclue en déclarant la classe de base Expr scellée; dans ce cas, toutes les classes qui étendent directement Expr doivent être dans le même fichier source que Expr.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.