13 votes

Scala : spécifier un type générique par défaut au lieu de Rien

J'ai une paire de classes qui ressemble à quelque chose comme ça. Il y a un Generator qui génère une valeur basée sur certaines valeurs de niveau classe, et une GeneratorFactory qui construit un Generator .

case class Generator[T, S](a: T, b: T, c: T) {
  def generate(implicit bf: CanBuildFrom[S, T, S]): S =
    bf() += (a, b, c) result
}

case class GeneratorFactory[T]() {
  def build[S <% Seq[T]](seq: S) = Generator[T, S](seq(0), seq(1), seq(2))
}

Vous remarquerez que GeneratorFactory.build accepte un argument de type S y Generator.generate produit une valeur de type S mais il n'y a rien de type S stocké par le Generator .

Nous pouvons utiliser les classes comme ceci. La fabrique travaille sur une séquence de Char et generate produit un String parce que build se voit attribuer un String .

val gb = GeneratorFactory[Char]()
val g = gb.build("this string")
val o = g.generate

Cela fonctionne bien et permet de gérer le String implicitement parce que nous utilisons le type GeneratorFactory .


Le problème

Le problème se pose maintenant lorsque je veux construire une Generator sans passer par l'usine. J'aimerais pouvoir le faire :

val g2 = Generator('a', 'b', 'c')
g2.generate // error

Mais je reçois une erreur parce que g2 a un type Generator[Char,Nothing] et Scala "Cannot construct a collection of type Nothing with elements of type Char based on a collection of type Nothing".

Ce que je veux, c'est un moyen d'indiquer à Scala que la "valeur par défaut" de l'option S est quelque chose comme Seq[T] au lieu de Nothing . En s'inspirant de la syntaxe des paramètres par défaut, on pourrait penser que c'est quelque chose comme :

case class Generator[T, S=Seq[T]]

Des solutions insuffisantes

Bien sûr, cela fonctionne si nous indiquons explicitement au générateur quel doit être son type généré, mais je pense qu'une option par défaut serait plus agréable (mon scénario réel est plus complexe) :

val g3 = Generator[Char, String]('a', 'b', 'c')
val o3 = g3.generate  // works fine, o3 has type String

J'ai pensé à surcharger Generator.apply pour avoir une version à un seul type générique, mais cela provoque une erreur car, apparemment, Scala ne peut pas faire la différence entre les deux apply définitions :

object Generator {
  def apply[T](a: T, b: T, c: T) = new Generator[T, Seq[T]](a, b, c)
}

val g2 = Generator('a', 'b', 'c')  // error: ambiguous reference to overloaded definition

Sortie souhaitée

Ce que je voudrais, c'est un moyen de construire simplement une Generator sans spécifier le type S et le faire passer par défaut à Seq[T] pour que je puisse le faire :

val g2 = Generator('a', 'b', 'c')
val o2 = g2.generate
// o2 is of type Seq[Char]

Je pense que ce serait l'interface la plus propre pour l'utilisateur.

Avez-vous une idée de la façon dont je peux y parvenir ?

5voto

Travis Brown Points 56342

Y a-t-il une raison pour laquelle vous ne voulez pas utiliser un trait de base et ensuite restreindre S comme nécessaire dans ses sous-classes ? L'exemple suivant répond à vos besoins :

import scala.collection.generic.CanBuildFrom

trait Generator[T] {
  type S
  def a: T; def b: T; def c: T
  def generate(implicit bf: CanBuildFrom[S, T, S]): S = bf() += (a, b, c) result
}

object Generator {
  def apply[T](x: T, y: T, z: T) = new Generator[T] {
    type S = Seq[T]
    val (a, b, c) = (x, y, z)
  }
}

case class GeneratorFactory[T]() {
  def build[U <% Seq[T]](seq: U) = new Generator[T] {
    type S = U
    val Seq(a, b, c, _*) = seq: Seq[T]
  }
}

J'ai fait S un type abstrait pour le garder un peu plus à l'écart de l'utilisateur, mais vous pourriez tout aussi bien en faire un paramètre de type.

4voto

Chris Hodapp Points 41

Cela ne répond pas directement à votre question principale, car je pense que d'autres s'en occupent. Il s'agit plutôt d'une réponse à votre demande de valeurs par défaut pour les arguments de type.

J'y ai réfléchi et j'ai même commencé à rédiger une proposition de modification du langage pour l'autoriser. Cependant, je me suis arrêté lorsque j'ai réalisé d'où venait le Rien. Il ne s'agit pas d'une sorte de "valeur par défaut" comme je le pensais. Je vais tenter d'expliquer d'où il vient.

Afin d'attribuer un type à un argument de type, Scala utilise le type le plus spécifique possible/légal. Ainsi, par exemple, supposons que vous ayez "class A[T](x : T)" et que vous disiez "new A[Int]". Vous avez directement spécifié la valeur "Int" pour T. Supposons maintenant que vous disiez "new A(4)". Scala sait que 4 et T doivent avoir le même type. 4 peut avoir un type compris entre "Int" et "Any". Dans cette plage de types, "Int" est le type le plus spécifique, donc Scala crée un "A[Int]". Supposons maintenant que vous disiez "new A[AnyVal]". Maintenant, vous recherchez le type le plus spécifique T tel que Int < : T < : Any et AnyVal < : T < : AnyVal. Par chance, Int < : AnyVal < : Any, donc T peut être AnyVal.

Continuons, supposons maintenant que vous avez "classe B[S > : String < : AnyRef]". Si vous dites "new B", vous n'obtiendrez pas un B[Nothing]. Au contraire, vous obtiendrez un B[String]. C'est parce que S est contraint en tant que String < : S < : AnyRef et String se trouve au bas de cette plage.

Donc, vous voyez, pour la "classe C[R]", "new C" ne vous donne pas un C[Nothing] parce que Nothing est une sorte de valeur par défaut pour les arguments de type. Plutôt, vous obtenez un C[Nothing] parce que Nothing est la chose la plus basse que R peut être (si vous ne spécifiez pas autrement, Nothing < : R < : Any).

C'est pourquoi j'ai abandonné mon idée d'argument de type par défaut : je n'ai pas trouvé de moyen de le rendre intuitif. Dans ce système de restriction des gammes, comment mettre en œuvre un défaut de faible priorité ? Ou, est-ce que le défaut est plus prioritaire que la logique "choisir le type le plus bas" s'il est dans la plage valide ? Je n'ai pas trouvé de solution qui ne soit pas déroutante, au moins dans certains cas. Si vous y parvenez, faites-le moi savoir, car je suis très intéressé.

edit : Notez que la logique est inversée pour les paramètres contravariants. Ainsi, si vous avez "classe D[-Q]" et que vous dites "nouveau D", vous obtenez un D[Any].

2voto

Miles Sabin Points 13604

Une option consiste à déplacer l'invocation de la CanBuildFrom à un endroit où il (ou, plutôt, ses instances) peut aider à déterminer S ,

case class Generator[T,S](a: T, b: T, c: T)(implicit bf: CanBuildFrom[S, T, S]) {
  def generate : S =
    bf() += (a, b, c) result
}

Exemple de session REPL,

scala> val g2 = Generator('a', 'b', 'c')
g2: Generator[Char,String] = Generator(a,b,c)

scala> g2.generate
res0: String = abc

Mise à jour

Le site GeneratorFactory devra également être modifié afin que son build propage une méthode appropriée CanBuildFrom à l'instance Generator constructeur,

case class GeneratorFactory[T]() {
  def build[S](seq: S)(implicit conv: S => Seq[T], bf: CanBuildFrom[S, T, S]) =
    Generator[T, S](seq(0), seq(1), seq(2))
}

Non pas qu'avec Scala < 2.10.0, vous ne pouvez pas mélanger les limites de vue et les listes de paramètres implicites dans la même définition de méthode, nous devons donc traduire la borne S <% Seq[T] à son paramètre implicite équivalent S => Seq[T] .

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