Convertir una Lista de Opciones a una Opción de Lista usando Scalaz


Quiero transformar un List[Option[T]] en a Option[List[T]]. El tipo de firma de la función es

def lo2ol[T](lo: List[Option[T]]): Option[List[T]]

El comportamiento esperado es mapear una lista que contiene solo Somes en una Some que contiene una lista de los elementos dentro de los elementos Some. Por otro lado, si la lista de entrada tiene al menos un None, el comportamiento esperado es simplemente devolver None. Por ejemplo:

scala> lo2ol(Some(1) :: Some(2) :: Nil)
res10: Option[List[Int]] = Some(List(1, 2))

scala> lo2ol(Some(1) :: None :: Some(2) :: Nil)
res11: Option[List[Int]] = None

scala> lo2ol(Nil : List[Option[Int]])
res12: Option[List[Int]] = Some(List())

Una implementación de ejemplo, sin scalaz, sería:

def lo2ol[T](lo: List[Option[T]]): Option[List[T]] = {
  lo.foldRight[Option[List[T]]](Some(Nil)){(o, ol) => (o, ol) match {
    case (Some(x), Some(xs)) => Some(x :: xs);
    case _ => None : Option[List[T]]; 
}}}

Recuerdo haber visto en algún lugar un ejemplo similar, pero usando Scalaz para simplificar el código. ¿Cómo se vería?


Una versión un poco más sucinta, usando Scala2. 8 PartialFunction.condOpt, pero aún sin Scalaz:

import PartialFunction._

def lo2ol[T](lo: List[Option[T]]): Option[List[T]] = {
  lo.foldRight[Option[List[T]]](Some(Nil)){(o, ol) => condOpt(o, ol) {
    case (Some(x), Some(xs)) => x :: xs
  }
}}
Author: Rafael de F. Ferreira, 2010-04-03

4 answers

Hay una función que convierte un List[Option[A]] en un Option[List[A]] en Scalaz. Es sequence. Para obtener None en caso de que cualquiera de los elementos sea None y a Some[List[A]] en caso de que todos los elementos sean Some, simplemente puede hacer esto:

import scalaz.syntax.traverse._
import scalaz.std.list._     
import scalaz.std.option._

lo.sequence

Este método en realidad convierte F[G[A] en G[F[A]] dado que existe una implementación de Traverse[F], y de Applicative[G] (Option y List satisfacer ambos y son proporcionados por esas importaciones).

La semántica de Applicative[Option] son tales que si alguno de los elementos de un List de Option s son None, entonces el sequence será None también. Si desea obtener una lista de todos los valores Some independientemente de si cualquier otro valor es None, puede hacer esto:

lo flatMap (_.toList)

Se puede generalizar que para cualquier Monad que también forma un Monoid (List pasa a ser uno de estos):

import scalaz.syntax.monad._

def somes[F[_],A](x: F[Option[A]])
                 (implicit m: Monad[F], z: Monoid[F[A]]) =
  x flatMap (o => o.fold(_.pure[F])(z.zero))
 20
Author: Apocalisp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-26 13:32:14

Por alguna razón no te gusta

if (lo.exists(_ isEmpty)) None else Some(lo.map(_.get))

? Es probablemente el más corto de Scala sin Scalaz.

 16
Author: Rex Kerr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-03 00:11:39

Mientras que el Applicative[Option] en Scalaz tiene el comportamiento incorrecto de usar directamente MA#sequence, también puede derivar un Applicative de un Monoid. Esto se hace conveniente con MA#foldMapDefault o MA#collapse.

En este caso, usamos un Monoid[Option[List[Int]]. Primero realizamos un mapa interno (MA#∘∘) para envolver los Int s individuales en List s de un elemento.

(List(some(1), none[Int], some(2)) ∘∘ {(i: Int) => List(i)}).collapse assert_≟ some(List(1, 2))
(List(none[Int]) ∘∘ {(i: Int) => List(i)}).collapse                   assert_≟ none[List[Int]]
(List[Option[Int]]() ∘∘ {(i: Int) => List(i)}).collapse               assert_≟ none[List[Int]]

Abstracción de List a cualquier contenedor con instancias para Traverse, Pointed y Monoid:

def co2oc[C[_], A](cs: C[Option[A]])
                  (implicit ct: Traverse[C], cp: Pointed[C], cam: Monoid[C[A]]): Option[C[A]] =
  (cs ∘∘ {(_: A).pure[C]}).collapse


co2oc(List(some(1), none[Int], some(2)))   assert_≟ some(List(1, 2))
co2oc(Stream(some(1), none[Int], some(2))) assert_≟ some(Stream(1, 2))
co2oc(List(none[Int]))                     assert_≟ none[List[Int]]
co2oc(List[Option[Int]]())                 assert_≟ none[List[Int]]

Lamentablemente, intentar compilar este código actualmente cualquiera de los disparadores #2741 o envía el compilador a un bucle infinito.

ACTUALIZACIÓN Para evitar atravesar la lista dos veces, debería haber usado foldMapDefault:

(List(some(1), none[Int], some(2)) foldMapDefault (_ ∘ ((_: Int).pure[List])))

Esta respuesta se basó en la solicitud original de que una lista vacía, o una lista que solo contenga Nones, debería devolver un None. Por cierto, esto sería mejor modelado por el tipo Option[scalaz.NonEmptyList] -- NonEmptyList garantiza al menos un elemento.

Si solo quieres la a List[Int], hay muchas maneras más fáciles, dadas en otros respuesta. Dos formas directas que no se han mencionado:

list collect { case Some(x) => x }
list flatten
 2
Author: retronym,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-04 05:58:02

Esto funcionó para mí. Espero que esta sea una solución correcta.

Devuelve None si una de las Opciones de la Lista es None, de lo contrario devuelve la Opción de List[A]

def sequence[A](a: List[Option[A]]): Option[List[A]] = {

  a.foldLeft(Option(List[A]())) {
    (prev, cur) => {

      for {
        p <- prev if prev != None
        x <- cur
      } yield x :: p

    }
  }

}
 0
Author: Dmitri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-05 22:56:22