为什么Scala编译器可以在对象外部提供隐式,而不能在对象内部提供?



标题可能相当模糊,但下面是代码:https://github.com/amorfis/why-no-implicit

所以有一个工具可以将Map[String, Any]转换为一个简单的case类。测试通过了,这段代码说明了它的全部内容:

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

这段代码可以工作。当提供简单的map

时,它很好地创建了case类实例但是,transform方法必须在外部调用。-就像在例子中一样。当我尝试将其移动到MapDecoder.to方法时-编译器抱怨缺少隐式。

所以我把MapDecoder.to中的代码改成:

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

:

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

,它停止工作。为什么呢?为什么隐式在一种情况下提供,而在另一种情况下没有?所有这些变化是,我想在其他地方调用transform方法,让MapDecoder.to返回case类,而不是一些变压器。

更新:

如果我想在一个我想转换的对象内实现to[A]方法该怎么办?我们将其命名为DataFrame,我希望这段代码能够工作:

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

问题是在这种情况下没有什么可以传递给apply。它也不可用父级(df.to[TargetData]())调用,因为编译器需要隐式的父级。不使用宏就能解决这个问题吗?

当编译器可以明确地在当前作用域中找到具有匹配类型的值时,可以提供隐式。

外部def to编译器看到你想要MapDecoder[TargetData]

里面看到的是MapDecoder[A],没有理由相信A =:= TargetData

在这种情况下,您必须将所有隐式作为to方法的参数传递。从你的代码中,它似乎必须是像

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

,但它会破坏人机工程学,因为你必须添加额外的参数,这些参数应该是推断的,但不能——在Scala 2中,你要么显式传递所有类型参数,要么不传递。有一些方法可以解决这个问题,比如将类型参数应用程序分成2个调用,像这样:

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]

将用作

MapDecoder.to[A](map)

被编译器编译为

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

它将非常类似于MapDecoder.to[TargetData](map).transform,但通过一个技巧,它将看起来更好。

@MateuszKubuszok回答了这个问题。我想对他的回答做一些评论。

添加隐式参数

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

你将.transform中的隐式解析从"now"即从to(其中A为抽象)的定义站点到"later";即to的呼叫点(其中ATargetData)。解决隐含的"现在";是不正确的,因为抽象的A不存在LabelledGeneric[A],只存在于case类,密封特征和类似它们。

这与implicitly[A](implicit a: A)的区别类似。

另一种延迟隐式解析的方法是内联。在Scala 3中,有内联的方法,以及其中使用的summonInline

在Scala 2中可以通过宏实现内联

// 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的PartiallyApplied模式(Applier)的解决方案似乎更容易(添加隐式参数是延迟隐式解析的更传统的方法,尽管可能存在无法向方法添加参数的情况)。


更新:

如果我想在一个对象内实现to[A]方法,我想转换?

您可以使用空参数列表定义apply

// implicit class or a class/object where you want to implement method inside
implicit class MapOps(map: Map[String, Any]) {
def as[A] = new Applier[A]
class Applier[A] {
def apply[R <: HList]()(implicit
gen: LabelledGeneric.Aux[A, R],
transformer: MapDecoder[R]
): Either[String, A] = new MapDecoderH[A](map).transform
}
}

(我重命名扩展方法为as,因为Map已经有.to)

并将其命名为map.as[TargetData]()

假设你不想像Spark-ish的df.to[TargetData]那样添加()。您总是可以定义自定义类型类。这是比方法(有或没有PartiallyApplied技巧)更灵活的解决方案

如何派生泛型。如果case类有类型参数- Shapeless

// type class
trait As[A] {
def as(map: Map[String, Any]): Either[String, A]
}
object As {
// materilizer
def apply[A: As]: As[A] = implicitly[As[A]]
// instances of the type class
implicit def makeAs[A, R <: HList](implicit
gen: LabelledGeneric.Aux[A, R],
transformer: MapDecoder[R]
): As[A] = new MapDecoderH[A](_).transform
}

implicit class MapOps(map: Map[String, Any]) {
// "implement method inside an object to transform"
def as[A: As]: Either[String, A] = As[A].as(map)
}

现在您可以像map.as[TargetData]一样调用没有()的方法。

所以你现在不需要宏了。我刚才提到的宏解决方案是为了更好地理解你的选择和更好地解释发生了什么。

最新更新