Reputation: 9521
I'm working on some system that uses external configuration and does some action depending upon the configuration provided. I have the following traits (methods omitted for simplicity):
sealed trait Tr[T]
case object Tr1 extends Tr[String]
case object Tr2 extends Tr[Int]
case object Tr3 extends Tr[Array[Byte]]
sealed trait Trr[T]
case object Trr1 extends Trr[String]
case object Trr2 extends Trr[Int]
case object Trr3 extends Trr[Array[Byte]]
trait Trrr[T]
case object Trrr1 extends Trrr[(String, Int)]
case object Trrr2 extends Trrr[(Int, String)]
case object Trrr3 extends Trrr[(Int, Int)]
case object Trrr4 extends Trrr[(String, String)]
case object Trrr5 extends Trrr[(String, Array[Byte])]
And the action:
def doUsefulAction[T, F](t1: Tr[T], t2: Trr[F], t3: Trrr[(T, F)]) = {
//...
}
The problem is the method invokation depends on the configuration:
def invokeWithConfig[T1, T2, T3](cfgTr1: String, cfgTr2: String, cfgTr3: String) = cfgTr1 match {
case "1" =>
cfgTr2 match {
case "1" =>
cfgTr3 match {
case "4" => doUsefulAction(Tr1, Trr1, Trrr4)
case _ => throw new IllegalArgumentException
}
case "2" =>
cfgTr3 match {
case "1" => doUsefulAction(Tr1, Trr2, Trrr1)
case _ => throw new IllegalArgumentException
}
case "3" =>
cfgTr3 match {
case "5" => doUsefulAction(Tr1, Trr3, Trrr5)
case _ => throw new IllegalArgumentException
}
case _ => throw new IllegalArgumentException
}
case "2" =>
//same boilerplate as above
case "3" =>
//same boilerplate as above
case _ => throw new IllegalArgumentException
}
The thing is there is tons of boilerplate with pattern matching. And this is just 3 traits. In case of 10 it becomes unreadable.
Is there a way to handle such a configuration yet staying types and avoid instanceOf
?
Maybe macro
can be useful here?
Upvotes: 1
Views: 156
Reputation: 1016
https://scalafiddle.io/sf/Z2NGo9y/0
This is a possible solution but it's not optimal yet you could eliminate more boilerplate by introducing something like shapeless/magnolia/scalaz-deriving to derive the implementations for the fromString
implementations.
But really Validated
and Applicative
are your friends here
EDIT: As requested the code here
import cats._
import cats.implicits._
import cats.data.Validated._
import cats.data.ValidatedNel
import cats.data.NonEmptyList
sealed trait Tr[T]
case object Tr1 extends Tr[String]
case object Tr2 extends Tr[Int]
case object Tr3 extends Tr[Array[Byte]]
object Tr{
def fromString(s:String):ValidatedNel[Throwable, Tr[_]] = s match {
case "1" => Tr1.validNel
case "2" => Tr2.validNel
case "3" => Tr3.validNel
case _ => new RuntimeException(s"$s is not a valid Tr").invalidNel
}
}
sealed trait Trr[T]
case object Trr1 extends Trr[String]
case object Trr2 extends Trr[Int]
case object Trr3 extends Trr[Array[Byte]]
object Trr{
def fromString(s:String):ValidatedNel[Throwable, Trr[_]] = s match {
case "1" => Trr1.validNel
case "2" => Trr2.validNel
case "3" => Trr3.validNel
case _ => new RuntimeException(s"$s is not a valid Trr").invalidNel
}
}
trait Trrr[T, T1]
case object Trrr1 extends Trrr[String, Int]
case object Trrr2 extends Trrr[Int, String]
case object Trrr3 extends Trrr[Int, Int]
case object Trrr4 extends Trrr[String, String]
case object Trrr5 extends Trrr[String, Array[Byte]]
object Trrr{
def fromString(s:String):ValidatedNel[Throwable, Trrr[_, _]] = s match {
case "1" => Trrr1.validNel
case "2" => Trrr2.validNel
case "3" => Trrr3.validNel
case "4" => Trrr4.validNel
case "5" => Trrr5.validNel
case _ => new RuntimeException(s"$s is not a valid Trrr").invalidNel
}
}
def doUseful[T1, T2](tr:Tr[T1], trr:Trr[T2], trrr:Trrr[T1,T2]):String = "called"
def dispatch(s1:String, s2:String, s3:String):Either[Throwable, String] = (
Tr.fromString(s1),
Trr.fromString(s2),
Trrr.fromString(s3),
)
.tupled
.leftMap(
errs => new RuntimeException(
Foldable[NonEmptyList].intercalate(errs.map(_.getMessage),"\n")
)
)
.toEither
.flatMap {
case (a@Tr1, b@Trr2, c@Trrr1) => Right(doUseful(a,b,c))
case _ => Left(new RuntimeException("non mapped possibility"))
//note the line below won't compile because there's no valid combination of T1, T2 to call doUseful
//case (a@Tr1, b@Trr2, c@Trrr4) => doUseful(a,b,c)
}
println(dispatch("1", "2", "1"))
println(dispatch("1", "2", "15"))
println(dispatch("1", "20", "15"))
Upvotes: 1