Reputation: 41909
Given:
case class Foo(a: Option[Int], b: Option[Int], c: Option[Int], d: Option[Int])
I'd like to only allow constructing a Foo
only if at least one of its arguments is Some
, i.e. not all fields are None
.
It would be quite a bit of code to write an Algebraic Data Type, and then make sub-classes for each variant:
sealed trait Foo
case class HasAOnly(a: Int) extends Foo
case class HasAB(a: Int, b: Int) extends Foo
// etc...
Is there a cleaner, i.e. less code, way to address my problem using shapeless
?
Upvotes: 9
Views: 715
Reputation: 11607
Thanks to the sealed abstract case class
trick which Rob Norris recently publicised, you can keep the characteristics of your Foo
case class but also provide your own smart constructor which returns an Option[Foo]
depending on whether the given arguments pass all your criteria or not:
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))
}
}
Upvotes: 5
Reputation: 317
I would recommend providing a builder pattern for your class. This is especially useful if the users of your library typically only specify some of the many optional parameters. And as a bonus with the separate methods per parameter they won't have to wrap everything in Some
You can use a single type parameter on the class to mark if it's complete (i.e. has at least one Some
parameter) and you can enforce this on the build method with an implicit parameter.
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))
}
I have experimented with a type-safe builder before. This gist has a more complex example, although that also keeps track which field has been set, so that it can be extracted later without an unsafe call to Option.get
.
https://gist.github.com/gjuhasz86/70cb1ca2cc057dac5ba7
Upvotes: 0
Reputation: 139038
You can do something like this with nested 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)
}
Now it's impossible to construct a Foo
with all None
s. You could also make the case class constructor private and have the Ior
logic happen in an alternative constructor on the companion object, which would make pattern matching a little nicer, but it would also make the example a little longer.
Unfortunately this is kind of clunky to use. What you really want is a generalization of Ior
in the same way that shapeless.Coproduct
is a generalization of Either
. I'm not personally aware of a ready-made version of anything like that, though.
Upvotes: 6