Kevin Meredith
Kevin Meredith

Reputation: 41909

Cleaning up `case class` with `Option` FIelds

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

Answers (3)

Yawar
Yawar

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

Gabor Juhasz
Gabor Juhasz

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

Travis Brown
Travis Brown

Reputation: 139038

You can do something like this with nested 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)
}

Now it's impossible to construct a Foo with all Nones. 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

Related Questions