Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
529 views
in Technique[技术] by (71.8m points)

scala - Any vs underscore in generics

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]]
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 TL;DR 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 of 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 hould 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 doesn't make any 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 useful 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]]

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...