scala generics any underscores

Scala: cualquier guión bajo vs en genéricos



generics any (2)

El primero es una abreviatura de un tipo existencial cuando el código no necesita saber qué tipo es o restringirlo:

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

Este formulario dice que el tipo de elemento de List es desconocido para la class Foo lugar de su segunda forma, que dice específicamente que el tipo de elemento de la List es Any .

Echa un vistazo a este breve artículo explicativo del blog sobre tipos existenciales en Scala.

¿Cuál es la diferencia entre las siguientes definiciones de Generics en Scala:

class Foo[T <: List[_]]

y

class Bar[T <: List[Any]]

Mi instinto me dice que son más o menos lo mismo, pero que el último es más explícito. Estoy encontrando casos donde el primero compila, pero el segundo no, pero no puedo señalar la diferencia exacta.

¡Gracias!

Editar:

¿Puedo lanzar otro en la mezcla?

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


OK, pensé que debería tener mi opinión, en lugar de simplemente publicar comentarios. Lo siento, esto va a ser largo, si quieres que el TLDR salte hasta el final.

Como dijo Randall Schulz, aquí _ hay una abreviatura de tipo existencial. A saber,

class Foo[T <: List[_]]

es una forma abreviada de

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

Tenga en cuenta que, al contrario de lo que menciona la respuesta de Randall Shulz (divulgación completa: también me equivoqué en una versión anterior de esta publicación, gracias a Jesper Nordenberg por señalarlo), esto no es lo mismo que:

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

ni es lo mismo que:

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

Tenga cuidado, es fácil equivocarse (como lo muestra mi goof anterior): el autor del artículo al que hace referencia la respuesta de Randall Shulz lo malinterpretó (ver comentarios) y lo arregló más tarde. Mi principal problema con este artículo es que, en el ejemplo que se muestra, se supone que el uso de existenciales nos salvará de un problema de tipeo, pero no es así. Ve a ver el código e intenta compilar compileAndRun(helloWorldVM("Test")) o compileAndRun(intVM(42)) . Sí, no compila. Simplemente hacer compileAndRun y compileAndRun genérico en A haría que el código compilara, y sería mucho más simple. En resumen, probablemente este no sea el mejor artículo para aprender sobre los existenciales y para qué sirven (el propio autor reconoce en un comentario que el artículo "necesita poner orden").

Así que preferiría recomendar leer este artículo: http://www.artima.com/scalazine/articles/scalas_type_system.html , en particular las secciones llamadas "Tipos existenciales" y "Varianza en Java y Scala".

El punto importante que debe obtener de este artículo es que los existenciales son útiles (además de ser capaz de tratar con clases Java genéricas) cuando se trata de tipos no covariantes. Aquí hay un ejemplo.

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

Esta clase es genérica (nótese también que es invariante), pero podemos ver que hello realmente no hace uso del parámetro tipo (a diferencia de getName ), así que si obtengo una instancia de Greets siempre debería ser capaz de llamarlo , lo que sea que T sea. Si quiero definir un método que tome una instancia de Greets y simplemente llame a su método hello , podría intentar esto:

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

Efectivamente, esto no se compila, ya que T sale de la nada aquí.

OK, hagamos el método genérico:

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

Genial, esto funciona. También podríamos usar existenciales aquí:

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

Funciona también Entonces, en general, no hay un beneficio real aquí al usar un parámetro de tipo sobre existencial (como en sayHi3 ) (como en sayHi2 ).

Sin embargo, esto cambia si Greets aparece a sí mismo como un parámetro de tipo a otra clase genérica. Digamos por ejemplo que queremos almacenar varias instancias de Greets (con diferentes T ) en una lista. Vamos a intentarlo:

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

La última línea no se compila porque Greets es invariable, por lo tanto, Greets[String] y Greets[Symbol] no pueden tratarse como Greets[Any] , aunque String y Symbol extienden Any .

OK, probemos con un existencial, usando la notación abreviada _ :

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

Esto compila bien, y puede hacerlo, como se esperaba:

greetsSet foreach (_.hello)

Ahora, recuerde que la razón por la que tuvimos un problema de verificación de tipo en primer lugar fue porque Greets es invariable. Si se convirtiera en una clase covariante ( class Greets[+T] ), entonces todo habría salido bien y no habríamos necesitado existenciales.

En resumen, los existenciales son útiles para tratar con clases genéricas invariables, pero si la clase genérica no necesita aparecer como un parámetro de tipo para otra clase genérica, es probable que no necesite existenciales y simplemente agregue un parámetro de tipo a tu método funcionará

Ahora regrese (¡por fin lo sé!) A su pregunta específica, con respecto a

class Foo[T <: List[_]]

Debido a que List es covariante, esto es para todos los intentos y propósitos lo mismo que decir:

class Foo[T <: List[Any]]

Entonces, en este caso, utilizar cualquiera de las anotaciones es realmente una cuestión de estilo.

Sin embargo, si reemplaza List por Set , las cosas cambian:

class Foo[T <: Set[_]]

Set es invariable y, por lo tanto, estamos en la misma situación que con la clase Greets de mi ejemplo. Por lo tanto, lo anterior realmente es muy diferente de

class Foo[T <: Set[Any]]