Scala inferred type arguments - Type bounds inferring to 'Nothing'
Asked Answered
C

4

8

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?

Cowardice answered 30/4, 2013 at 2:33 Comment(0)
R
3

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.

Rior answered 30/4, 2013 at 19:9 Comment(3)
Thanks. I'd seen that syntax around and was wondering what it meant.Cowardice
I don't think this answer is correct. Please check my reply at the bottom of my answer (this comment would not have provided enough space for the discussion).Rowdyish
@Régis You're right. I misinterpreted the scoping rules in the spec. It is indeed an inference issue, as you described in your answer. That said, using an implicit evidence parameter is the easiest way to encode the relationship and support the desired inference.Rior
O
5

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.

Osculate answered 30/4, 2013 at 13:36 Comment(0)
R
3

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.

Rior answered 30/4, 2013 at 19:9 Comment(3)
Thanks. I'd seen that syntax around and was wondering what it meant.Cowardice
I don't think this answer is correct. Please check my reply at the bottom of my answer (this comment would not have provided enough space for the discussion).Rowdyish
@Régis You're right. I misinterpreted the scoping rules in the spec. It is indeed an inference issue, as you described in your answer. That said, using an implicit evidence parameter is the easiest way to encode the relationship and support the desired inference.Rior
W
2

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

case class Query[U <: Schema[T], T]( schema: U with Schema[T] ) {
    ...
}
Wilding answered 20/2, 2015 at 7:19 Comment(0)
U
1

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.

Urina answered 30/4, 2013 at 13:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.