#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