Очистка `case class` с помощью полей `Option`

#scala #shapeless

#scala #бесформенный

Вопрос:

Учитывая:

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

Я хотел бы разрешить создание a только Foo только в том случае, Some если хотя бы один из его аргументов является None , т. е. не все поля являются в качестве в, ,.

Потребовалось бы совсем немного кода, чтобы написать алгебраический тип данных, а затем создать подклассы для каждого варианта:

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

Есть ли более чистый, то есть с меньшим количеством кода, способ решения моей проблемы с помощью shapeless ?

Ответ №1:

Вы можете сделать что-то подобное с вложенными Ior s:

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

Теперь невозможно построить a Foo со всеми None s. Вы также могли бы сделать конструктор класса case закрытым и сделать так, чтобы Ior логика выполнялась в альтернативном конструкторе сопутствующего объекта, что сделало бы сопоставление с шаблоном немного приятнее, но это также сделало бы пример немного длиннее.

К сожалению, это довольно неуклюже в использовании. Чего вы действительно хотите, так это обобщения Ior таким же образом, shapeless.Coproduct что является обобщением Either . Однако я лично не знаю о готовой версии чего-либо подобного.

Ответ №2:

Благодаря sealed abstract case class трюку, который недавно обнародовал Роб Норрис, вы можете сохранить характеристики своего Foo класса case, но также предоставить свой собственный интеллектуальный конструктор, который возвращает значение 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))
    }
}
  

Ответ №3:

Я бы рекомендовал предоставить шаблон builder для вашего класса. Это особенно полезно, если пользователи вашей библиотеки обычно указывают только некоторые из множества необязательных параметров. И в качестве бонуса с отдельными методами для каждого параметра им не придется оборачивать все в Some

Вы можете использовать единственный параметр типа для класса, чтобы отметить, является ли он полным (т. Е. имеет хотя бы один 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