Falta de coincidencia de tipos en Scala para mayor comprensión

81

¿Por qué esta construcción causa un error de no coincidencia de tipos en 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 cambio Some con la Lista, se compila bien:

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

Esto también funciona bien:

for (first <- Some(1); second <- Some(2)) yield (first,second)
Felipe Kamakura
fuente
2
¿Qué resultado esperaba que Scala devolviera en el ejemplo fallido?
Daniel C. Sobral
1
Cuando lo estaba escribiendo pensé que obtendría una opción [Lista [(Int, Int)]].
Felipe Kamakura

Respuestas:

117

Porque las comprensiones se convierten en llamadas al método mapo flatMap. Por ejemplo este:

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

se convierte en eso:

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

Por lo tanto, el primer valor de bucle (en este caso List(1)) recibirá la flatMapllamada al método. Dado que flatMapen a Listdevuelve otro List, el resultado de la comprensión será, por supuesto, a List. (Esto era nuevo para mí: porque las comprensiones no siempre resultan en flujos, ni siquiera necesariamente en Seqs).

Ahora, eche un vistazo a cómo flatMapse declara en Option:

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

Mantén esto en mente. Veamos cómo el error de comprensión (el que tiene Some(1)) se convierte en una secuencia de llamadas de mapa:

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

Ahora, es fácil ver que el parámetro de la flatMapllamada es algo que devuelve un List, pero no un Option, como se requiere.

Para solucionar el problema, puede hacer lo siguiente:

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

Eso se compila muy bien. Vale la pena señalar que Optionno es un subtipo de Seq, como a menudo se supone.

Madoc
fuente
31

Un consejo fácil de recordar, para las comprensiones tratará de devolver el tipo de colección del primer generador, Option [Int] en este caso. Por lo tanto, si comienza con Some (1) , debe esperar un resultado de Option [T].

Si desea un resultado del tipo Lista , debe comenzar con un generador de listas.

¿Por qué tener esta restricción y no asumir que siempre querrá algún tipo de secuencia? Puede tener una situación en la que tenga sentido regresar Option. Tal vez tenga un Option[Int]que desee combinar con algo para obtener un Option[List[Int]], digamos con la siguiente función (i:Int) => if (i > 0) List.range(0, i) else None:; luego podría escribir esto y obtener Ninguno cuando las cosas no "tengan sentido":

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 forma en que se expanden las comprensiones en el caso general es de hecho un mecanismo bastante general para combinar un objeto de tipo M[T]con una función (T) => M[U]para obtener un objeto de tipo M[U]. En su ejemplo, M puede ser Opción o Lista. En general, tiene que ser del mismo tipo M. Por lo tanto, no puede combinar Option con List. Para ver ejemplos de otras cosas que pueden ser M, observe las subclases de este rasgo .

¿Por qué combinarlo List[T]con el (T) => Option[T]trabajo cuando empezaste con la Lista? En este caso, la biblioteca usa un tipo más general donde tiene sentido. Por lo tanto, puede combinar List con Traversable y hay una conversión implícita de Option a Traversable.

La conclusión es la siguiente: piense qué tipo desea que devuelva la expresión y comience con ese tipo como primer generador. Envuélvalo en ese tipo si es necesario.

huynhjl
fuente
Yo diría que es una mala elección de diseño hacer que la forsintaxis regular haga este tipo de desugaring functor / monádico. ¿Por qué no tener métodos con nombres diferentes para el mapeo de funciones / mónadas, como fmap, etc., y reservar la forsintaxis para tener un comportamiento extremadamente simple que coincida con las expectativas provenientes de prácticamente cualquier otro lenguaje de programación convencional?
ely
Puede hacer que el tipo de fmap / lift separado sea tan genérico como desee sin hacer que una declaración de flujo de control de computación secuencial convencional se vuelva muy sorprendente y tenga complicaciones de rendimiento matizadas, etc. "Todo funciona con para" no vale eso.
ely
4

Probablemente tenga algo que ver con que Option no sea un Iterable. El implícito Option.option2Iterablemanejará el caso en el que el compilador espera que el segundo sea un Iterable. Espero que la magia del compilador sea diferente según el tipo de variable de ciclo.

sblundy
fuente
1

Siempre encontré esto útil:

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
fuente