Geben Sie zum Verständnis Mismatch in Scala ein

81

Warum verursacht diese Konstruktion in Scala einen Typ-Mismatch-Fehler?

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)

Wenn ich das Einige mit der Liste wechsle, wird es gut kompiliert:

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

Das funktioniert auch gut:

for (first <- Some(1); second <- Some(2)) yield (first,second)
Felipe Kamakura
quelle
2
Welches Ergebnis erwarteten Sie von Scala in dem fehlgeschlagenen Beispiel?
Daniel C. Sobral
1
Als ich es schrieb, dachte ich, ich würde eine Option [Liste [(Int, Int)]] bekommen.
Felipe Kamakura

Antworten:

117

Zum Verständnis werden in Aufrufe der Methode mapoder umgewandelt flatMap. Zum Beispiel dieses:

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

wird das:

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

Daher List(1)erhält der erste Schleifenwert (in diesem Fall ) den flatMapMethodenaufruf. Da flatMapbei einer ListRückkehr eine andere ist List, wird das Ergebnis des Verständnisses natürlich a sein List. (Das war neu für mich: Zum Verständnis führen nicht immer Streams, nicht einmal unbedingt zu Seqs.)

Schauen Sie sich nun an, wie flatMapdeklariert wird in Option:

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

Denken Sie daran. Mal sehen, wie das fehlerhafte Verständnis (das mit Some(1)) in eine Folge von Kartenaufrufen umgewandelt wird:

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

Jetzt ist leicht zu erkennen, dass der Parameter des flatMapAufrufs bei Bedarf ein List, aber kein a zurückgibt Option.

Um das Problem zu beheben, können Sie Folgendes tun:

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

Das passt gut zusammen. Es ist erwähnenswert, dass dies Optionkein Subtyp von ist Seq, wie oft angenommen wird.

Madoc
quelle
31

Ein leicht zu merkender Tipp, zum Verständnis wird versucht, den Typ der Sammlung des ersten Generators, in diesem Fall Option [Int], zurückzugeben. Wenn Sie also mit Some (1) beginnen , sollten Sie ein Ergebnis von Option [T] erwarten.

Wenn Sie ein Ergebnis vom Listentyp wünschen , sollten Sie mit einem Listengenerator beginnen.

Warum diese Einschränkung haben und nicht davon ausgehen, dass Sie immer eine Sequenz wollen? Sie können eine Situation haben, in der es sinnvoll ist, zurückzukehren Option. Vielleicht haben Sie eine Option[Int], die Sie mit etwas kombinieren möchten, um eine zu erhalten Option[List[Int]], sagen wir mit der folgenden Funktion : (i:Int) => if (i > 0) List.range(0, i) else None; Sie könnten dies dann schreiben und None erhalten, wenn die Dinge keinen "Sinn ergeben":

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

Wie das Verständnis im allgemeinen Fall erweitert wird, ist in der Tat ein ziemlich allgemeiner Mechanismus, um ein Objekt vom Typ M[T]mit einer Funktion (T) => M[U]zu kombinieren , um ein Objekt vom Typ zu erhalten M[U]. In Ihrem Beispiel kann M Option oder Liste sein. Im Allgemeinen muss es der gleiche Typ sein M. Sie können Option also nicht mit List kombinieren. Beispiele für andere mögliche Dinge finden Sie Min Unterklassen dieses Merkmals .

Warum hat das Kombinieren List[T]mit der (T) => Option[T]Arbeit funktioniert, als Sie mit der Liste begonnen haben? In diesem Fall verwendet die Bibliothek einen allgemeineren Typ, wenn dies sinnvoll ist. Sie können also List mit Traversable kombinieren und es gibt eine implizite Konvertierung von Option zu Traversable.

Das Fazit lautet: Überlegen Sie, welchen Typ der Ausdruck zurückgeben soll, und beginnen Sie mit diesem Typ als erstem Generator. Wickeln Sie es bei Bedarf in diesen Typ ein.

huynhjl
quelle
Ich würde argumentieren, dass es eine schlechte Wahl für das Design ist, eine reguläre forSyntax für diese Art von Funktor / monadischem Desugaring zu verwenden. Warum nicht anders benannte Methoden für die Zuordnung von Funktoren / Monaden fmapusw. und Reservesyntax verwenden for, um ein äußerst einfaches Verhalten zu erzielen, das den Erwartungen entspricht, die von praktisch jeder anderen gängigen Programmiersprache ausgehen?
Ely
Sie können das separate fmap / lift-Material so allgemein gestalten, wie Sie möchten, ohne dass eine Mainstream-Anweisung für den Ablauf der sequentiellen Rechensteuerung sehr überraschend wird und nuancierte Leistungskomplikationen usw. aufweist. "Alles funktioniert mit" ist das nicht wert.
Ely
4

Es hat wahrscheinlich etwas damit zu tun, dass Option kein Iterable ist. Das Implizite Option.option2Iterablebehandelt den Fall, in dem der Compiler erwartet, dass die Sekunde eine Iterable ist. Ich erwarte, dass die Compiler-Magie je nach Typ der Schleifenvariablen unterschiedlich ist.

sblundy
quelle
1

Ich fand das immer hilfreich:

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)
user451151
quelle