克服高阶函数函数参数的 Scala 类型擦除
Overcoming Scala Type Erasure For Function Argument of Higher-Order Function
本质上,我想做的是为自定义 class 编写 "map" 的重载版本,这样每个版本的 map 仅在传递给它的函数类型上有所不同。
这就是我想做的事情:
object Test {
case class Foo(name: String, value: Int)
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map(func: Int => Int) = Foo(f.name, func(f.value))
def map(func: String => String) = Foo(func(f.name), f.value)
}
def main(args: Array[String])
{
def square(n: Int): Int = n * n
def rev(s: String): String = s.reverse
val f = Foo("Test", 3)
println(f.string)
val g = f.map(rev)
val h = g.map(square)
println(h.string)
}
}
当然,由于类型擦除,这是行不通的。任一版本的 map 都可以单独使用,并且它们可以以不同的方式命名,并且一切正常。然而,用户可以简单地根据传递给它的函数类型调用正确的地图函数是非常重要的。
在寻找解决此问题的方法时,我遇到了 TypeTags。这是我想出的代码,我认为它接近正确,但当然不能正常工作:
import scala.reflect.runtime.universe._
object Test {
case class Foo(name: String, value: Int)
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map[A: TypeTag](func: A => A) =
typeOf[A] match {
case i if i =:= typeOf[Int => Int] => f.mapI(func)
case s if s =:= typeOf[String => String] => f.mapS(func)
}
def mapI(func: Int => Int) = Foo(f.name, func(f.value))
def mapS(func: String => String) = Foo(func(f.name), f.value)
}
def main(args: Array[String])
{
def square(n: Int): Int = n * n
def rev(s: String): String = s.reverse
val f = Foo("Test", 3)
println(f.string)
val g = f.map(rev)
val h = g.map(square)
println(h.string)
}
}
当我尝试 运行 此代码时,出现以下错误:
[error] /src/main/scala/Test.scala:10: type mismatch;
[error] found : A => A
[error] required: Int => Int
[error] case i if i =:= typeOf[Int => Int] => f.mapI(func)
[error] ^
[error] /src/main/scala/Test.scala:11: type mismatch;
[error] found : A => A
[error] required: String => String
[error] case s if s =:= typeOf[String => String] => f.mapS(func)
func 确实是 A => A 类型,那么我如何告诉编译器我在 运行 时匹配正确的类型?
非常感谢。
在您定义的 map
中,类型 A
表示函数的参数和结果。那么 func
的类型就是 A => A
。然后你基本上检查一下,例如 typeOf[A] =:= typeOf[Int => Int]
。这意味着 func
将是 (Int => Int) => (Int => Int)
,这是错误的。
使用 TypeTag
s 解决此问题的方法之一如下所示:
def map[T, F : TypeTag](func: F)(implicit ev: F <:< (T => T)) = {
func match {
case func0: (Int => Int) @unchecked if typeOf[F] <:< typeOf[Int => Int] => f.mapI(func0)
case func0: (String => String) @unchecked if typeOf[F] <:< typeOf[String => String] => f.mapS(func0)
}
}
不过你必须用下划线来调用它:f.map(rev _)
。它可能会抛出匹配错误。
也许可以改进此代码,但我建议您做得更好。克服重载方法参数类型擦除的最简单方法是使用 DummyImplicit
。只需向某些方法添加一个或多个隐式 DummyImplicit
参数:
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map(func: Int => Int)(implicit dummy: DummyImplicit) = Foo(f.name, func(f.value))
def map(func: String => String) = Foo(func(f.name), f.value)
}
克服方法参数类型擦除的更通用的方法是使用the magnet pattern。这是一个工作示例:
sealed trait MapperMagnet {
def map(foo: Foo): Foo
}
object MapperMagnet {
implicit def forValue(func: Int => Int): MapperMagnet = new MapperMagnet {
override def map(foo: Foo): Foo = Foo(foo.name, func(foo.value))
}
implicit def forName(func: String => String): MapperMagnet = new MapperMagnet {
override def map(foo: Foo): Foo = Foo(func(foo.name), foo.value)
}
}
implicit class FooUtils(f: Foo) {
def string = s"${f.name}: ${f.value}"
// Might be simply `def map(func: MapperMagnet) = func.map(f)`
// but then it would require those pesky underscores `f.map(rev _)`
def map[T](func: T => T)(implicit magnet: (T => T) => MapperMagnet): Foo =
magnet(func).map(f)
}
这是有效的,因为当您调用 map
时,隐式 magnet
在编译时使用完整类型信息解析,因此不会发生擦除,也不需要运行时类型检查。
我认为磁铁版本更干净,作为奖励它不使用任何运行时反射调用,你可以在参数中不带下划线调用 map
:f.map(rev)
,而且它不能抛出运行时匹配错误。
更新:
现在想起来,这里的 magnet 并不比完整的类型类简单,但它可能更能表达意图。不过,与 typeclass 相比,这是一个鲜为人知的模式。无论如何,为了完整性,这里是使用类型类模式的相同示例:
sealed trait FooMapper[F] {
def map(foo: Foo, func: F): Foo
}
object FooMapper {
implicit object ValueMapper extends FooMapper[Int => Int] {
def map(foo: Foo, func: Int => Int) = Foo(foo.name, func(foo.value))
}
implicit object NameMapper extends FooMapper[String => String] {
def map(foo: Foo, func: String => String) = Foo(func(foo.name), foo.value)
}
}
implicit class FooUtils(f: Foo) {
def string = s"${f.name}: ${f.value}"
def map[T](func: T => T)(implicit mapper: FooMapper[T => T]): Foo =
mapper.map(f, func)
}
本质上,我想做的是为自定义 class 编写 "map" 的重载版本,这样每个版本的 map 仅在传递给它的函数类型上有所不同。
这就是我想做的事情:
object Test {
case class Foo(name: String, value: Int)
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map(func: Int => Int) = Foo(f.name, func(f.value))
def map(func: String => String) = Foo(func(f.name), f.value)
}
def main(args: Array[String])
{
def square(n: Int): Int = n * n
def rev(s: String): String = s.reverse
val f = Foo("Test", 3)
println(f.string)
val g = f.map(rev)
val h = g.map(square)
println(h.string)
}
}
当然,由于类型擦除,这是行不通的。任一版本的 map 都可以单独使用,并且它们可以以不同的方式命名,并且一切正常。然而,用户可以简单地根据传递给它的函数类型调用正确的地图函数是非常重要的。
在寻找解决此问题的方法时,我遇到了 TypeTags。这是我想出的代码,我认为它接近正确,但当然不能正常工作:
import scala.reflect.runtime.universe._
object Test {
case class Foo(name: String, value: Int)
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map[A: TypeTag](func: A => A) =
typeOf[A] match {
case i if i =:= typeOf[Int => Int] => f.mapI(func)
case s if s =:= typeOf[String => String] => f.mapS(func)
}
def mapI(func: Int => Int) = Foo(f.name, func(f.value))
def mapS(func: String => String) = Foo(func(f.name), f.value)
}
def main(args: Array[String])
{
def square(n: Int): Int = n * n
def rev(s: String): String = s.reverse
val f = Foo("Test", 3)
println(f.string)
val g = f.map(rev)
val h = g.map(square)
println(h.string)
}
}
当我尝试 运行 此代码时,出现以下错误:
[error] /src/main/scala/Test.scala:10: type mismatch;
[error] found : A => A
[error] required: Int => Int
[error] case i if i =:= typeOf[Int => Int] => f.mapI(func)
[error] ^
[error] /src/main/scala/Test.scala:11: type mismatch;
[error] found : A => A
[error] required: String => String
[error] case s if s =:= typeOf[String => String] => f.mapS(func)
func 确实是 A => A 类型,那么我如何告诉编译器我在 运行 时匹配正确的类型?
非常感谢。
在您定义的 map
中,类型 A
表示函数的参数和结果。那么 func
的类型就是 A => A
。然后你基本上检查一下,例如 typeOf[A] =:= typeOf[Int => Int]
。这意味着 func
将是 (Int => Int) => (Int => Int)
,这是错误的。
使用 TypeTag
s 解决此问题的方法之一如下所示:
def map[T, F : TypeTag](func: F)(implicit ev: F <:< (T => T)) = {
func match {
case func0: (Int => Int) @unchecked if typeOf[F] <:< typeOf[Int => Int] => f.mapI(func0)
case func0: (String => String) @unchecked if typeOf[F] <:< typeOf[String => String] => f.mapS(func0)
}
}
不过你必须用下划线来调用它:f.map(rev _)
。它可能会抛出匹配错误。
也许可以改进此代码,但我建议您做得更好。克服重载方法参数类型擦除的最简单方法是使用 DummyImplicit
。只需向某些方法添加一个或多个隐式 DummyImplicit
参数:
implicit class FooUtils(f: Foo) {
def string() = s"${f.name}: ${f.value}"
def map(func: Int => Int)(implicit dummy: DummyImplicit) = Foo(f.name, func(f.value))
def map(func: String => String) = Foo(func(f.name), f.value)
}
克服方法参数类型擦除的更通用的方法是使用the magnet pattern。这是一个工作示例:
sealed trait MapperMagnet {
def map(foo: Foo): Foo
}
object MapperMagnet {
implicit def forValue(func: Int => Int): MapperMagnet = new MapperMagnet {
override def map(foo: Foo): Foo = Foo(foo.name, func(foo.value))
}
implicit def forName(func: String => String): MapperMagnet = new MapperMagnet {
override def map(foo: Foo): Foo = Foo(func(foo.name), foo.value)
}
}
implicit class FooUtils(f: Foo) {
def string = s"${f.name}: ${f.value}"
// Might be simply `def map(func: MapperMagnet) = func.map(f)`
// but then it would require those pesky underscores `f.map(rev _)`
def map[T](func: T => T)(implicit magnet: (T => T) => MapperMagnet): Foo =
magnet(func).map(f)
}
这是有效的,因为当您调用 map
时,隐式 magnet
在编译时使用完整类型信息解析,因此不会发生擦除,也不需要运行时类型检查。
我认为磁铁版本更干净,作为奖励它不使用任何运行时反射调用,你可以在参数中不带下划线调用 map
:f.map(rev)
,而且它不能抛出运行时匹配错误。
更新:
现在想起来,这里的 magnet 并不比完整的类型类简单,但它可能更能表达意图。不过,与 typeclass 相比,这是一个鲜为人知的模式。无论如何,为了完整性,这里是使用类型类模式的相同示例:
sealed trait FooMapper[F] {
def map(foo: Foo, func: F): Foo
}
object FooMapper {
implicit object ValueMapper extends FooMapper[Int => Int] {
def map(foo: Foo, func: Int => Int) = Foo(foo.name, func(foo.value))
}
implicit object NameMapper extends FooMapper[String => String] {
def map(foo: Foo, func: String => String) = Foo(func(foo.name), foo.value)
}
}
implicit class FooUtils(f: Foo) {
def string = s"${f.name}: ${f.value}"
def map[T](func: T => T)(implicit mapper: FooMapper[T => T]): Foo =
mapper.map(f, func)
}