Home > Blockchain >  Why the Scala compiler can provide implicit outside of object, but cannot inside?
Why the Scala compiler can provide implicit outside of object, but cannot inside?

Time:11-15

The title might be quite vague, but here is the code: https://github.com/amorfis/why-no-implicit

So there is a tool to transform Map[String, Any] to a simple case class. The tests pass and this piece of code illustrates what it is all about:

        case class TargetData(
          groupId: String,
          validForAnalysis: Boolean,
          applicationId: Int
        )

        val map = Map(
          "groupId" -> "123456712345",
          "applicationId" -> 31,
          "validForAnalysis" -> true
        )

        val transformed: TargetData = MapDecoder.to[TargetData](map).transform

This code works. It nicely creates the case class instance when provided the simple map

However, the transform method has to be called "outside" - just like in the example. When I try to move it to the MapDecoder.to method - the compiler complains about the missing implicit.

So I change the code in MapDecoder.to from this:

def to[A](map: Map[String, Any]) = new MapDecoderH[A](map)

to this:

def to[A](map: Map[String, Any]) = new MapDecoderH[A](map).transform

and it stops working. Why is that? Why the implicit is provided in one case but not in the other? All that changes is that I want to call the transform method in other place to have MapDecoder.to returning the case class not some transformer.

CodePudding user response:

Implicit can be provided when the compiler can unambiguously find a value in the current scope with matching type.

Outside def to compiler sees that you want MapDecoder[TargetData].

Inside it sees MapDecoder[A] and have no reason to believe that A =:= TargetData.

In such situation you'd have to pass all the implicits as arguments of to method. From your code it seems it would have to be something like

def to[A, R <: HList](map: Map[String, Any])(implicit
  gen: LabelledGeneric.Aux[A, R],
  transformer: MapDecoder[R]
) = new MapDecoderH[A](map).transform

but it would break the ergonomy, since you'd have to add additional parameter which should be inferred but cannot - in Scala 2 you are passing all type arguments explicitly or none. There are ways to work around it like by splitting the type param application into 2 calls like this:

class Applier[A] {

  def apply[R <: HList](map: Map[String, Any])(implicit
    gen: LabelledGeneric.Aux[A, R],
    transformer: MapDecoder[R]
  ) = new MapDecoderH[A](map).transform
}

def to[A] = new Applier[A]

which would be used as

MapDecoder.to[A](map)

desugared by compiler to

MapDecoder.to[A].apply[InferredR](map)(/*implicit*/gen, /*implicit*/transformer)

It would be very similar to MapDecoder.to[TargetData](map).transform but through a trick it would look much nicer.

CodePudding user response:

@MateuszKubuszok answered the question. I'll just make a couple of comments to his answer.

Adding implicit parameters

def to[A](map: Map[String, Any]) = new MapDecoderH[A](map).transform

// ===>

def to[A, R <: HList](map: Map[String, Any])(implicit
  gen: LabelledGeneric.Aux[A, R],
  transformer: MapDecoder[R]
) = new MapDecoderH[A](map).transform

you postpone implicit resolution in .transform from "now" i.e. the definition site of to (where A is abstract) to "later" i.e. the call site of to (where A is TargetData). Resolving implicits "now" is incorrect since LabelledGeneric[A] doesn't exist for abstract A, only for case classes, sealed traits, and like them.

This is similar to the difference implicitly[A] vs. (implicit a: A).

Another way of postponing implicit resolution is inlining. In Scala 3 there are inline methods for that along with summonInline used in them.

In Scala 2 inlining can be achieved with macros

// libraryDependencies  = "org.scala-lang" % "scala-reflect" % "2.13.10"
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

def to[A](map: Map[String, Any]): Either[String, A] = macro toImpl[A]

def toImpl[A: c.WeakTypeTag](c: blackbox.Context)(map: c.Tree): c.Tree = {
  import c.universe._
  q"new MapDecoderH[${weakTypeOf[A]}]($map).transform"
}

@MateuszKubuszok's solution with PartiallyApplied pattern (Applier) seems to be easier (adding implicit parameters is more conventional way to postpone implicit resolution although there can be situations when you just can't add parameters to a method).

  • Related