【问题标题】:Scala: Class of Options to Option of ClassScala:选项类到类选项
【发布时间】:2017-10-20 23:10:46
【问题描述】:

有没有一种巧妙的方法可以将只有 Option 字段的类转换为包含在选项中的类似类?

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)

这只是乏味的打字,似乎应该/可能是一种标准方式,例如在 Cats 或 Scalaz 中?!

【问题讨论】:

  • 我怀疑您正在考虑 PartialFunction 的提升和取消:scala-lang.org/api/current/scala/Function$.html希望对您有所帮助
  • @Pavel 我不太确定我是否关注。
  • 有一个标准的 scala 功能,它用 Option 和返回来包装函数结果,不确定这是否适合您的用例
  • 如果你想变得非常通用(将任何只有 Option 字段的案例类转换为具有完全类型安全的相同形状的另一个案例类),你可以用 shapeless 编写它。基本上你需要做的是case class Data -> HList : *-&gt;*[Option]#λ -> fold: Option[HList] -> Generic.materialize[DataX]。如果您想这样做,我可以尝试在答案中实现它。但总的来说,这不是一些标准的 scalaZ 方式:)
  • @ISeeVoices 你能做到吗?我真的很感激这一点,因为我相信shapeless 是解决问题的方法,但我不了解库以实际提出解决方案。

标签: scala


【解决方案1】:

在我们开始实施之前,有两个小提示。

1) 使用 shapeless,您可以将任何案例类(或 ADT)转换为其通用表示并返回。
- 如果是案例类 - 通用表示将是 HList
- 如果是 ADT(密封特征/抽象类,对象/案例类扩展它) - 通用表示将是 Coproduct

您可以进行此类转换,因为它们是同构的。进行此类转换很有用,因为 HList - 具有 List 的属性,因此您可以通用地做一些不错的事情,例如映射、折叠、过滤等等。

2) 函数式编程中有一个抽象,称为 Sequence(或者有时更普遍的 Traversable Functor),它将F[G[A]] 转换为G[F[A]],给定G - 是Applicative。因此,例如,您可以将 List[Option[A]] -> Option[List[A]]Future[Either[?, A]] 转换为 Either[? Future[A]] 等。这正是您想要实现的目标。

所以计划是:

  1. 将您的案例类转换为HList,其中包含Options 你的情况。

  2. HList 上使用序列操作(你会得到Option[Hlist]

  3. 映射Option 以将HList 转换为您的下一个案例类表示。

我正要自己实现Sequencer,但发现那部分已经实现了https://github.com/typelevel/shapeless-contrib ("org.typelevel" %% "shapeless-scalaz" % "0.4.0")。您可以查看 Sequencer 的实现,不要害怕,一开始它看起来像是完全的魔法。在查看了无形的类型类的几个实现之后,它开始变得有意义。

所以非泛型实现很简单:

  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)
  }

这样我们就失去了流派性,所以让我们将更多类型拉到函数中。 我们绝对需要提供
输入类型:I
输出类型:O.
我们还需要提供通用表示,无形隐含地提供:Generic.Aux[I, Ri] Ri - 将是 HList,在您的情况下由 Option 组成。
然后你需要一个序列器,它将你的HList 转换为另一个HListOption(一般来说,HList of FsF[HList]):Sequencer.Aux[A, F[Ro]],其中F - 是一个函子。

所以最终实现如下:。

  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
  }

其中,F - 是任何 Applicative,不一定是 Option。 问题是,最终,scala 编译器无法很好地推断类型,您需要手动推断它们:

    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))
    )

发生这种情况是因为Sequencer 仅适用于HList,但通用表示也可以是Coproduct,因此您需要提供证据,证明您的通用分解将是HList。理想情况下,您只需要指定输出类型和应用类型:

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

我仍然认为这是可能的,但我只是还没弄清楚怎么做。我会尝试再调整一下,也许我会找到方法。

UPD 我设法做到了,结束代码是:

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))

【讨论】:

    【解决方案2】:

    这个答案有点来自this

    首先,这可以推广到每个Applicative 函子,所以我不会专门使用Option,除了示例。其次,这对所有 Generic/HList 好东西都使用了 shapeless。

    你想sequence 超过HLists。这是作为折叠实现的。这是累加函数:

    // (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)
      }
    }
    

    那么,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
    

    现在,我们使用Generic 将其拖出HListland。首先,我们需要在DataDataX 之间建立某种关系,否则我们会从HList Int :: Int :: HNil 中查找DataX,这是行不通的。在这种情况下,我认为最好将 Data 参数化为一些构造函数 F,但我认为类型类也可以工作:

    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))
    

    这可行,但如果

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

    这变得复杂了,因为它也可能有

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

    如果F = Option,你真的不知道该怎么做,因为获得Option[Int :: Option[String]]Option[Int :: String] 是有意义的。我想我会通过将两个版本的类的HLists 压缩在一起来实现它,然后将它们折叠起来以找出如何从一个到另一个。不过我不会在这里实现它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-31
      • 1970-01-01
      • 2015-04-03
      • 1970-01-01
      • 2017-02-08
      • 2013-09-07
      相关资源
      最近更新 更多