0

This is hard to phrase, so please, let me show an example:

trait Cache

trait QueryLike {
  type Result
}

trait Query[A] extends QueryLike {
  type Result = A
  def exec: Result
}

trait CachedQuery[A] extends QueryLike {
  type Result = A
  def execWithCache(cache: Cache): Result
}

def exec(query: QueryLike)(implicit cache: Cache): query.Result = query match {
  case q: Query[query.Result] => q.exec
  case cq: CachedQuery[query.Result] => cq.execWithCache(cache)
}

This compiles and runs fine as pattern matching is done on different types (Query, CachedQuery) instead of relying on generics like this question.

But I still get compiler warning like :

Warning:(18, 12) abstract type Result in type pattern A$A4.this.Query[query.Result] is unchecked since it is eliminated by erasure case q: Query[query.Result] => q.exec

Since I don't work on dependent type query.Result directly in anyway (like casting it for different operations), it'd be ideal to just erase it completely and do away with the warning. But unfortunately, using wildcard doesn't work for a good reason:

...
case q: Query[_] => q.exec // type mismatch
case cq: CachedQuery[_] => cq.execWithCache(cache)
...

Is there a better way to do this without generating compiler warning?

Community
  • 1
  • 1
Daniel Shin
  • 5,086
  • 2
  • 30
  • 53

2 Answers2

2

This error isn't really specific to path-dependent types. If you tried to match on any Query[A] you would get the same error, because the type parameter A is erased at runtime. In this case, it's not possible that the type parameter can be anything other than the type you're looking for. Since a Query[A] is a QueryLike { type Result = A}, it should also be a Query[query.Result], though this is a somewhat unusual way to look at it. You could use the @unchecked annotation to suppress the warning, if you wish:

def exec(query: QueryLike)(implicit cache: Cache): query.Result = query match {
  case q: Query[query.Result @unchecked] => q.exec
  case cq: CachedQuery[query.Result @unchecked] => cq.execWithCache(cache)
}

While it's tough to say if this would apply to your actual use-case, you could also restructure your code to avoid matching entirely, and handle it (possibly) more elegantly via polymorphism. Since the last exec requires an implicit Cache anyway, it wouldn't seem to hurt to allow that for each QueryLike. Your API can be more uniform this way, and you wouldn't need to figure out which method to call.

trait Cache

trait QueryLike {
  type Result
  def exec(implicit cache: Cache): Result
}

trait Query[A] extends QueryLike {
  type Result = A
}

trait CachedQuery[A] extends QueryLike {
  type Result = A
}

def exec(query: QueryLike)(implicit cache: Cache): query.Result = query.exec

If Query[A] requires an exec without a Cache, you could also provide an overload with a DummyImplicit to allow it to work without one.

def exec(implicit d: DummyImplicit): Result
Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
  • Thanks for the answer. Although I thought of passing `cache` to all `QueryLike`, I decided not to as it seemed to negate the code clarity and `CachedQuery` is somewhat an exceptional case anyway so it was quite discouraging to do so. My acutal class with `exec` has `cache` object in scope. `@unchecked` annotation seems interesting. Thanks for the pointer – Daniel Shin Feb 08 '16 at 05:19
  • You might also find another way to resolve the `Cache` for the `CachedQuery` other than the implicit, which would allow you to remove it from the signature and make everything uniform. _How_ is difficult to say, because I don't know the specific use-case. But right now if you have a `query: Query[A]`, and pass it to `exec` (with the match), it still requires the implicit `Cache`, which is the same problem. – Michael Zajac Feb 08 '16 at 05:25
  • Oh no. What I meant by scope wasn't the implicit. My class looks somewhat like this: `class DB { val cache: Cache = ???; def exec(query: QueryLike) = ??? }`. So the caller of `exec` is completely oblivious to whether the query is executed with cache or not. The code example above wasn't very clear as to that. I added implicit just for the sake of making it compile. – Daniel Shin Feb 08 '16 at 05:31
0

Actually, the problem seems to be very specific to path depended types: the problem is the type of q and cq. q.Result is a incompatible type to query.Result because the Scala type checker does not know, that query and q and qc have to be the same reference.

Thus, Query[query.Result] actually does require a runtime type check. I've noticed this, when I tried to remove the type parameter and just use the inner result. Then the pattern matches do not generate a warning any more but the return type of q.exec won't be compatible with query.Result.

One solution, without using @unchecked or asInstanceOf etc. would be to turn Result into a type parameter of QueryLike. In general, you should not use abstract type members for things that you want to take "out of a context" an pass around freely. So turning a type member into a type parameter somewhere in the inheritance hierarchy is kind of strange.

So this works fine, as the compiler knows, that he does not have to check the type parameter:

trait QueryLike[A] {

}

trait Query[A] extends QueryLike[A] {
  def exec: A
}

trait CachedQuery[A] extends QueryLike[A] {
  def execWithCache(cache: Cache): A
}

def exec[A](query: QueryLike[A])(implicit cache: Cache): A = query match {
  case q: Query[A] => q.exec
  case cq: CachedQuery[A] => cq.execWithCache(cache)
}

Another approach would be to add the method to a common base trait of CachedQuery and Query.

trait QueryLike {
  type Result
}

trait Query[A] extends QueryLike with ExecWithCache {
  type Result = A
  def exec: Result
  override def execWithCache(implicit cache: Cache) = exec
}

trait CachedQuery[A] extends QueryLike with ExecWithCache {
  type Result = A
  def exec(cache: Cache): Result
  override def execWithCache(implicit cache: Cache) = exec(cache)
}

trait ExecWithCache extends QueryLike {
  def execWithCache(implicit cache: Cache): Result
}

To change this, Scala would probably have to be able to determine when two stable accessors are identical.

dth
  • 2,287
  • 10
  • 17
  • Thanks for the answer. Could you elaborate on switching to type parameter in place of abstract type? As far as I can tell, it still requires a type parameter on `Query` and `CachedQuery` like: `def exec[A](query: QueryLike[A]): A = q match { case q: Query[A] => ???; case cq: CachedQuery[A] = ??? }`. – Daniel Shin Feb 09 '16 at 04:06
  • I've included the code in my answer. This is more or less the standard way to do this, I assumed you had some reason for not doing it that way. – dth Feb 09 '16 at 12:32