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)
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 Seq
s.)
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.
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.
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.
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)