paul paul - 1 month ago 13
Scala Question

Function with Generic type that extend class

I´m trying to use a function which use a generic type which is passed as part of the definition of a map.

Here my code

object EventMapping {


val eventMapping = collection.mutable.Map[Class[_ <: EventBase], (User, EventBase) => Unit]()

setMapping(classOf[UserCreated], (user, evt) => user.loadUserName(evt.userName))


private def setMapping[T <: EventBase](clazz: Class[T],fn: (User, T) => Unit) {
eventMapping += clazz -> fn.asInstanceOf[(User, EventBase) => Unit]
}

}


UserCreated class is a en event class that extend from EventBase, so since in setMapping I define T as extended from EventBase, and in setMapping invocation I´m defining which class type I´m using, in the function

user.loadUserName(evt.userName))


I was expecting that evt it would be consider as the CreatedUser event, but still the compiler consider as EventBase.

Similar code base on Java works, but I don't know what I´m missing here.

Here the UserCreated class

class UserCreated @JsonCreator() (@JsonProperty("userName")val userName: String) extends EventBase{


@JsonProperty("userName") def getUserName: String = {
userName
}
}


This is the stack trace

[info] Compiling 5 Scala sources to /Development/proyectX/target/scala-2.11/classes...
[error] /Development/proyectX/app/persistance/EventMapping.scala:11: missing parameter type
[error] setMapping(classOf[UserCreated], (user, evt) => user.loadUserName(evt.asInstanceOf[UserCreated].userName))
[error] ^
[error] one error found
[error] (compile:compile) Compilation failed

Answer

The issue is that in the definition of setMapping, you are telling the compiler only that T is a subtype of EventBase. So when you call setMapping and inside the call you have evt.userName, the compiler can't guarantee that all subtypes of EventBase support the userName member, so that's a compile error. So when you do evt.asInstanceOf[UserCreated].userName, you personally are guaranteeing to the compiler that evt is really a UserCreated and it does support the userName member.

Secondly, the missing parameter type compile error (not a stack trace btw, stack traces are only from runtime exceptions) is a result of Scala's type inference algorithm not being perfect. Because of a quirk in the algorithm, you can make it more accurate by moving the mapping function (fn) into its own parameter list:

object EventMapping {
  val eventMapping =
    collection.mutable.Map[Class[_ <: EventBase], (User, EventBase) => Unit]()

  setMapping(classOf[UserCreated]) { (user, evt) =>
    user.loadUserName(evt.userName)
  }

  private def setMapping[T <: EventBase](
    clazz: Class[T])(fn: (User, T) => Unit): Unit =
    eventMapping += clazz -> fn.asInstanceOf[(User, EventBase) => Unit]
}

This will also remove the need to downcast evt.asInstanceOf[UserCreated] because now the compiler can infer it properly.

Lastly, sometimes the type inferencer just can't line everything up properly and still gives you a compile error. In that case you may just pass in a type argument explicitly:

setMapping[UserCreated](classOf[UserCreated]) { (user, evt) =>
  user.loadUserName(evt.userName)
}

This tells the compiler that everywhere you have the generic type T, in this call replace that with UserCreated.

P.S. the fact that you have to downcast is usually a sign that you could be using a more idiomatic and composeable Scala feature--typeclasses.

Comments