scalaimplicitshapeless

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


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.

UPDATE:

What if I want to implement to[A] method inside an object I want to transform? Let's call it DataFrame, and I want this code to work:

val df: DataFrame = ...
df.to[TargetData] // There is no apply called here

The problem is in such case there is nothing to pass to apply. It is also not feasible to call it with parens (df.to[TargetData]()) because then the compiler requires implicits in parens. Is it even possible to solve it without using macros?


Solution

  • 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.