191 votes

Comment définir la "disjonction de types" (union de types) ?

Un moyen qui a a été suggéré pour traiter les doubles définitions de méthodes surchargées est de remplacer la surcharge par le pattern matching :

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

Cette approche exige que nous abandonnions la vérification statique des types sur les arguments à foo . Il 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 m'approcher avec Either mais cela 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 semble qu'une solution générale (élégante, efficace) nécessiterait de définir Either3 , Either4 , .... Quelqu'un connaît-il une autre solution pour atteindre le même objectif ? À 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 pour que je puisse simplement les importer ?

191voto

michid Points 3526

Miles Sabin décrit une très belle façon d'obtenir le type d'union dans son récent billet de blogue Types d'union sans boîte en Scala via l'isomorphisme de 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 des 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
}

0 votes

Merci d'avoir fait passer le message. Absolument génial !

17 votes

C'est l'une des choses les plus impressionnantes que j'ai vues.

0 votes

J'essaie toujours de comprendre comment faire fonctionner ceci dans un contexte de vararg, où les arguments individuels peuvent être de différents types "concrets".

151voto

Daniel C. Sobral Points 159554

Eh bien, dans le cas spécifique de Any* l'astuce ci-dessous ne fonctionnera pas, car elle n'accepte pas les types mixtes. Cependant, comme les types mixtes ne fonctionnent 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 foo comme ça :

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) o foo("abc") et cela fonctionnera, mais essayez foo(true) et il échouera. Le code client peut contourner ce problème en créant un fichier de type StringOrInt[Boolean] sauf si, comme indiqué par Randall ci-dessous, vous faites StringOrInt a sealed classe.

Cela fonctionne parce que T: StringOrInt signifie 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 faire fonctionner le code qui demande ce type.

14 votes

Si class StringOrInt[T] est fait sealed la "fuite" à laquelle vous faites référence ("Bien sûr, le code client pourrait contourner ce problème en créant un fichier de type StringOrInt[Boolean] ") est bouché, au moins si StringOrInt réside dans un fichier à part entière. Alors les objets témoins doivent être définis dans la même souce que StringOrInt .

3 votes

J'ai essayé de généraliser quelque peu cette solution (postée comme réponse ci-dessous). Le principal inconvénient par rapport à la Either semble être que nous perdons une grande partie du support du compilateur pour vérifier la correspondance.

0 votes

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

32voto

missingfaktor Points 44003

Voici la méthode de Rex Kerr pour coder les types d'union. C'est simple et direct !

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 este excellent article de blog de Miles Sabin qui fournit une autre façon d'encoder les types d'union en Scala.

6 votes

Malheureusement, cet encodage peut être défait : scala> f(9.2: AnyVal) passe le vérificateur de type.

0 votes

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

11 votes

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

19voto

Aaron Novstrup Points 10742

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 les suivants

  • Comme Daniel l'a souligné, il ne gère pas les collections/varargues 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 le Either approche, une généralisation plus poussée nécessiterait de définir des modèles analogues. Or3 , Or4 etc. Bien entendu, il serait beaucoup plus simple de définir de tels traits que de définir les caractères correspondants. Either classes.

Mise à jour :

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

14voto

Kevin Wright Points 31665

Une solution de classe de type est probablement la plus belle façon de procéder ici, en utilisant des implicites. Cette approche est similaire à celle des monoïdes mentionnée dans le livre d'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)

              ^

0 votes

Je peux me tromper, mais je ne pense pas que ce soit ce que le PO recherchait. Le PO demandait un type de données qui pourrait représenter une union disjointe de types, et ensuite faire une analyse de cas sur celle-ci. 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 compilatoire.

6 votes

Le site réel La question posée était de savoir comment exposer un comportement différent pour différents types, mais sans surcharge. Sans connaissance des classes de types (et peut-être une certaine exposition au C/C++), un type union semble être la seule solution. L'interface préexistante de Scala Either type tend à renforcer cette conviction. L'utilisation de classes de types via les implicites de Scala est une meilleure solution au problème sous-jacent, mais c'est un concept relativement nouveau et encore peu connu, ce qui explique pourquoi le PO ne savait même pas qu'il fallait les considérer comme une alternative possible à un type union.

0 votes

Cela fonctionne-t-il avec le sous-typage ? stackoverflow.com/questions/45255270/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X