Scala: hacer una conversión implícita A-> B Funcione para la opción [A] -> Opción [B

StackOverflow https://stackoverflow.com/questions/8823816

  •  27-10-2019
  •  | 
  •  

Pregunta

Estoy tratando de escribir una función que vuelva a usar las conversiones implícitas que tengo para el objeto A -> Objeto B cuando están envueltos de una manera genérica para que la opción [A] -> Opción [B] Conversiones también trabajar.

Lo que se me ocurre es:

implicit def fromOptionToOption[A, B](from: Option[A])(implicit conversion: (A) => B): Option[B] = from.map(conversion(_))

Esto funciona cuando asigno algunos (..) a un valor pero no cuando asigno una opción Val; Consulte la siguiente salida de la consola:

scala> trait T
defined trait T

scala> case class Foo(i: Int) extends T
defined class Foo

scala> case class Bar(i: Int) extends T
defined class Bar

scala> implicit def fromFooToBar(f: Foo):Bar = Bar(f.i)
fromFooToBar: (f: Foo)Bar

scala> implicit def fromBarToFoo(b: Bar):Foo = Foo(b.i)
fromBarToFoo: (b: Bar)Foo

scala> implicit def fromOptionToOption[A, B](from: Option[A])(implicit conversion: (A) => B): Option[B] = from.map(conversion(_))
fromOptionToOption: [A, B](from: Option[A])(implicit conversion: (A) => B)Option[B]

scala> val foo: Option[Foo] = Some(Bar(1))
foo: Option[Foo] = Some(Foo(1))
// THIS WORKS as expected

scala> val fooOpt = Some(Foo(4))
fooOpt: Some[Foo] = Some(Foo(4))

scala> val barOpt2: Option[Bar] = fooOpt
<console>:16: error: type mismatch;
 found   : Some[Foo]
 required: Option[Bar]
       val barOpt2: Option[Bar] = fooOpt
                                  ^
//THIS FAILS.

Realmente no veo la diferencia entre la primera y la segunda conversión. De alguna manera no invoca la conversión implícita en este último. Supongo que tiene algo que ver con el sistema de tipos, pero todavía no puedo ver cómo. ¿Algunas ideas? -Albert (estoy en Scala 2.9.1)

¿Fue útil?

Solución

Aquí hay pista:

scala> val fooOpt: Option[Bar] = Option(Foo(1))
fooOpt: Option[Bar] = Some(Bar(1))

Y otro:

scala> implicit def foobar(x: String): Int = augmentString(x).toInt
foobar: (x: String)Int

scala> val y: Option[String] = Option(1)
y: Option[String] = Some(1)

scala> val y: Option[Int] = Option("1")
y: Option[Int] = Some(1)

Parece legítimamente extraño insecto. Abriría un caso de prueba más pequeño y abriría un problema (o buscaría uno en Jira).

Como un aparte:

Puede usar alguna teoría de categoría para manejar muchos tipos diferentes de cosas "opcionistas".

package object fun {
  trait Functor[Container[_]] {
    def fmap[A,B](x: Container[A], f: A => B): Container[B]
  }
  object Functor {
     implicit object optionFunctor extends Functor[Option] {
       override def fmap[A,B](x: Option[A], f: A => B): Option[B] = x map f
     }
     // Note: With some CanBuildFrom magic, we can support Traversables here.
  }
  implicit def liftConversion[F[_], A, B](x: F[A])(implicit f: A => B, functor: Functor[F]): F[B] = 
    functor.fmap(x,f)

}

Eso es un poco más avanzado, ya que está mapeando alguna teoría de categoría FP en el problema, pero es una solución más general para elevar las conversaciones implícitas en contenedores según sea necesario. Observe cómo encadenan utilizando un método de conversación implícito que toma un argumento implícito más limitado.

Además, esto debería hacer que los ejemplos funcionen:

scala> val tmp = Option(Foo(1))
tmp: Option[Foo] = Some(Foo(1))

scala> val y: Option[Bar] = tmp
y: Option[Bar] = Some(Bar(1))

Y hacer su uso de Some más peligroso:

scala> val tmp = Some(Foo(1))
tmp: Some[Foo] = Some(Foo(1))

scala> val y: Option[Bar] = tmp
<console>:25: error: could not find implicit value for parameter functor: fun.Functor[Some]
       val y: Option[Bar] = tmp
                            ^

Eso te está diciendo que diferencia es crítico e interactúa con implicados. Supongo que te encontraste con un error muy raro, probablemente difícil de solucionar que se puede evitar usando otras técnicas.

Otros consejos

Es posible que no sepas de ello, pero hay una bandera para eso: -Xlog-implicits. Y esto es lo que dice:

scala> val barOpt2: Option[Bar] = fooOpt
fromOptionToOption is not a valid implicit value for Some[Foo] => Option[Bar] because:
incompatible: (from: Option[Foo])(implicit conversion: Foo => B)Option[B] does not match expected type Some[Foo] => Option[Bar]
<console>:16: error: type mismatch;
 found   : Some[Foo]
 required: Option[Bar]
       val barOpt2: Option[Bar] = fooOpt
                                  ^

Y ahí tienes, no sabe qué tipo B debe ser. 0__ mencionó que este problema no sucede con colecciones invariantes, y eso tiene sentido. En colecciones invariantes, B Debe ser exactamente Bar, mientras que para las colecciones covariantes podría ser cualquier subtipo de Bar.

Entonces, ¿por qué lo hace? val foo: Option[Foo] = Some(Bar(1)) ¿trabajar? Bueno, también hay una bandera para eso ... -Ytyper-debug. No para los débiles, sin embargo, dada la verbosidad extrema.

De todos modos, comparé lo que sucede en ambos casos, y la respuesta es bastante simple ... no es el Option que se está convirtiendo en ese caso, pero Bar! Recuerde, declaró una conversión implícita de Bar => Foo, por lo que está aplicando esa conversión antes de pasando el resultado a Some!

No funciona porque la especificación del lenguaje Scala define la vista de la siguiente manera:

Los parámetros y métodos implícitos también pueden definir conversiones implícitas llamadas puntos de vista. Una vista desde el tipo S digitar T se define por un valor implícito que tiene tipo de función S => t o (=> S) => t o por un método convertible a un valor de ese tipo.

fromOptionToOption No se ajusta a las tres categorías, ya que toma un parámetro implícito. El compilador no parece encontrar convertidor con el destino y la fuente tienen un tipo genérico.

Definiendo una vista desde Option[Foo] a Option[Bar] funciona como se esperaba.

trait T
case class Foo(i: Int) extends T
case class Bar(i: Int) extends T

object Main {
  implicit def fromFooToBar(f: Foo):Bar = Bar(f.i)
  implicit def fromBarToFoo(b: Bar):Foo = Foo(b.i)
  // implicit def fromOptionToOption[A, B](from: Option[A])(implicit conversion: (A) => B): Option[B] =
  //  from.map(conversion(_))
  implicit def fromOptionFooToOptionBar(o: Option[Foo]): Option[Bar] = o map { foo => foo } 

  def test(): Option[Bar] = {
    val fooOpt = Some(Foo(4))
    val barOpt2: Option[Bar] = fooOpt

    barOpt2
  }
}

println(Main.test)

Ejecutando esta impresión:

$ scala so.scala
Some(Bar(4))

Sin embargo, no todo está perdido. No es tan agradable como el general Option a Option, pero podemos hacer algo como cualquier cosa que pueda convertirse en Bar a Option[Bar] por vista vinculada.

trait T
case class Foo(i: Int) extends T
case class Bar(i: Int) extends T

object Main {
  implicit def fromFooToBar(f: Foo):Bar = Bar(f.i)
  implicit def fromBarToFoo(b: Bar):Foo = Foo(b.i)
  implicit def fromOptionToOptionBar[A <% Bar](from: Option[A]): Option[Bar] =
    from map { foo => foo }

  def test(): Option[Bar] = {
    val fooOpt = Some(Foo(4))
    val barOpt2: Option[Bar] = fooOpt

    barOpt2
  }
}

println(Main.test)

Aquí hay otra solución que se puede utilizar para general Option a Option pero requiere más .convert llamar:

trait T
case class Foo(i: Int) extends T
case class Bar(i: Int) extends T

case class Converter[A](x: Option[A]) {
  def convert[B](implicit ev: Function1[A, B]): Option[B] = x map { a: A => ev(a) }
}

object Main {
  implicit def optionToConverter[A](x: Option[A]) = Converter(x)
  implicit def fooToBar(x: Foo) = Bar(x.i)

  def test(): Option[Bar] = {
    val fooOpt = Some(Foo(4))
    val barOpt: Option[Bar] = fooOpt.convert
    barOpt
  }
}

println(Main.test)

De hecho, es un problema muy extraño. Traté de usar otro tipo que Option, y resulta que el problema es que Option es covariante en su parámetro de tipo. Todo esto funciona:

case class A[B](value: B)  // invariant in B

case class X()
case class Y()

implicit def xtoy(x: X): Y = Y()
implicit def ytox(x: Y): X = X()
implicit def movea[U, V](from: A[U])(implicit view: U => V): A[V] =  A[V](from.value)

def test(a: A[Y]) = "ok"
test(A(X()))   // (1)
val f = A(X())
test(f)        // (2)

Pero si en cambio defino A como

case class A[+B](value: B)  // covariant in B

El caso (2) falla. El caso (1) siempre tiene éxito, porque Scala ya se convierte X a Y antes de envolverlo en un A.

Ahora que sabemos el problema fuente, debes esperar a que explique un gurú de tipo por qué Esto es en realidad un problema ... la conversión sigue siendo válida, ya ves:

askForY(movea(f))  // succeeds, even with A[+B]

Yo mejoré @jseureth respuesta y soporte agregado para Traversable:

trait Mappable[A, B, C[_]] {
  def apply(f: A => B): C[B]
}

package object app {

  implicit class OptionMappable[A, B, C[X] <: Option[X]](option: C[A]) extends Mappable[A, B, Option] {
    override def apply(f: A => B): Option[B] = option.map(f)
  }

  implicit class TraversableMappable[A, B, C[X] <: Traversable[X]](traversable: C[A])
    (implicit cbf: CanBuildFrom[C[A], B, C[B]]) extends Mappable[A, B, C] {
    override def apply(f: A => B): C[B] = {
      val builder = cbf(traversable)
      builder.sizeHint(traversable)
      builder ++= traversable.map(f)
      builder.result()
    }
  }

  implicit def liftConversion[C[_], A, B](x: C[A])
    (implicit f: A => B, m: C[A] => Mappable[A, B, C]): C[B] = m(x)(f)

}

Ahora puede convertir implícitamente opciones y traversables:

implicit def f(i: Int): String = s"$i"

val a: Option[String] = Some(1)
val b: Seq[String] = Seq(1, 2, 3)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top