web-dev-qa-db-fra.com

Type incompatibilité sur Scala pour la compréhension

Pourquoi cette construction provoque-t-elle une erreur d’appariement de types dans Scala?

for (first <- Some(1); second <- List(1,2,3)) yield (first,second)

<console>:6: error: type mismatch;
 found   : List[(Int, Int)]
 required: Option[?]
       for (first <- Some(1); second <- List(1,2,3)) yield (first,second)

Si je commute le Some avec la List, ça compile bien:

for (first <- List(1,2,3); second <- Some(1)) yield (first,second)
res41: List[(Int, Int)] = List((1,1), (2,1), (3,1))

Cela fonctionne aussi très bien:

for (first <- Some(1); second <- Some(2)) yield (first,second)
72
Felipe Kamakura

Les compréhensions sont converties en appels à la méthode map ou flatMap. Par exemple celui-ci:

for(x <- List(1) ; y <- List(1,2,3)) yield (x,y)

devient cela:

List(1).flatMap(x => List(1,2,3).map(y => (x,y)))

Par conséquent, la première valeur de boucle (dans ce cas, List(1)) recevra l'appel de méthode flatMap. Puisque flatMap sur une List retourne une autre List, le résultat de la compréhension sera bien sûr une List. (C’était nouveau pour moi: les compréhensions n’aboutissent pas toujours en flux, pas même nécessairement en Seqs.)

Voyons maintenant comment flatMap est déclaré dans Option:

def flatMap [B] (f: (A) ⇒ Option[B]) : Option[B]

Garde ça en tête. Voyons comment l’erreur de compréhension (celle avec Some(1)) est convertie en une séquence d’appels de carte:

Some(1).flatMap(x => List(1,2,3).map(y => (x, y)))

Maintenant, il est facile de voir que le paramètre de l'appel flatMap est quelque chose qui retourne une List, mais pas une Option, comme requis.

Pour résoudre le problème, vous pouvez procéder comme suit:

for(x <- Some(1).toSeq ; y <- List(1,2,3)) yield (x, y)

Cela compile bien. Il est à noter que Option n'est pas un sous-type de Seq, comme on le suppose souvent.

110
Madoc

Un conseil facile à retenir, for comprehensions essaiera de renvoyer le type de la collection du premier générateur, Option [Int] dans ce cas. Donc, si vous commencez par Some (1), vous devez vous attendre à un résultat de l'option [T]. 

Si vous voulez un résultat de type List, vous devez commencer par un générateur de liste.

Pourquoi cette restriction et ne pas présumer que vous voudrez toujours une sorte de séquence? Vous pouvez avoir une situation où il est logique de retourner Option. Peut-être avez-vous un Option[Int] à combiner avec quelque chose pour obtenir un Option[List[Int]], par exemple avec la fonction suivante: (i:Int) => if (i > 0) List.range(0, i) else None; vous pouvez alors écrire ceci et obtenir None quand les choses ne "ont pas de sens":

val f = (i:Int) => if (i > 0) Some(List.range(0, i)) else None
for (i <- Some(5); j <- f(i)) yield j
// returns: Option[List[Int]] = Some(List(0, 1, 2, 3, 4))
for (i <- None; j <- f(i)) yield j
// returns: Option[List[Int]] = None
for (i <- Some(-3); j <- f(i)) yield j
// returns:  Option[List[Int]] = None

La manière dont pour les compréhensions sont développés dans le cas général est en fait un mécanisme assez général pour combiner un objet de type M[T] avec une fonction (T) => M[U] pour obtenir un objet de type M[U]. Dans votre exemple, M peut être Option ou List. En général, il doit s'agir du même type M. Vous ne pouvez donc pas combiner Option avec List. Pour des exemples d'autres choses qui peuvent être M, regardez sous-classes de ce trait .

Pourquoi la combinaison de List[T] avec (T) => Option[T] a-t-elle fonctionné quand vous avez commencé avec la liste? Dans ce cas, la bibliothèque utilise un type plus général où cela a du sens. Vous pouvez donc combiner Liste avec Traversable et il existe une conversion implicite d’Option en Traversable.

La ligne de fond est la suivante: réfléchissez au type que vous souhaitez que l'expression renvoie et commencez avec ce type en tant que premier générateur. Enveloppez-le dans ce type si nécessaire. 

30
huynhjl

Cela a probablement quelque chose à voir avec Option n'étant pas un Itérable. Le Option.option2Iterable implicite gèrera le cas où le compilateur s’attend à ce que la seconde soit un Iterable. Je m'attends à ce que la magie du compilateur soit différente selon le type de variable de boucle.

4
sblundy

J'ai toujours trouvé cela utile:

scala> val foo: Option[Seq[Int]] = Some(Seq(1, 2, 3, 4, 5))
foo: Option[Seq[Int]] = Some(List(1, 2, 3, 4, 5))

scala> foo.flatten
<console>:13: error: Cannot prove that Seq[Int] <:< Option[B].
   foo.flatten
       ^

scala> val bar: Seq[Seq[Int]] = Seq(Seq(1, 2, 3, 4, 5))
bar: Seq[Seq[Int]] = List(List(1, 2, 3, 4, 5))

scala> bar.flatten
res1: Seq[Int] = List(1, 2, 3, 4, 5)

scala> foo.toSeq.flatten
res2: Seq[Int] = List(1, 2, 3, 4, 5)
0
user451151