Jack Koenig
Jack Koenig

Reputation: 6064

Overcoming Scala Type Erasure For Function Argument of Higher-Order Function

Essentially, what I would like to do is write overloaded versions of "map" for a custom class such that each version of map differs only by the type of function passed to it.

This is what I would like to do:

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)

  }
}

Of course, because of type erasure, this won't work. Either version of map will work alone, and they can be named differently and everything works fine. However, it is very important that a user can call the correct map function simply based on the type of the function passed to it.

In my search for how to solve this problem, I cam across TypeTags. Here is the code I came up with that I believe is close to correct, but of course doesn't quite work:

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)

  }
}

When I attempt to run this code I get the following errors:

[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)

It is true that func is of type A => A, so how can I tell the compiler that I'm matching on the correct type at runtime?

Thank you very much.

Upvotes: 2

Views: 214

Answers (1)

Kolmar
Kolmar

Reputation: 14224

In your definition of map, type A means the argument and result of the function. The type of func is then A => A. Then you basically check that, for example typeOf[A] =:= typeOf[Int => Int]. That means func would be (Int => Int) => (Int => Int), which is wrong.

One of ways of fixing this using TypeTags looks like this:

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)
  }
}

You'd have to call it with an underscore though: f.map(rev _). And it may throw match errors.

It may be possible to improve this code, but I'd advise to do something better. The simplest way to overcome type erasure on overloaded method arguments is to use DummyImplicit. Just add one or several implicit DummyImplicit arguments to some of the methods:

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)
}

A more general way to overcome type erasure on method arguments is to use the magnet pattern. Here is a working example of it:

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)
}

This works because when you call map, the implicit magnet is resolved at compile time using full type information, so no erasure happens and no runtime type checks are needed.

I think the magnet version is cleaner, and as a bonus it doesn't use any runtime reflective calls, you can call map without underscore in the argument: f.map(rev), and also it can't throw runtime match errors.

Update:

Now that I think of it, here magnet isn't really simpler than a full typeclass, but it may show the intention a bit better. It's a less known pattern than typeclass though. Anyway, here is the same example using the typeclass pattern for completeness:

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)
}

Upvotes: 6

Related Questions