Below is a specific instance of a kind of situation that I sometimes encounter with parameterized types. Basically, there are type parameters that I know are compatible, but I don't know how to prove that to some parts of the code.
I'm writing a request router that maps urls to handler functions. Below is some simplified code. I create a List[Route]
where a Route
is basically a UrlMatcher, Function
pair.
class Route[A](matcher: UrlMatcher[A], handler: HandlerFunction[A])
abstract class UrlMatcher[A] {
def match(url: String): Option[A] // None if no match
The type parameter A
is for the "arguments" that the matcher might extract from the URL. They would be passed to the handler function. For example, a UrlMatcher[Int]
that sees a URL path like "/users/123" could pass 123 to a getUser(id: Int)
function. The router might look like:
val routes = ArrayBuffer[Route[_]]
def callHandler(url: String) {
for (r <- routes) {
val args = r.matcher.matchUrl(url)
if (args.isDefined)
r.handler(args.get) // <--- error here
}
The problem is that I get type mismatch errors because I don't know how to tell it the two types are the same.
type mismatch; found: args.type (with underlying type Option[Any])
required: _$1
I know I can redesign it so that Route
has a method like matchAndCall
, but I'd like to keep this logical flow if possible.
Update/Edit
I don't fully understand existential types, but I tried this...
val routes = ArrayBuffer[T forSome { type T }]()
And it removed the mismatch error above. However, I have another one where I was inserting into the ArrayBuffer
.
def route[P](matcher: UrlMatcher[P], handler: Handler[P]): AbstractRoute = {
val route = new Route(matcher, handler)
otherRoutes.append(route) // error here
route
}
Now the error is...
type mismatch; found : Route[P] required: Route[T forSome { type T }] Note: P <: T
forSome { type T }, but class Route is invariant in type P. You may wish to define
P as +P instead. (SLS 4.5)
Why is P
incompatible with T
, since their are no constraints on T
?
This is one of the reasons existential types (the Scala equivalent of wildcard types) are a Bad Thing (TM) that is best avoided when not doing Java interop: the compiler cannot (or just isn't smart enough to) reason normally about which types are equal to which, because they are all gone...
To make the compiler understand that those types are the same, you need to give that type a name somehow.
A type parameter is a possibility: you define a parametrized method with the content of your for comprehension, so that within the method, the type is well-known.
Note: in simpler cases, you could also use variance to have a
List[Route[Any]]
, and your problem goes away, the type is well-known again. Here I am not sure you could makeRoute[A]
covariant.Existential types mostly exist to represent Java wildcards, Java raw types and the JVM's view of types (reflection and stuff), even though they are more powerful than those three constructs.
If you can design things to avoid using them, you will save yourself a lot of pain.This was a bit controversial, but a least, there are a lot of limitations with the way they interact with type inference, so one has to be careful.