Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala inferred type arguments - Type bounds inferring to 'Nothing'

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?

like image 346
James Davies Avatar asked Apr 30 '13 02:04

James Davies


4 Answers

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.

like image 165
Régis Jean-Gilles Avatar answered Nov 08 '22 07:11

Régis Jean-Gilles


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.

like image 39
Aaron Novstrup Avatar answered Nov 08 '22 06:11

Aaron Novstrup


I had the same problem. The following worked for me:

case class Query[U <: Schema[T], T]( schema: U with Schema[T] ) {
    ...
}
like image 2
Andreas Flueckiger Avatar answered Nov 08 '22 06:11

Andreas Flueckiger


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.

like image 1
cmbaxter Avatar answered Nov 08 '22 06:11

cmbaxter