【问题标题】:Overcoming Scala Type Erasure For Function Argument of Higher-Order Function克服高阶函数的函数参数的 Scala 类型擦除
【发布时间】:2015-10-07 00:49:50
【问题描述】:

基本上,我想做的是为自定义类编写“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)

  }
}

当然,由于类型擦除,这是行不通的。任何一个版本的地图都可以单独工作,它们可以命名不同,一切正常。但是,用户可以简单地根据传递给它的函数类型调用正确的映射函数,这一点非常重要。

在寻找如何解决这个问题的过程中,我发现了 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,那么如何告诉编译器我在运行时匹配了正确的类型?

非常感谢。

【问题讨论】:

    标签: scala functional-programming overloading type-erasure


    【解决方案1】:

    map 的定义中,键入A 表示函数的参数和结果。那么func 的类型就是A => A。然后你基本上检查一下,例如typeOf[A] =:= typeOf[Int => Int]。这意味着func 将是(Int => Int) => (Int => Int),这是错误的。

    使用TypeTags 解决此问题的一种方法如下所示:

    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 在编译时使用完整类型信息解析,因此不会发生擦除,也不需要运行时类型检查。

    我认为磁铁版本更干净,而且它不使用任何运行时反射调用,你可以在参数中不带下划线的情况下调用mapf.map(rev),而且它也不能抛出运行时匹配错误。

    更新:

    现在我想起来了,这里的磁铁并不比完整的类型类简单,但它可能会更好地显示意图。不过,它是一种比 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)
    }
    

    【讨论】:

    • 注意:这里的磁铁实际上与完整的类型类没有什么不同,但可能更好地显示意图?
    • 我发现您的解决方案突出了意图。您能否详细说明为什么def map(func: MapperMagnet) 需要下划线而选择的方式不需要?
    • @Mik378 使用签名def map(func: MapperMagnet),scala 不需要函数作为参数,因此您必须将其显式扩展为带下划线的值。
    • 只有一个问题:有def map(func: Int =&gt; Int) = Foo(f.name, func(f.value)) def map(func: String =&gt; String) = Foo(func(f.name), f.value),为什么调用时还需要下划线:f.map(rev _)。实际上,编译器不能检测到一个函数是预期的输入,然后隐式转换它(从def 到文字值)吗?它是重载上下文特有的吗?
    • @Mik378 你是什么意思?由于类型擦除,您评论中的那 2 个地图无法编译。
    猜你喜欢
    • 1970-01-01
    • 2021-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-02
    相关资源
    最近更新 更多