I'm attempting to write a simple query monad and am having trouble getting my generic type annotations correct.
My first attempt went as follows (vastly simplified for conciseness)
case class Person( val name: String )
abstract class Schema[T]
object People extends Schema[Person]
case class Query[U <: Schema[T], T]( schema: U ) { <---- Type signature
def results: Seq[T] = ...
def where( f: U => Operation ) = ...
}
class TypeText extends Application {
val query = Query( People ) <---- Type inference fails
}
The compiler didn't like this, as it couldn't infer the type of 'T'.
error: inferred type arguments [People.type,Nothing] do not conform to method apply's type parameter bounds [U <: Schema[T],T]
While experimenting I found that using view bounds instead works as expected
case class Query[U <% Schema[T], T]( schema: U ) {
(Note the use of view bound "<%" instead of type bound "<:")
However in my limited understanding of the type system, since I'm expecting an actual subclass (and not just convertibility) of Schema[T], I would assume type bound "<:" is the correct bounds to be using here?
If this is the case, what am I missing - how do I give the compiler enough hints to infer T correctly when using type bounds instead of view bounds?
This is not a fully statisfying answer (at least to me) as I have to admit that I cannot put words on exactly where and why the inference fails here. I only have some fuzzy intuitions about it. The problem is related to the compiler having to infer two type parameters at a time. As to why changing the type bound to a view bound fixes the compilation, my understanding is that now there are two parameter lists, and that as a result we now have two successive phases of type inferences instead of two inferences at a time. Indeed, the following:
case class Query[U <% Schema[T], T]( schema: U )
is the same as:
case class Query[U, T]( schema: U )( implicit conv: U => Schema[T] )
The first parameter list drives the inference of U
, and then the second one (note that U
is now know) will drive the inference of T
.
In the case of the expression Query( People )
, the parameter People
will drive the type inferencer to set U
to People.type
. Then, the compiler will look for an implicit conversion from People.type
to Schema[T]
, to pass in the second parameter list. The only one in scope is the (trivial) conversion from People.type
to Schema[Person]
, driving the inferencer to deduce that T = Person
.
To fix the compilation without resorting to a view bound, you can replace the type parameter T
with an abstract type:
case class Person( val name: String )
sealed trait Schema {
type T
}
abstract class SchemaImpl[_T] extends Schema {
type T = _T
}
object People extends SchemaImpl[Person]
case class Query[U <: Schema]( schema: U ) {
def results: Seq[schema.T] = ???
}
class TypeText extends Application {
val query = Query( People )
}
UPDATE:
@Aaron Novstrup's:
To the extent of my knowledge, your answer is incorrect (update to the update: the orignal answer from Aaron claimed that the Query
declaration was equivalenbt to case class Query[U <: Schema[X], T](schema: U)
).
case class Query[U <: Schema[X], T](schema: U)
does not even compile. Let's say that you meant
case class Query[U <: Schema[_], T](schema: U)
(which does compile), it's easy to check in the REPL that it is not the same either.
Indeed, the following compiles fine:
case class Query[U <: Schema[_], T](schema: U)
type MyQuery = Query[Schema[String], Int]
While, the following does not:
case class Query[U <: Schema[T], T](schema: U)
type MyQuery = Query[Schema[String], Int]
Hence proving the difference. The error is:
<console>:10: error: type arguments [Schema[String],Int] do not conform to class Query's type parameter bounds [U <: Schema[T],T]
type MyQuery = Query[Schema[String], Int]
Which clearly shows that the first and second occurences of T
denote the same type, and we do have a relationship between the two type parameters.
In order to encode the relationship between the two type parameters, you can use something like
case class Query[U, T](schema: U)(implicit ev: U <:< Schema[T]) { ... }
See §4.3 and §4.4 of the Scala Language Spec for more info.
I had the same problem. The following worked for me:
case class Query[U <: Schema[T], T]( schema: U with Schema[T] ) {
...
}
I've always found that when using two type identifiers on a class/function, the type inference system does not work as expected and you have to be explicit like so:
val query = Query[People.type, Person]( People )
If you changed your Query
declaration to this:
case class Query[U <: Schema[_]( schema: U )
You'd be able to do this:
val query = Query( People )
But then you would not know the underlying type of the Schema
supplied and would not be able to properly implement the results
function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With