Search code examples
scalapattern-matchingtype-erasurepath-dependent-type

How to pattern match with dependent type without using it?


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?


Solution

  • 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