使用 `Option` 字段清理 `case class`

Cleaning up `case class` with `Option` FIelds

鉴于:

case class Foo(a: Option[Int], b: Option[Int], c: Option[Int], d: Option[Int])

我只想允许构造一个 Foo 只有 如果至少有一个参数是 Some,即不是所有字段都是 None.

编写一个代数数据类型,然后为每个变体创建子类会是相当多的代码:

sealed trait Foo
case class HasAOnly(a: Int)      extends Foo
case class HasAB(a: Int, b: Int) extends Foo
// etc...

是否有更简洁的方法,即更少的代码,可以使用 shapeless 解决我的问题?

你可以用嵌套的 Iors 做这样的事情:

import cats.data.Ior

case class Foo(iors: Ior[Ior[Int, Int], Ior[Int, Int]]) {
  def a: Option[Int] = iors.left.flatMap(_.left)
  def b: Option[Int] = iors.left.flatMap(_.right)
  def c: Option[Int] = iors.right.flatMap(_.left)
  def d: Option[Int] = iors.right.flatMap(_.right)
}

现在不可能构造一个包含所有 NoneFoo。您还可以将 case class 构造函数设为私有,并让 Ior 逻辑发生在伴随对象的替代构造函数中,这将使模式匹配更好一些,但它也会使示例有点更长。

不幸的是,这使用起来有点笨拙。您真正想要的是 Ior 的泛化,就像 shapeless.CoproductEither 的泛化一样。不过,我个人并不知道类似的现成版本。

我建议为您的 class 提供构建器模式。如果您的库的用户通常只指定许多可选参数中的一些,这将特别有用。作为每个参数的单独方法的奖励,他们不必将所有内容包装在 Some

您可以在 class 上使用单个类型参数来标记它是否完整(即至少有一个 Some 参数)并且您可以使用隐式参数在构建方法上强制执行此操作.

sealed trait Marker
trait Ok extends Marker
trait Nope extends Markee

case class Foo private(a: Option[Int], b: Option[Int], c: Option[Int], d: Option[Int])

object Foo{
  case class Builder[T <: Marker](foo: Foo){
    def a(x:Int) = Builder[Ok](foo = foo.copy(a=Some(x)))
    def b(x:Int) = Builder[Ok](foo = foo.copy(b=Some(x)))
    // ...

    def build(implicit ev: T <:< Ok) = foo
  }

  def create = Builder[Nope](Foo(None, None, None, None))
}

我之前尝试过类型安全的构建器。这个要点有一个更复杂的例子,尽管它也跟踪哪个字段已设置,以便稍后可以在不安全调用 Option.get 的情况下提取它。 https://gist.github.com/gjuhasz86/70cb1ca2cc057dac5ba7

多亏了 Rob Norris 最近 publicisedsealed abstract case class 技巧,您可以保留 Foo 案例的特征 class 而且还可以提供您自己的智能构造函数returns 一个 Option[Foo] 取决于给定的参数是否符合您的所有标准:

sealed abstract case class Foo(
  a: Option[Int], b: Option[Int], c: Option[Int], d: Option[Int])

object Foo {
  private class Impl(
    a: Option[Int], b: Option[Int], c: Option[Int], d: Option[Int])
    extends Foo(a, b, c, d)

  def apply(
    a: Option[Int],
    b: Option[Int],
    c: Option[Int],
    d: Option[Int]): Option[Foo] =
    (a, b, c, d) match {
      case (None, None, None, None) => None
      case _ => Some(new Impl(a, b, c, d))
    }
}