¿Existe una forma ordenada de convertir una clase que solo tiene campos Option a una clase similar envuelta en una opción?

case class Data(a: Option[Int], b: Option[Int])
case class DataX(a: Int, b: Int)

def convert(data: Data): Option[DataX] = 
  for {
    alpha <- data.a
    beta <- data.b
  } yield DataX(alpha, beta)

Es simplemente tedioso escribir y parece que debería / podría haber una forma estándar, por ejemplo, en Cats o Scalaz?

3
Max Power 19 oct. 2017 a las 12:53

2 respuestas

La mejor respuesta

Hay dos pequeñas notas antes de pasar a la implementación.

1) Con shapeless, puede convertir cualquier clase de caso (o ADT) a su representación genérica y viceversa.
- Si es la clase de caso, la representación genérica será HList
- Si es el ADT (rasgo sellado / clase abstracta, con objetos / clases de casos extendiéndolo) - la representación genérica será Coproduct

Puede hacer tales conversiones porque son isomórficas. Hacer tales conversiones es útil porque, HList - tiene propiedades de List, por lo que puede hacer algunas cosas interesantes de forma genérica, como mapear, plegar, filtrar y otras cosas.

2) Hay una abstracción en la programación funcional, llamada Secuencia (o, a veces, de manera más general, Functor Traversable), que convierte F[G[A]] en G[F[A]], dado G - es { {X3}}. Entonces, por ejemplo, puede convertir List[Option[A]] -> Option[List[A]], o Future[Either[?, A]] a Either[? Future[A]], etc. Eso es exactamente lo que desea lograr.

Entonces el plan es:

  1. Convierta su clase de caso a HList, que consta de Option s en Tu caso.

  2. Use la operación de secuencia en HList (obtendría Option[Hlist])

  3. Asigne sobre Option para convertir HList a su siguiente representación de clase de caso.

Estaba a punto de implementar el Sequencer yo mismo, pero descubrí que esa parte ya estaba implementada https: // github .com / typelevel / shapeless-contrib ("org.typelevel" %% "shapeless-scalaz" % "0.4.0"). Puede mirar a través de la implementación de Sequencer, no tenga miedo, que al principio parecería magia completa. Después de mirar a través de varias implementaciones de clases de tipos para sin forma, comienza a tener sentido.

Entonces, la implementación no genérica es sencilla:

  import shapeless.Generic
  import scalaz.std.option.optionInstance
  import shapeless.contrib.scalaz.functions._

  def convertData(d: DataO): Option[Data] = {
    val x = Generic[DataO].to(d)
    val b = sequence(x)
    b.map(Generic[Data].from)
  }

De esta manera perdimos la generosidad, así que vamos a incorporar más tipos a la función. Definitivamente necesitamos proporcionar
tipo de entrada: I y
tipo de salida: O.
También debemos proporcionar una representación genérica, que sin forma proporciona implícitamente: Generic.Aux[I, Ri] Ri: sería HList, que consta de Option en su caso.
Entonces necesitas un secuenciador, que convierte tu HList, a Option de otro HList (en general, HList of Fs a F[HList]): {{X10} }, donde F - es un Functor.

Entonces, al final, la implementación es la siguiente :.

  def convertData[I, Ri <: HList, F[_]: Functor, O, Ro](d: I)(implicit GI: Generic.Aux[I, Ri],  Se: Sequencer.Aux[Ri, F[Ro]], G: Generic.Aux[O, Ro]): F[O] = {
    val x = GI.to(d)
    val b = sequence(x)
    val y = Functor[F].map(b)(G.from)
    y
  }

Donde, F - es cualquier Aplicativo, no necesariamente Option. El problema es que, al final, el compilador de Scala no puede inferir tipos correctamente, y necesita inferirlos manualmente:

    case class DataO(i: Option[Int])
    case class Data(i: Int)
    convertData[DataO, shapeless.::[Option[Int], HNil], Option, Data, shapeless.::[Int, HNil]](
      DataO(Option(2))
    )

Esto sucede porque Sequencer solo funciona con HList, pero la representación genérica también puede ser Coproduct, por lo que debe proporcionar la evidencia de que su descomposición genérica será HList. Idealmente, solo necesitaría especificar el tipo de salida y el tipo de aplicación:

convert[Data, Option](DataO(None)).

Sigo pensando que es posible, pero todavía no he descubierto cómo. Intentaré modificarlo un poco más, quizás descubra el camino.

UPD Logré hacerlo, el código final es:

import scalaz.std.option.optionInstance
import shapeless.contrib.scalaz.functions._

case class DataO(i: Option[Int])    
case class Data(i: Int)

case class Converter[I, O]() {
  def convert[Ri <: HList, Ro, F[_]: Functor](d: I)(implicit GI: Generic.Aux[I, Ri], Se: Sequencer.Aux[Ri, F[Ro]], G: Generic.Aux[O, Ro]): F[O] = {
    val x = GI.to(d)
    val b = sequence(x)
    val y = Functor[F].map(b)(G.from)
    y
  }
}

//usage
Converter[DataO, Data].convert(DataO(Option(1)) // Some(Data(1))
0
I See Voices 20 oct. 2017 a las 15:10

Esta respuesta toma un poco de esto.

En primer lugar, esto se generaliza a todos los functores Applicative, por lo que no usaré Option específicamente excepto para ejemplos. En segundo lugar, se usa sin forma para todas las golosinas Generic / HList.

Quieres sequence más de HList s. Esto se implementa como un pliegue. Esta es la función de acumulación:

// (a, b) => a :: b plus applicative and shapeless noise
object sequenceFold extends Poly2 {
  implicit def fold[A, B <: HList, F[_]: Applicative] = at[F[A], F[B]] { (fa, fb) =>
    fa.map { a: A => b: B => a :: b }.ap(fb)
  }
}

Entonces, sequenceH

def sequenceH[
  F[_]: Applicative,
  L <: HList,
  O <: HList
](l: L)(implicit
  restrict: UnaryTCConstraint[L, F], // All elements of L are F[something]
  folder: RightFolder.Aux[L, F[HNil], sequenceFold.type, F[O]] // evidence for fold
): F[O] = l.foldRight(Applicative[F].pure(HNil: HNil))(sequenceFold)(folder)
// This is rather painful to use, because type inference breaks down utterly

Ahora, usamos Generic para arrastrar esto fuera de HList tierra. Primero, necesitamos alguna relación entre Data y DataX, porque de lo contrario, buscaremos DataX desde HList Int :: Int :: HNil, y eso no funciona . En este caso, creo que es mejor parametrizar Data sobre algún constructor F, pero las clases de tipos también funcionarían, creo:

case class DataF[F[_]](a: F[Int], b: F[Int])
type Data = DataF[Option]
type Id[X] = X
type DataX = DataF[Id]

def sequenceCase[
  D[F[_]], // Types like DataF
  F[_]: Applicative,
  IL <: HList,
  OL <: HList
](i: D[F])(implicit
  genI: Generic.Aux[D[F], IL], // D[F] <=> IL
  restrict: UnaryTCConstraint[IL, F], // All elements of IL <=> D[F] are F[something]
  folder: RightFolder.Aux[IL, F[HNil], sequenceFold.type, F[OL]], // Can sequence IL to O[OL]
  genO: Generic.Aux[D[Id], OL] // OL <=> D[Id]
): F[D[Id]] = sequenceH(genI.to(i))(Applicative[F], restrict, folder).map(genO.from)

// Type inference is fixed here
Seq(DataF[Option](None   , None   ),
    DataF[Option](Some(1), None   ),
    DataF[Option](None   , Some(1)),
    DataF[Option](Some(1), Some(1))
).map(sequenceCase(_))
// None, None, None, Some(DataF[Id](1, 1))

Esto funciona, pero ¿y si

case class DataF2[F[_]](a: F[Int], b: String)
// b is NOT in F

Esto se complica, porque también es posible tener

case class DataF3[F[_]](a: F[Int], b: Option[String])

Y realmente no sabe qué hacer si F = Option, porque tiene sentido obtener Option[Int :: Option[String]] y Option[Int :: String]. Creo que lo implementaría comprimiendo las HList para las dos versiones de la clase juntas, luego doblando los pares para descubrir cómo pasar de una a la otra. Sin embargo, no lo implementaré aquí.

0
HTNW 20 oct. 2017 a las 12:14