Niezgodność typów w Scali do zrozumienia

81

Dlaczego ta konstrukcja powoduje błąd niezgodności typu w Scali?

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)

Jeśli przełączę Some with the List, kompiluje się dobrze:

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

To również działa dobrze:

for (first <- Some(1); second <- Some(2)) yield (first,second)
Felipe Kamakura
źródło
2
Jakich rezultatów spodziewałeś się, że Scala powróci w nieudanym przykładzie?
Daniel C. Sobral
1
Kiedy to pisałem, myślałem, że dostanę Option [List [(Int, Int)]].
Felipe Kamakura

Odpowiedzi:

117

Dla zrozumienia są konwertowane na wywołania metody maplub flatMap. Na przykład ten:

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

staje się:

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

Dlatego pierwsza wartość pętli (w tym przypadku List(1)) otrzyma flatMapwywołanie metody. Ponieważ flatMapa Listzwraca inny List, wynikiem zrozumienia będzie oczywiście a List. (To było dla mnie nowe: rozumienie nie zawsze prowadzi do strumieni, a nawet niekoniecznie w Seqs.)

Teraz spójrz, jak flatMapjest deklarowane w Option:

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

Pamiętaj o tym. Zobaczmy, jak błędne do zrozumienia (to z Some(1)) jest konwertowane na sekwencję wywołań mapy:

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

Teraz łatwo zauważyć, że parametr flatMapwywołania jest czymś, co zwraca a List, ale nie Option, zgodnie z wymaganiami.

Aby to naprawić, możesz wykonać następujące czynności:

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

To się dobrze komponuje. Warto zauważyć, że Optionnie jest to podtyp Seq, jak się często zakłada.

Madoc
źródło
31

Łatwa do zapamiętania wskazówka, ponieważ wyrażenia spróbują zwrócić typ zbioru pierwszego generatora, w tym przypadku Option [Int]. Tak więc, jeśli zaczniesz od Some (1) , powinieneś spodziewać się wyniku opcji [T].

Jeśli chcesz otrzymać wynik typu Lista , powinieneś zacząć od generatora list.

Dlaczego masz to ograniczenie i nie zakładać, że zawsze będziesz chciał jakiejś sekwencji? Możesz mieć sytuację, w której powrót ma sens Option. Może masz coś Option[Int], co chcesz połączyć z czymś, aby uzyskać Option[List[Int]], powiedzmy z następującą funkcją (i:Int) => if (i > 0) List.range(0, i) else None:; możesz następnie napisać to i uzyskać Brak, gdy coś nie ma "sensu":

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

Jak dla listowe są rozszerzane w ogólnym przypadku są w rzeczywistości dość ogólny mechanizm połączyć obiekt typu M[T]z funkcją (T) => M[U]uzyskać obiektu typu M[U]. W twoim przykładzie M może oznaczać Option lub List. Generalnie musi być tego samego typu M. Więc nie możesz łączyć Option z Listą. Aby zobaczyć przykłady innych rzeczy, które mogą być M, spójrz na podklasy tej cechy .

Dlaczego jednak połączyłeś się List[T]z (T) => Option[T]pracą, kiedy zaczynałeś z Listą? W tym przypadku biblioteka używa bardziej ogólnego typu tam, gdzie ma to sens. Możesz więc połączyć List z Traversable i istnieje niejawna konwersja z Option na Traversable.

Najważniejsze jest to: zastanów się, jaki typ ma zwrócić wyrażenie i zacznij od tego typu jako pierwszego generatora. W razie potrzeby zawiń go w ten typ.

huynhjl
źródło
Twierdzę, że to zły wybór projektowy, aby zwykła forskładnia wykonywała tego typu funktor / monadyczne desugowanie. Dlaczego nie mieć inaczej nazwanych metod mapowania funktorów / monad, takich jak fmapitp., I zarezerwować forskładnię, aby uzyskać niezwykle proste zachowanie, które odpowiada oczekiwaniom pochodzącym z praktycznie każdego innego głównego nurtu języka programowania?
ely
Możesz sprawić, że oddzielne rzeczy typu fmap / lift będą tak ogólne, jak chcesz, bez sprawiania, że ​​główne instrukcje przepływu sterowania obliczeniami sekwencyjnymi stają się bardzo zaskakujące i mają skomplikowane komplikacje wydajnościowe itp. „Wszystko działa z for” nie jest tego warte.
ely
4

Prawdopodobnie ma to coś wspólnego z tym, że opcja nie jest iterowalna. Niejawne Option.option2Iterableobsłuży przypadek, w którym kompilator oczekuje, że sekunda będzie iterowalna. Spodziewam się, że magia kompilatora różni się w zależności od typu zmiennej pętli.

sblundy
źródło
1

Zawsze uważałem to za pomocne:

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
źródło