0

In the following example I would like to be capable to use an implicit type class - Process - wit a trait as input. But the compilator does not recognize any implicit in that case. I was expecting as Input is a sealed trait and I made the implicits implementation for both InputA and InputB that during the runtime it select by itself accordingly to the type provided.

  sealed trait Input
  final case class InputA(i: Int) extends Input
  final case class InputB(s: String) extends Input

  trait Process[U] {
    def give(u :U): U
  }

  object Process {
    implicit val processInputA: Process[InputA] = (u: InputA) => u.copy(i = u.i+10)
    implicit val processInputB: Process[InputB] = (u: InputB) => u.copy(s = u.s+"add")
  }
  
  object UseProcess {
    def run[U](u: U)(implicit process: Process[U]): U = process.give(u)
  }
  val g: Input = InputB("1")
  val res3: Input = UseProcess.run(g). ==>> No implicits found for parameter process: Process[Input]

Is there a way to have it working or type class work only with implementation on concrete type.

Thanks in advance for your answer

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
gwenael
  • 35
  • 5

2 Answers2

2

It is not about trait vs class.

The method you are calling looks like this

run(x: Input)(implicit y: Process[Input]

In order to call, the compiler needs to find an implicit instance of type Process[Input] somewhere in scope, but there aren't any.

Note, that Process[InputB] is not a subclass of Process[Input] because Process is invariant in its parameter. You could try to make it covariant like trait Process[+U] ... but that won't work in this case, because function parameters are contravariant (you can't have give(u: U) if U is covariant).

If you could make your trait covariant though, it still wouldn't work, because now both processInputA and processInputB would fit, so the resolution would be ambiguous.

Basically, upcasting to Input at g: Input is a bad idea. If you are writing a function where you want to use the trait for abstraction, you should parameterize it:

    def foo[T <: Input : Process](g: T) = UseProcess.run(g) 

^^ this should work.

Dima
  • 39,570
  • 6
  • 44
  • 70
  • **Comments have been [moved to chat](https://chat.stackoverflow.com/rooms/251847/discussion-on-answer-by-dima-scala-how-to-derivate-a-type-class-on-a-trait); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed. – blackgreen Feb 13 '23 at 23:42
  • Ok, so summarizing our [discussion](https://chat.stackoverflow.com/rooms/251847/discussion-on-answer-by-dima-scala-how-to-derivate-a-type-class-on-a-trait) with you, @Dima, here is my humble suggesting an improvement (not for discussion here). I guess the section starting from *"upcasting to `Input` at `g: Input` is a bad idea"* is not completely accurate. `val g: Input = InputB("1")` is just MCVE, different scenarios can be behind this line. Sometimes the only statically known type being the parent type is unavoidable. Feel free to ignore the suggestion. Thanks. – Dmytro Mitin Feb 14 '23 at 08:06
1

I was expecting as Input is a sealed trait and I made the implicits implementation for both InputA and InputB that during the runtime it select by itself accordingly to the type provided.

Implicits (instances of a type class) are resolved at compile time (not runtime), during type checking (compilation phase typer).

Runtime vs. Compile time

In val g: Input = InputB("1") it becomes known that g is an InputB and not InputA only at runtime. At compile time it's only known that g is an Input.

So either let compiler know at compile time that g is InputB

val g: InputB = InputB("1")

or define an instance of the type class for Input

implicit val processInput: Process[Input] = {
  case u: InputA => implicitly[Process[InputA]].give(u)
  case u: InputB => implicitly[Process[InputB]].give(u)
}
val g: Input = InputB("1")
val res3: Input = UseProcess.run(g) // InputB(1add)

As you can see, if your logic is based on a runtime value, you need pattern matching (occurring mostly at runtime) rather than implicit (type class) resolution. Implicits (type classes) are kind of "pattern matching" at compile time.

You can also derive an instance Process[Input] based on instances Process[InputA], Process[InputB] rather than define it manually for every sealed-trait hierarchy.

// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.{:+:, CNil, Coproduct, Inl, Inr, Generic}

implicit def genericProcess[A, C <: Coproduct](implicit
  generic: Generic.Aux[A, C],
  process: Process[C]
): Process[A] = a => generic.from(process.give(generic.to(a)))

implicit def cNilProcess: Process[CNil] = _.impossible

implicit def cConsProcess[H, T <: Coproduct](implicit
  hProcess: Process[H],
  tProcess: Process[T]
): Process[H :+: T] =
  _.eliminate(h => Inl(hProcess.give(h)), t => Inr(tProcess.give(t)))
val g: Input = InputB("1")
val res3: Input = UseProcess.run(g) // InputB(1add)

Use the lowest subtype in a typeclass?

Type class instance for case objects defined in sealed trait

How to accept only a specific subtype of existential type?

Covariant case class mapping to its base class without a type parameter and back

An alternative to defining an instance Process[Input] at compile time could be to run compilation and resolve instances at runtime. Surely, this approach is less type-safe. If there is no instance TC[I] the following code fails at runtime.

// libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
import scala.reflect.runtime.{currentMirror => rm}
import scala.reflect.runtime.universe._
// libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value
import scala.tools.reflect.ToolBox

val tb = rm.mkToolBox()

def getInstance[TC[_]] = new PartiallyAppliedGetInstance[TC]

class PartiallyAppliedGetInstance[TC[_]] {
  def apply[I](i: I)(implicit wtt: WeakTypeTag[TC[_]]): TC[I] =
    tb.eval(
      tb.untypecheck(
        tb.inferImplicitValue(
          appliedType(
            weakTypeOf[TC[_]].typeConstructor,
            rm.classSymbol(i.getClass).toType
          )
        )
      )
    ).asInstanceOf[TC[I]]
}
val g: Input = InputB("1")
val res3: Input = UseProcess.run(g)(getInstance[Process](g)) // InputB(1add)

Scala upper type bounds for reflectioned type

Is there anyway, in Scala, to get the Singleton type of something from the more general type?

Load Dataset from Dynamically generated Case Class

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • `implicit val processInput: Process[Input]` your implementation returns `Input`, not `Process[Input]` ... To make it return `Process`, you'd have to make it covariant, but that doesn't work because it has parameter in contravariant position ... – Dima Feb 14 '23 at 13:54
  • @Dima *"your implementation returns `Input`, not `Process[Input]`"* No, it returns exactly `Process[Input]` because `Process` is a [SAM](https://www.scala-lang.org/files/archive/spec/2.13/06-expressions.html#sam-conversion). You can check that the code compiles https://scastie.scala-lang.org/DmytroMitin/BoxmbAAtR7aIowcVwelD0Q/2 *"you'd have to make it covariant"* No, I don't. – Dmytro Mitin Feb 14 '23 at 15:13
  • @Dima Desugared implementation is https://scastie.scala-lang.org/DmytroMitin/BoxmbAAtR7aIowcVwelD0Q/5 Derivation also is working https://scastie.scala-lang.org/DmytroMitin/BoxmbAAtR7aIowcVwelD0Q/3 – Dmytro Mitin Feb 14 '23 at 15:26
  • yeah, ok, I misread it – Dima Feb 14 '23 at 15:58