I have a sealed case class family that specify some rules, which are deserialized from an external source. I also have a typeclass with a few instances to perform the actual logic, like those:
import scala.util.Try
sealed trait ReaderConfig
case class Substring(begin: Int, end: Int) extends ReaderConfig
case class Regex(expr: String) extends ReaderConfig
trait Read[M[_], RC <: ReaderConfig] {
def apply(config: RC, raw: String): M[String]
}
object Read {
implicit val TryReadSubstring: Read[Try, Substring] = (config: Substring, raw: String) => Try {
raw.substring(config.begin, config.end)
}
implicit val TryReadRegex: Read[Try, Regex] = (config: Regex, raw: String) => Try {
config.expr.r.findFirstIn(raw).get
}
trait Helper[RC <: ReaderConfig] {
def as[M[_]](implicit read: Read[M, RC]): M[String]
}
def apply[RC <: ReaderConfig](config: RC)(raw: String) = new Helper[RC] {
override def as[M[_]](implicit read: Read[M, RC]): M[String] = read.apply(config,raw)
}
}
Now, while using it with concrete types there's no problem to find the correct implicit.
@ val ok: Try[String] = Read(Substring(0,1))("abc").as[Try]
ok: Try[String] = Success("a")
@ val Fail: Try[String] = Read(Substring(1000,9001))("abc").as[Try]
Fail: Try[String] = Failure(
java.lang.StringIndexOutOfBoundsException: String index out of range: 9001
)
When I have a val that has the upper trait as type (such as when I deserialize it as mentioned above), it fails to compile, as expected:
@ val config: ReaderConfig = Substring(0,1)
config: ReaderConfig = Substring(0, 1)
@ val fail2: Try[String] = Read(config)("abc").as[Try]
cmd8.sc:1: could not find implicit value for parameter read: $sess.cmd2.Read[scala.util.Try,$sess.cmd1.ReaderConfig]
val fail2: Try[String] = Read(config)("abc").as[Try]
^
Compilation Failed
The only solution I came up with is to write a function that will match up the actual types with the correct instances, such as:
val tryRead: ReaderConfig => String => Try[String] = {rc => raw => rc match {
case s: Substring => Read[Substring](s)(raw).as[Try]
case r: Regex => Read[Regex](r)(raw).as[Try]
}}
Then it happily compiles and I can use it in those circumstances.
@ tryRead(config)("abc")
res9: Try[String] = Success("a")
The trait is sealed, so the compiler should warn me about missing case
s, but obviously it will turn out to be very cumbersome as soon as I have more of those instances.
Is there some way that I could have this function automatically generated?
It is after all something that can be created by copying and pasting case
s and just filling in the variable pattern.
I think a more common pattern is to just create a single typeclass instance for the super type (ReaderConfig
), rather than one instance per subtype. Then use smart constructors to create ReaderConfig
s. For example, Cats provide some
and none
constructors for Option
: 42.some
returns Option[Int]
rather than Some[Int]
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With