scala - Any vs underscore in generics

2019-01-10 01:35发布

问题:

What is the different between the following Generics definitions in Scala:

class Foo[T <: List[_]]

and

class Bar[T <: List[Any]]

My gut tells me they are about the same but that the latter is more explicit. I am finding cases where the former compiles but the latter doesn't, but can't put my finger on the exact difference.

Thanks!

Edit:

Can I throw another into the mix?

class Baz[T <: List[_ <: Any]]

回答1:

OK, I figured I should have my take on it, instead of just posting comments. Sorry, this is going to be long, if you want the TLDR skip to the end.

As Randall Schulz said, here _ is a shorthand for an existential type. Namely,

class Foo[T <: List[_]]

is a shorthand for

class Foo[T <: List[Z] forSome { type Z }]

Note that contrary to what Randall Shulz's answer mentions (full disclosure: I got it wrong too in an earlier version fo this post, thanks to Jesper Nordenberg for pointing it out) this not the same as:

class Foo[T <: List[Z]] forSome { type Z }

nor is it the same as:

class Foo[T <: List[Z forSome { type Z }]

Beware, it is easy to get it wrong (as my earlier goof shows): the author of the article referenced by Randall Shulz's answer got it wrong himself (see comments), and fixed it later. My main problem with this article is that in the example shown, the use of existentials is supposed to save us from a typing problem, but it does not. Go check the code, and try to compile compileAndRun(helloWorldVM("Test")) or compileAndRun(intVM(42)). Yep, does not compile. Simply making compileAndRun generic in A would make the code compile, and it would be much simpler. In short, that's probably not the best article to learn about existentials and what they are good for (the author himself acknowledge in a comment that the article "needs tidying up").

So I would rather recommend reading this article: http://www.artima.com/scalazine/articles/scalas_type_system.html, in particular the sections named "Existential types" and "Variance in Java and Scala".

The important point that you you should get from this article is that existentials are useful (apart from being able to deal with generic java classes) when dealing with non covariant types. Here is an example.

case class Greets[T]( private val name: T ) {
  def hello() { println("Hello " + name) }
  def getName: T = name
}

This class is generic (note also that is is invariant), but we can see that hello really don't make use of the type parameter (unlike getName), so if I get an instance of Greets I should always be able to call it, whatever T is. If I want to define a method that takes a Greets instance and just calls its hello method, I could try this:

def sayHi1( g: Greets[T] ) { g.hello() } // Does not compile

Sure enough, this does not compile, as T comes out of nowhere here.

OK then, let's make the method generic:

def sayHi2[T]( g: Greets[T] ) { g.hello() }
sayHi2( Greets("John"))
sayHi2( Greets('Jack))

Great, this works. We could also use existentials here:

def sayHi3( g: Greets[_] ) { g.hello() }
sayHi3( Greets("John"))
sayHi3( Greets('Jack))

Works too. So all in all, there is no real benefit here from using an existential (as in sayHi3) over type parameter (as in sayHi2).

However, this changes if Greets appears itself as a type parameter to another generic class. Say by example that we want to store several instances of Greets (with different T) in a list. Let's try it:

val greets1: Greets[String] = Greets("John")
val greets2: Greets[Symbol] = Greets('Jack)
val greetsList1: List[Greets[Any]] = List( greets1, greets2 ) // Does not compile

The last line does not compile because Greets is invariant, so a Greets[String] and Greets[Symbol] cannot be treated as a Greets[Any] even though String and Symbol both extends Any.

OK, let's try with an existential, using the shorthand notation _:

val greetsList2: List[Greets[_]] = List( greets1, greets2 ) // Compiles fine, yeah

This compiles fine, and you can do, as expected:

greetsSet foreach (_.hello)

Now, remember that the reason we had a type checking problem in the first place was because Greets is invariant. If it was turned into a covariant class (class Greets[+T]) then everything would have worked out of the box and we would never have needed existentials.


So to sum up, existentials are usefull to deal with generic invariant classes, but if the generic class does not need to appear itself as a type parameter to another generic class, chances are that you don't need existentials and simply adding a type parameter to your method will work

Now come back(at last, I know!) to your specific question, regarding

class Foo[T <: List[_]]

Because List is covariant, this is for all intents and purpose the same as just saying:

class Foo[T <: List[Any]]

So in this case, using either notation is really just a matter of style.

However, if you replace List with Set, things change:

class Foo[T <: Set[_]]

Set is invariant and thus we are in the same situation as with the Greets class from my example. Thus the above really is very different from

class Foo[T <: Set[Any]]


回答2:

The former is a shorthand for an existential type when the code doesn't need to know what the type is or constrain it:

class Foo[T <: List[Z forSome { type Z }]

This form says that the element type of List is unknown to class Foo rather than your second form, which says specifically that the List's element type is Any.

Check out this brief explanatory blog article on Existential Types in Scala.