6
votes

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 cases, 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 cases and just filling in the variable pattern.

1

1 Answers

2
votes

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 ReaderConfigs. For example, Cats provide some and none constructors for Option: 42.some returns Option[Int] rather than Some[Int].