In my code i've a class that to check if it's valid i should eval if at least one of the possible field combinations exist (by exist i mean that each field of the combination should not be empty). Example:
case class Test( a: Option[String]
, b: Option[String]
, c: Option[String]
, d: Option[String]
, e: Option[Double]
, f: Option[Double])
To be "valid", at least one of the following field combinations must exist ("a,b,c","a,d,e","a,f")
I was trying to do this with scala cats library but i'm kinda lost. Any suggestion would be highly appreciated.
If you want to validate it you can just:
case class Test( a: Option[String]
, b: Option[String]
, c: Option[String]
, d: Option[String]
, e: Option[Double]
, f: Option[Double]) {
// "a,b,c","a,d,e","a,f"
def isValid = (a.isDefined && b.isDefined && c.isDefined) ||
(a.isDefined && d.isDefined && e.isDefined) ||
(a.isDefined && f.isDefined)
}
If you want to make sure that you can only create it if one of field is defined you would have to use a smart constructor
sealed abstract case class Test private ( a: Option[String]
, b: Option[String]
, c: Option[String]
, d: Option[String]
, e: Option[Double]
, f: Option[Double])
object Test {
def create( a: Option[String]
, b: Option[String]
, c: Option[String]
, d: Option[String]
, e: Option[Double]
, f: Option[Double]): Either[String, Test] =
if ((a.isDefined && b.isDefined && c.isDefined) ||
(a.isDefined && d.isDefined && e.isDefined) ||
(a.isDefined && f.isDefined))
Right(new Test(a, b, c, d, e, f) {})
else
Left("All arguments are empty")
}
Alternatively use ADT ensuring that one of fields is defined:
sealed trait Test extends Product with Serializable
object Test {
final case class Case1( a: String
, b: String
, c: String
, d: Option[String]
, e: Option[Double]
, f: Option[Double]) extends Test
final case class Case2( a: String
, b: Option[String]
, c: Option[String]
, d: String
, e: String
, f: Option[Double]) extends Test
final case class Case3( a: String
, b: Option[String]
, c: Option[String]
, d: Option[String]
, e: Option[Double]
, f: Double) extends Test
}
You could use Cats here... but for what? You aren't combining a tuple or collection of Options into a single Option. You don't swap F[Option[X]
into Option[F[X]
or the other way round. There are no side effects, mappings, traversions, constructions of new objects from smaller objects embedded in some context, etc. You can try doing things like
implicit val booleanMonoid: Monoid[Boolean] = new Monoid[Boolean] {
def combine(a: Boolean, b: Boolean) = a && b
def empty = true
}
def isValid = List(a, b, c).foldMap(_.isDefined) ||
List(a, d, e).foldMap(_.isDefined) ||
List(a, f).foldMap(_.isDefined)
or maybe even
def isValid = (
(a, b, c).tupled.void orElse (a, d, e).tupled.void orElse (a, f).tupled.void
).isDefined
but that is hardly any better than
def isValid = List(a, b, c).exists(_.isDefined) ||
List(a, d, e).exists(_.isDefined) ||
List(a, f).exists(_.isDefined)
achievable in vanilla Scala. I guess you could simply the notation by using some Ring
defined on Option[_]
to use *
and +
:
implicit val ring: Ring[Option[_]] = ... // yup, existential type here
def isValid = ((a * b * c) + (a * d * e) + (a * f)).isDefined
(which would require typelevel algebra) but for using it in just one place I wouldn't see the gain.