stringtranslate.com

Covarianza y contravarianza (informática)

Muchos sistemas de tipos de lenguajes de programación admiten subtipos . Por ejemplo, si el tipo es un subtipo de , entonces una expresión de tipo debe ser sustituible siempre que se utilice una expresión de tipo . CatAnimalCat Animal

La variación es cómo se relaciona la subtipificación entre tipos más complejos con la subtipificación entre sus componentes. Por ejemplo, ¿cómo debería relacionarse una lista de Cats con una lista de Animals? ¿O cómo debería relacionarse una función que regresa Cat con una función que regresa Animal?

Dependiendo de la variación del constructor de tipos , la relación de subtipos de los tipos simples puede conservarse, invertirse o ignorarse para los respectivos tipos complejos. En el lenguaje de programación OCaml , por ejemplo, "lista de gatos" es un subtipo de "lista de animales" porque el constructor del tipo de lista es covariante . Esto significa que la relación de subtipificación de los tipos simples se conserva para los tipos complejos.

Por otro lado, "función de Animal a Cadena" es un subtipo de "función de Gato a Cadena" porque el constructor del tipo de función es contravariante en el tipo de parámetro . Aquí, la relación de subtipificación de los tipos simples se invierte para los tipos complejos.

Un diseñador de lenguajes de programación considerará la variación al diseñar reglas de escritura para características del lenguaje como matrices , herencia y tipos de datos genéricos . Al hacer que los constructores de tipos sean covariantes o contravariantes en lugar de invariantes , se aceptarán más programas como bien tipificados. Por otro lado, los programadores a menudo consideran que la contravarianza no es intuitiva y realizar un seguimiento preciso de la variación para evitar errores de tipo en tiempo de ejecución puede generar reglas de escritura complejas.

Para mantener el sistema de tipos simple y permitir programas útiles, un lenguaje puede tratar un constructor de tipos como invariante incluso si sería seguro considerarlo variante, o tratarlo como covariante incluso si eso podría violar la seguridad de tipos.

Definicion formal

Supongamos Aque y Bson tipos y I<U>denota la aplicación de un constructor de tipos I con argumento de tipo U. Dentro del sistema de tipos de un lenguaje de programación, una regla de escritura para un constructor de tipos Ies:

El artículo considera cómo se aplica esto a algunos constructores de tipos comunes.

Ejemplos de C#

Por ejemplo, en C# , si Cates un subtipo de Animal, entonces:

La variación de una interfaz genérica de C# se declara colocando el atributo out(covariante) o in(contravariante) en (cero o más) sus parámetros de tipo. [2] : 144  Las interfaces anteriores se declaran como , y . Los tipos con más de un parámetro de tipo pueden especificar diferentes variaciones en cada parámetro de tipo. Por ejemplo, el tipo de delegado representa una función con un parámetro de entrada contravariante de tipo y un valor de retorno covariante de tipo . [3] [2] : 145  El compilador verifica que todos los tipos estén definidos y utilizados de manera consistente con sus anotaciones y, de lo contrario, señala un error de compilación.IEnumerable<out T>Action<in T>IList<T>Func<in T, out TResult>TTResult

Las reglas de escritura para la variación de la interfaz garantizan la seguridad de los tipos. Por ejemplo, an representa una función de primera clase que espera un argumento de tipo , [2] : 144  y siempre se puede usar una función que pueda manejar cualquier tipo de animal en lugar de una que solo pueda manejar gatos.Action<T>T

matrices

Los tipos de datos (fuentes) de solo lectura pueden ser covariantes; Los tipos de datos de sólo escritura (sumideros) pueden ser contravariantes. Los tipos de datos mutables que actúan como fuentes y sumideros deben ser invariantes. Para ilustrar este fenómeno general, considere el tipo de matriz . Para el tipo Animalpodemos hacer el tipo , que es una "matriz de animales". A los efectos de este ejemplo, esta matriz admite elementos de lectura y escritura.Animal[]

Tenemos la opción de tratar esto como:

Si queremos evitar errores tipográficos, sólo la tercera opción es segura. Claramente, no todos pueden tratarse como si fueran a , ya que un cliente que lea de la matriz esperará a , pero an puede contener, por ejemplo, a . Por tanto, la regla contravariante no es segura.Animal[]Cat[]CatAnimal[]Dog

Por el contrario, a no puede tratarse como . Siempre debería ser posible poner a en un . Con matrices covariantes no se puede garantizar que esto sea seguro, ya que la tienda de respaldo podría ser en realidad una matriz de gatos. Por lo tanto, la regla covariante tampoco es segura: el constructor de la matriz debe ser invariante . Tenga en cuenta que esto es sólo un problema para las matrices mutables; la regla covariante es segura para matrices inmutables (de solo lectura). Asimismo, la regla contravariante sería segura para matrices de sólo escritura.Cat[]Animal[]DogAnimal[]

Matrices covariantes en Java y C#

Las primeras versiones de Java y C# no incluían genéricos, también denominado polimorfismo paramétrico . En tal entorno, hacer que las matrices sean invariantes descarta programas polimórficos útiles.

Por ejemplo, considere escribir una función para mezclar una matriz, o una función que pruebe la igualdad de dos matrices usando Object. equalsmétodo sobre los elementos. La implementación no depende del tipo exacto de elemento almacenado en la matriz, por lo que debería ser posible escribir una única función que funcione en todos los tipos de matrices. Es fácil implementar funciones de tipo:

booleanos igualesArrays ( Objeto [] a1 , Objeto [] a2 ); void shuffleArray ( Objeto [] a );      

Sin embargo, si los tipos de matriz se trataran como invariantes, solo sería posible llamar a estas funciones en una matriz de exactamente el tipo . Por ejemplo, no se podría mezclar una serie de cadenas.Object[]

Por lo tanto, tanto Java como C# tratan los tipos de matrices de forma covariante. Por ejemplo, en Java es un subtipo de y en C# es un subtipo de .String[]Object[]string[]object[]

Como se analizó anteriormente, las matrices covariantes provocan problemas con las escrituras en la matriz. Java [4] : 126  y C# se ocupan de esto marcando cada objeto de matriz con un tipo cuando se crea. Cada vez que se almacena un valor en una matriz, el entorno de ejecución verificará que el tipo de tiempo de ejecución del valor sea igual al tipo de tiempo de ejecución de la matriz. Si hay una discrepancia, se genera un ArrayStoreException(Java) [4] : 126  o (C#):ArrayTypeMismatchException

// a es una matriz de un solo elemento de String String [] a = new String [ 1 ] ;    // b es una matriz de Objeto Objeto [ ] b = a ;   // Asigna un número entero a b. Esto sería posible si b realmente fuera // una matriz de Objeto, pero como realmente es una matriz de Cadena, // obtendremos una java.lang.ArrayStoreException. segundo [ 0 ] = 1 ;  

En el ejemplo anterior, se puede leer de la matriz (b) de forma segura. Solo intentar escribir en la matriz puede generar problemas.

Una desventaja de este enfoque es que deja la posibilidad de que se produzca un error en tiempo de ejecución que un sistema de tipos más estricto podría haber detectado en tiempo de compilación. Además, perjudica el rendimiento porque cada escritura en una matriz requiere una verificación adicional del tiempo de ejecución.

Con la adición de genéricos, Java [4] : ​​126–129  y C# ahora ofrecen formas de escribir este tipo de función polimórfica sin depender de la covarianza. A las funciones de comparación y barajado de matrices se les pueden dar los tipos parametrizados

< T > booleano igualArrays ( T [] a1 , T [] a2 ); <T> void shuffleArray ( T [ ] a ) ;        

Alternativamente, para exigir que un método C# acceda a una colección en forma de solo lectura, se puede usar la interfaz en lugar de pasarle una matriz .IEnumerable<object>object[]

Tipos de funciones

Los lenguajes con funciones de primera clase tienen tipos de funciones como "una función que espera un gato y devuelve un animal" (escrita en sintaxis OCaml o en sintaxis C# ).cat -> animalFunc<Cat,Animal>

Esos lenguajes también necesitan especificar cuándo un tipo de función es un subtipo de otro, es decir, cuándo es seguro usar una función de un tipo en un contexto que espera una función de un tipo diferente. Es seguro sustituir una función f por una función g si f acepta un tipo de argumento más general y devuelve un tipo más específico que g . Por ejemplo, las funciones de tipo , y se pueden utilizar donde se esperaba. (Se puede comparar esto con el principio de solidez de la comunicación: "sé liberal en lo que aceptas y conservador en lo que produce"). La regla general es:animal -> catcat -> catanimal -> animalcat -> animal

si y .

Usando la notación de reglas de inferencia, la misma regla se puede escribir como:

En otras palabras, el constructor de tipo → es contravariante en el tipo de parámetro (entrada) y covariante en el tipo de retorno (salida) . Esta regla fue establecida formalmente por primera vez por John C. Reynolds , [5] y luego popularizada en un artículo de Luca Cardelli . [6]

Cuando se trata de funciones que toman funciones como argumentos , esta regla se puede aplicar varias veces. Por ejemplo, aplicando la regla dos veces, vemos que si . En otras palabras, el tipo es covariante en la posición de . Para tipos complicados puede resultar confuso rastrear mentalmente por qué una determinada especialización de tipo es o no segura para tipos, pero es fácil calcular qué posiciones son covariantes y contravariantes: una posición es covariante si está en el lado izquierdo de un número par de flechas aplicándose a él.

Herencia en lenguajes orientados a objetos

Cuando una subclase anula un método en una superclase, el compilador debe comprobar que el método anulado tenga el tipo correcto. Si bien algunos lenguajes requieren que el tipo coincida exactamente con el tipo en la superclase (invariancia), también es seguro permitir que el método principal tenga un tipo "mejor". Según la regla de subtipo habitual para tipos de funciones, esto significa que el método principal debe devolver un tipo más específico (covarianza del tipo de retorno) y aceptar un argumento más general (contravarianza del tipo de parámetro). En notación UML , las posibilidades son las siguientes (donde la Clase B es la subclase que extiende la Clase A, que es la superclase):

Para ver un ejemplo concreto, supongamos que estamos escribiendo una clase para modelar un refugio de animales . Suponemos que Cates una subclase de Animaly que tenemos una clase base (usando la sintaxis de Java)

diagrama UML
clase  Refugio de animales {  Animal getAnimalForAdoption () { // ... } void putAnimal ( Animal animal ) { //... } }           

Ahora la pregunta es: si subclasificamos AnimalShelter, ¿qué tipos podemos darle a getAnimalForAdoptionand putAnimal?

Tipo de retorno del método covariante

En un lenguaje que permite tipos de retorno covariantes , una clase derivada puede anular el getAnimalForAdoptionmétodo para devolver un tipo más específico:

diagrama UML
clase  CatShelter extiende AnimalShelter {    Gato getAnimalForAdoption () { return nuevo Gato (); } }      

Entre los principales lenguajes OO, Java , C++ y C# (a partir de la versión 9.0 [7] ) admiten tipos de retorno covariantes. Agregar el tipo de retorno covariante fue una de las primeras modificaciones del lenguaje C++ aprobada por el comité de estándares en 1998. [8] Scala y D también admiten tipos de retorno covariantes.

Tipo de parámetro del método contravariante

De manera similar, es seguro permitir que un método predominante acepte un argumento más general que el método de la clase base:

diagrama UML
clase  CatShelter extiende AnimalShelter { void putAnimal ( Objeto animal ) { // ... } }         

Sólo unos pocos lenguajes orientados a objetos realmente permiten esto (por ejemplo, Python cuando se verifica el tipo con mypy). C++, Java y la mayoría de los demás lenguajes que admiten la sobrecarga y/o el sombreado interpretarían esto como un método con un nombre sobrecargado o sombreado.

Sin embargo, Sather apoyó tanto la covarianza como la contravarianza. La convención de llamada para métodos anulados es covariante sin parámetros ni valores de retorno, y contravariante con parámetros normales (con el modo en ).

Tipo de parámetro del método covariante

Un par de lenguajes convencionales, Eiffel y Dart [9], permiten que los parámetros de un método primordial tengan un tipo más específico que el método de la superclase (covarianza del tipo de parámetro). Por lo tanto, el siguiente código Dart escribiría verificación, putAnimalanulando el método en la clase base:

diagrama UML
clase CatShelter extiende AnimalShelter {     void putAnimal ( animal gato covariante ) { // ... } }      

Esto no es seguro para tipos. Al elevar a CatSheltera an AnimalShelter, se puede intentar colocar un perro en un refugio para gatos. Eso no cumple con CatShelterlas restricciones de parámetros y resultará en un error de tiempo de ejecución. La falta de seguridad de tipos (conocida como el "problema de los abucheos" en la comunidad Eiffel, donde "cat" o "CAT" es una Disponibilidad o Tipo Cambiado) ha sido un problema de larga data. A lo largo de los años, se han propuesto varias combinaciones de análisis estático global, análisis estático local y nuevas características del lenguaje para remediarlo, [10] [11] y se han implementado en algunos compiladores de Eiffel.

A pesar del problema de seguridad de tipos, los diseñadores de Eiffel consideran que los tipos de parámetros covariantes son cruciales para modelar los requisitos del mundo real. [11] El refugio para gatos ilustra un fenómeno común: es un tipo de refugio para animales pero tiene restricciones adicionales , y parece razonable usar herencia y tipos de parámetros restringidos para modelar esto. Al proponer este uso de la herencia, los diseñadores de Eiffel rechazan el principio de sustitución de Liskov , que establece que los objetos de las subclases siempre deben estar menos restringidos que los objetos de su superclase.

Otro ejemplo de lenguaje convencional que permite la covarianza en los parámetros de los métodos es PHP en lo que respecta a los constructores de clases. En el siguiente ejemplo, se acepta el método __construct(), a pesar de que el parámetro del método es covariante con el parámetro del método principal. Si este método fuera diferente a __construct(), se produciría un error:

interfaz  AnimalInterface  {}interfaz  DogInterface  extiende  AnimalInterface  {}clase  Perro  implementa  DogInterface  {}clase  Mascota {  función pública  __construct ( AnimalInterface $animal ) {} }   clase  PetDog  extiende  Pet {  función pública  __construct ( DogInterface $perro ) { padre :: __construct ( $perro ); } }     

Otro ejemplo en el que los parámetros covariantes parecen útiles son los llamados métodos binarios, es decir, métodos en los que se espera que el parámetro sea del mismo tipo que el objeto sobre el que se invoca el método. Un ejemplo es el compareTométodo: comprueba si viene antes o después en algún orden, pero la forma de comparar, digamos, dos números racionales será diferente de la forma de comparar dos cadenas. Otros ejemplos comunes de métodos binarios incluyen pruebas de igualdad, operaciones aritméticas y operaciones de conjuntos como subconjunto y unión.a.compareTo(b)ab

En versiones anteriores de Java, el método de comparación se especificaba como una interfaz Comparable:

interfaz  comparable {  int compararA ( Objeto o ); }  

El inconveniente de esto es que el método está especificado para tomar un argumento de tipo Object. Una implementación típica primero descartaría este argumento (arrojando un error si no es del tipo esperado):

class  RationalNumber implementa Comparable { int numerador ; denominador entero ; //... public int compareTo ( Objeto otro ) { Número Racional otroNum = ( Número Racional ) otro ; devuelve un número entero . comparar ( numerador * otroNum . denominador , otroNum . numerador * denominador ); } }                          

En un lenguaje con parámetros covariantes, al argumento se le compareTopodría dar directamente el tipo deseado RationalNumber, ocultando el encasillamiento. (Por supuesto, esto aún daría un error de tiempo de ejecución si compareToluego se invocara, por ejemplo, a String).

Evitar la necesidad de tipos de parámetros covariantes

Otras características del lenguaje pueden proporcionar los beneficios aparentes de los parámetros covariantes al tiempo que preservan la sustituibilidad de Liskov.

En un lenguaje con genéricos (también conocido como polimorfismo paramétrico ) y cuantificación limitada , los ejemplos anteriores se pueden escribir de forma segura. [12] En lugar de definir AnimalShelter, definimos una clase parametrizada . (Un inconveniente de esto es que el implementador de la clase base necesita prever qué tipos necesitarán especializarse en las subclases).Shelter<T>

clase  Refugio < T extiende Animal > {    T getAnimalForAdoption () { // ... }     void putAnimal ( T animal ) { // ... } }      clase  CatShelter extiende Refugio < Gato > {    Gato getAnimalForAdoption () { // ... }     void putAnimal ( gato animal ) { // ... } }     

De manera similar, en versiones recientes de Java Comparablese ha parametrizado la interfaz, lo que permite omitir el downcast de forma segura:

clase  NúmeroRacional implementa Comparable < NúmeroRacional > {    numerador entero ; denominador entero ; // ... public int compareTo ( NumberRacional otherNum ) { return Integer . comparar ( numerador * otroNum . denominador , otroNum . numerador * denominador ); } }                   

Otra característica del idioma que puede ayudar es el envío múltiple . Una razón por la que los métodos binarios son difíciles de escribir es que en una llamada como , seleccionar la implementación correcta de realmente depende del tipo de tiempo de ejecución de ambos y , pero en un lenguaje OO convencional solo se tiene en cuenta el tipo de tiempo de ejecución de . En un lenguaje con despacho múltiple estilo Common Lisp Object System (CLOS) , el método de comparación podría escribirse como una función genérica donde ambos argumentos se usan para la selección del método.a.compareTo(b)compareToaba

Giuseppe Castagna [13] observó que en un lenguaje mecanografiado con envío múltiple, una función genérica puede tener algunos parámetros que controlan el envío y algunos parámetros "sobrantes" que no. Debido a que la regla de selección de método elige el método aplicable más específico, si un método reemplaza a otro método, entonces el método reemplazante tendrá tipos más específicos para los parámetros de control. Por otro lado, para garantizar la seguridad de tipos, el lenguaje aún debe exigir que los parámetros sobrantes sean al menos igual de generales. Usando la terminología anterior, los tipos utilizados para la selección del método en tiempo de ejecución son covariantes, mientras que los tipos no utilizados para la selección del método en tiempo de ejecución son contravariantes. Los lenguajes convencionales de envío único como Java también obedecen esta regla: solo se usa un argumento para la selección del método (el objeto receptor, pasado a un método como argumento oculto this) y, de hecho, el tipo de thises más especializado dentro de los métodos primordiales que en el superclase.

Castagna sugiere que los ejemplos en los que los tipos de parámetros covariantes son superiores (en particular, los métodos binarios) deberían manejarse mediante despacho múltiple; que es naturalmente covariante. Sin embargo, la mayoría de los lenguajes de programación no admiten envío múltiple.

Resumen de varianza y herencia.

La siguiente tabla resume las reglas para anular métodos en los idiomas discutidos anteriormente.

Tipos genéricos

En los lenguajes de programación que admiten genéricos (también conocidos como polimorfismo paramétrico ), el programador puede ampliar el sistema de tipos con nuevos constructores. Por ejemplo, una interfaz C# como hace posible construir nuevos tipos como o . Entonces surge la pregunta cuál debería ser la variación de estos constructores de tipos.IList<T>IList<Animal>IList<Cat>

Hay dos enfoques principales. En lenguajes con anotaciones de variación del sitio de declaración (por ejemplo, C# ), el programador anota la definición de un tipo genérico con la variación prevista de sus parámetros de tipo. Con las anotaciones de variación del sitio de uso (por ejemplo, Java ), el programador anota los lugares donde se crea una instancia de un tipo genérico.

Anotaciones de variación del sitio de declaración

Los lenguajes más populares con anotaciones de variación del sitio de declaración son C# y Kotlin (usando las palabras clave outy in), y Scala y OCaml (usando las palabras clave +y -). C# solo permite anotaciones de variación para tipos de interfaz, mientras que Kotlin, Scala y OCaml las permiten tanto para tipos de interfaz como para tipos de datos concretos.

Interfaces

En C#, cada parámetro de tipo de una interfaz genérica se puede marcar como covariante ( out), contravariante ( in) o invariante (sin anotación). Por ejemplo, podemos definir una interfaz de iteradores de solo lectura y declararla covariante (fuera) en su parámetro de tipo.IEnumerator<T>

interfaz IEnumerator < out T > { T Current { get ; } bool MoverSiguiente (); }         

Con esta declaración, IEnumeratorserá tratado como covariante en su parámetro de tipo, por ejemplo, es un subtipo de .IEnumerator<Cat>IEnumerator<Animal>

El verificador de tipos exige que cada declaración de método en una interfaz solo mencione los parámetros de tipo de manera consistente con las anotaciones in/ out. Es decir, un parámetro que fue declarado covariante no debe ocurrir en ninguna posición contravariante (donde una posición es contravariante si ocurre bajo un número impar de constructores de tipo contravariante). La regla precisa [14] [15] es que los tipos de retorno de todos los métodos en la interfaz deben ser válidos de manera covariante y todos los tipos de parámetros del método deben ser válidos de manera contravariante , donde S-ly válido se define de la siguiente manera:

Como ejemplo de cómo se aplican estas reglas, considere la interfaz. IList<T>

interfaz IList <T> { void Insertar ( int índice , T elemento ) ; _ IEnumerator < T > GetEnumerator (); }        

El tipo Tde parámetro Insertdebe ser válido de forma contravariante, es decir, el parámetro de tipo Tno debe estar etiquetado out. De manera similar, el tipo de resultado debe ser válido de manera covariante, es decir, (dado que es una interfaz covariante) el tipo debe ser válido de manera covariante, es decir, el parámetro de tipo no debe estar etiquetado . Esto muestra que no se permite marcar la interfaz como covariante o contravariante.IEnumerator<T>GetEnumeratorIEnumeratorTTinIList

En el caso común de una estructura de datos genérica como IList, estas restricciones significan que un outparámetro sólo se puede utilizar para métodos que obtienen datos de la estructura, y un inparámetro sólo se puede utilizar para métodos que introducen datos en la estructura, de ahí la elección de palabras clave.

Datos

C# permite anotaciones de variación en los parámetros de las interfaces, pero no en los parámetros de las clases. Debido a que los campos en las clases de C# siempre son mutables, las clases con parámetros variantes en C# no serían muy útiles. Pero los lenguajes que enfatizan datos inmutables pueden hacer un buen uso de los tipos de datos covariantes. Por ejemplo, en todos Scala , Kotlin y OCaml el tipo de lista inmutable es covariante: es un subtipo de .List[Cat]List[Animal]

Las reglas de Scala para verificar las anotaciones de varianza son esencialmente las mismas que las de C#. Sin embargo, existen algunos modismos que se aplican a estructuras de datos inmutables en particular. Se ilustran con la siguiente (extracto de) definición de la clase.List[A]

clase abstracta sellada Lista [ + A ] extiende AbstractSeq [ A ] { def head : A def tail : Lista [ A ]             /** Agrega un elemento al principio de esta lista. */ def :: [ B >: A ] ( x : B ): Lista [ B ] = nueva scala . recopilación . inmutable . :: ( x , esto ) /** ... */ }            

Primero, los miembros de la clase que tienen un tipo variante deben ser inmutables. Aquí, headtiene el tipo A, que fue declarado covariante ( +), y de hecho headfue declarado como método ( def). Intentar declararlo como un campo mutable ( var) sería rechazado como error de tipo.

En segundo lugar, incluso si una estructura de datos es inmutable, a menudo tendrá métodos en los que el tipo de parámetro se produce de forma contravariante. Por ejemplo, considere el método ::que agrega un elemento al principio de una lista. (La implementación funciona creando un nuevo objeto de la clase :: con nombre similar , la clase de listas no vacías). El tipo más obvio para darle sería

def :: ( x : A ): Lista [ A ]    

Sin embargo, esto sería un error de tipo, porque el parámetro covariante Aaparece en una posición contravariante (como un parámetro de función). Pero existe un truco para solucionar este problema. Damos ::un tipo más general, que permite agregar un elemento de cualquier tipo B siempre que Bsea un supertipo de A. Tenga en cuenta que esto depende de Listque sea covariante, ya que this tiene tipo y lo tratamos como si tuviera tipo . A primera vista puede no ser obvio que el tipo generalizado sea correcto, pero si el programador comienza con la declaración de tipo más simple, los errores de tipo señalarán el lugar que necesita generalizarse.List[A]List[B]

Inferir varianza

Es posible diseñar un sistema de tipos donde el compilador infiera automáticamente las mejores anotaciones de varianza posibles para todos los parámetros de tipos de datos. [16] Sin embargo, el análisis puede volverse complejo por varias razones. Primero, el análisis es no local ya que la varianza de una interfaz Idepende de la varianza de todas las interfaces que Imenciona. En segundo lugar, para obtener mejores soluciones únicas, el sistema de tipos debe permitir parámetros bivariantes (que sean simultáneamente covariantes y contravariantes). Y finalmente, la variación de los parámetros de tipo debería ser una elección deliberada por parte del diseñador de una interfaz, no algo que simplemente sucede.

Por estas razones [17] la mayoría de los lenguajes hacen muy poca inferencia de varianza. C# y Scala no infieren ninguna anotación de variación. OCaml puede inferir la varianza de tipos de datos concretos parametrizados, pero el programador debe especificar explícitamente la varianza de tipos abstractos (interfaces).

Por ejemplo, considere un tipo de datos OCaml Tque envuelve una función

escriba  ( ' a ,  ' b )  t  =  T  de  ( ' a  ->  ' b )

El compilador inferirá automáticamente que Tes contravariante en el primer parámetro y covariante en el segundo. El programador también puede proporcionar anotaciones explícitas, que el compilador comprobará si se cumplen. Así la siguiente declaración es equivalente a la anterior:

escriba  (- ' a ,  + ' b )  t  =  T  de  ( ' a  ->  ' b )

Las anotaciones explícitas en OCaml resultan útiles al especificar interfaces. Por ejemplo, la interfaz de biblioteca estándar para tablas de asociación incluye una anotación que dice que el constructor del tipo de mapa es covariante en el tipo de resultado.Map.S

 tipo de  módulo S  =  tipo sig  tipo de  clave  (+ ' a ) t val vacío : ' a t val mem : clave -> ' a t -> bool ... fin                

Esto garantiza que, por ejemplo, sea un subtipo de .cat IntMap.tanimal IntMap.t

Anotaciones de variación del sitio de uso (comodines)

Una desventaja del enfoque del sitio de declaración es que muchos tipos de interfaz deben hacerse invariantes. Por ejemplo, vimos arriba que IListtenía que ser invariante, porque contenía tanto Insertcomo GetEnumerator. Para exponer más variación, el diseñador de API podría proporcionar interfaces adicionales que proporcionen subconjuntos de los métodos disponibles (por ejemplo, una "lista de solo inserción" que solo proporcione Insert). Sin embargo, esto rápidamente se vuelve difícil de manejar.

Variación del sitio de uso significa que la variación deseada se indica con una anotación en el sitio específico en el código donde se utilizará el tipo. Esto brinda a los usuarios de una clase más oportunidades para crear subtipos sin necesidad de que el diseñador de la clase defina múltiples interfaces con diferentes variaciones. En cambio, en el momento en que se crea una instancia de un tipo genérico en un tipo parametrizado real, el programador puede indicar que solo se utilizará un subconjunto de sus métodos. De hecho, cada definición de una clase genérica también pone a disposición interfaces para las partes covariantes y contravariantes de esa clase.

Java proporciona anotaciones de variación del sitio de uso mediante comodines , una forma restringida de tipos existenciales acotados . Se puede crear una instancia de un tipo parametrizado mediante un comodín junto con un límite superior o inferior, por ejemplo, o . Un comodín ilimitado como equivale a . Tal tipo representa algún tipo desconocido que satisface el límite. [4] : 139  Por ejemplo, si tiene tipo , entonces el verificador de tipo aceptará?List<? extends Animal>List<? super Animal>List<?>List<? extends Object>List<X>XlList<? extends Animal>

Animal a = l . obtener ( 3 );   

porque Xse sabe que el tipo es un subtipo de Animal, pero

yo . agregar ( nuevo Animal ()); 

será rechazado como un error de tipo ya que an Animalno es necesariamente un X. En general, dada alguna interfaz , una referencia a prohíbe el uso de métodos de la interfaz donde ocurre de manera contravariante en el tipo de método. Por el contrario, si tuviera tipo uno podría llamar pero no .I<T>I<? extends T>TlList<? super Animal>l.addl.get

Los subtipos comodín en Java se pueden visualizar como un cubo.

Mientras que los tipos parametrizados sin comodines en Java son invariantes (por ejemplo, no existe una relación de subtipo entre y ), los tipos con comodines se pueden hacer más específicos especificando un límite más estricto. Por ejemplo, es un subtipo de . Esto muestra que los tipos comodín son covariantes en sus límites superiores (y también contravariantes en sus límites inferiores ). En total, dado un tipo comodín como , hay tres formas de formar un subtipo: especializando la clase , especificando un límite más estricto o reemplazando el comodín con un tipo específico (ver figura). [4] : 139 List<Cat>List<Animal>List<? extends Cat>List<? extends Animal>C<? extends T>CT?

Al aplicar dos de las tres formas de subtipificación anteriores, es posible, por ejemplo, pasar un argumento de tipo a un método que espera un . Este es el tipo de expresividad que resulta de los tipos de interfaz covariantes. El tipo actúa como un tipo de interfaz que contiene solo los métodos covariantes de , pero el implementador de no tuvo que definirlo de antemano.List<Cat>List<? extends Animal>List<? extends Animal>List<T>List<T>

En el caso común de una estructura de datos genérica IList, los parámetros covariantes se utilizan para los métodos que obtienen datos de la estructura y los parámetros contravariantes para los métodos que colocan datos en la estructura. El mnemotécnico para Producer Extends, Consumer Super (PECS), del libro Effective Java de Joshua Bloch, ofrece una forma sencilla de recordar cuándo utilizar la covarianza y la contravarianza. [4] : 141 

Los comodines son flexibles, pero tienen un inconveniente. Si bien la variación del sitio de uso significa que los diseñadores de API no necesitan considerar la variación de los parámetros de tipo en las interfaces, a menudo deben utilizar firmas de métodos más complicados. Un ejemplo común involucra la Comparableinterfaz. [4] : 66  Supongamos que queremos escribir una función que encuentre el elemento más grande de una colección. Los elementos necesitan implementar el compareTométodo, [4] : 66  por lo que un primer intento podría ser

< T extiende Comparable < T >> T max ( Colección < T > coll );     

Sin embargo, este tipo no es lo suficientemente general: se puede encontrar el máximo de a , pero no de a . El problema es que no lo implementa , sino la (mejor) interfaz . En Java, a diferencia de C#, no se considera un subtipo de . En cambio, se debe modificar el tipo de :Collection<Calendar>Collection<GregorianCalendar>GregorianCalendarComparable<GregorianCalendar>Comparable<Calendar>Comparable<Calendar>Comparable<GregorianCalendar>max

< T extiende Comparable <? super T >> T max ( Colección < T > coll );       

El comodín acotado transmite la información que llama solo a métodos contravariantes desde la interfaz. Este ejemplo en particular es frustrante porque todos los métodos son contravariantes, por lo que la condición es trivialmente cierta. Un sistema de sitio de declaración podría manejar este ejemplo con menos desorden al anotar solo la definición de .? super TmaxComparableComparableComparable

El maxmétodo se puede cambiar aún más utilizando un comodín de límite superior para el parámetro del método: [18]

< T extiende Comparable <? super T >> T max ( Colección <? extiende T > coll );         

Comparación de anotaciones de sitio de declaración y sitio de uso

Las anotaciones de variación del sitio de uso brindan flexibilidad adicional, lo que permite que más programas realicen comprobaciones de tipo. Sin embargo, han sido criticados por la complejidad que añaden al lenguaje, lo que genera firmas tipográficas complicadas y mensajes de error.

Una forma de evaluar si la flexibilidad adicional es útil es ver si se utiliza en programas existentes. Una encuesta de un gran conjunto de bibliotecas Java [16] encontró que el 39% de las anotaciones comodín podrían haber sido reemplazadas directamente por anotaciones de sitio de declaración. Así, el 61% restante es una indicación de los lugares donde Java se beneficia al tener disponible el sistema de sitio de uso.

En un lenguaje de sitio de declaración, las bibliotecas deben exponer menos variación o definir más interfaces. Por ejemplo, la biblioteca Scala Collections define tres interfaces separadas para clases que emplean covarianza: una interfaz base covariante que contiene métodos comunes, una versión mutable invariante que agrega métodos de efectos secundarios y una versión inmutable covariante que puede especializar las implementaciones heredadas para explotar estructuras. intercambio. [19] Este diseño funciona bien con anotaciones de sitio de declaración, pero la gran cantidad de interfaces conlleva un costo de complejidad para los clientes de la biblioteca. Y modificar la interfaz de la biblioteca puede no ser una opción; en particular, uno de los objetivos al agregar genéricos a Java era mantener la compatibilidad binaria hacia atrás.

Por otro lado, los comodines de Java son complejos en sí mismos. En una presentación en una conferencia [20], Joshua Bloch los criticó por ser demasiado difíciles de entender y utilizar, afirmando que al añadir apoyo para los cierres "simplemente no podemos permitirnos otros comodines ". Las primeras versiones de Scala usaban anotaciones de variación del sitio de uso, pero a los programadores les resultaba difícil usarlas en la práctica, mientras que las anotaciones del sitio de declaración resultaron muy útiles al diseñar clases. [21] Las versiones posteriores de Scala agregaron comodines y tipos existenciales de estilo Java; sin embargo, según Martin Odersky , si no hubiera necesidad de interoperabilidad con Java, probablemente no se habrían incluido. [22]

Ross Tate sostiene [23] que parte de la complejidad de los comodines de Java se debe a la decisión de codificar la variación del sitio de uso utilizando una forma de tipos existenciales. Las propuestas originales [24] [25] utilizaban una sintaxis de propósito especial para las anotaciones de varianza, escribiendo en lugar de la más detallada de Java .List<+Animal>List<? extends Animal>

Dado que los comodines son una forma de tipos existenciales, se pueden utilizar para más cosas además de la variación. Un tipo como ("una lista de tipos desconocidos" [26] ) permite que los objetos se pasen a métodos o se almacenen en campos sin especificar exactamente sus parámetros de tipo. Esto es particularmente valioso para clases en las que la mayoría de los métodos no mencionan el parámetro de tipo.List<?>Class

Sin embargo, la inferencia de tipos para tipos existenciales es un problema difícil. Para el implementador del compilador, los comodines de Java plantean problemas con la terminación del verificador de tipos, la inferencia de argumentos de tipos y programas ambiguos. [27] En general, es indecidible si un programa Java que utiliza genéricos está bien tipificado o no, [28] por lo que cualquier verificador de tipos tendrá que entrar en un bucle infinito o un tiempo de espera para algunos programas. Para el programador, genera mensajes de error de tipo complicado. El tipo de Java verifica los tipos de comodines reemplazando los comodines con variables de tipo nuevas (la llamada conversión de captura ). Esto puede hacer que los mensajes de error sean más difíciles de leer, porque se refieren a variables de tipo que el programador no escribió directamente. Por ejemplo, intentar agregar a Cata dará un error comoList<? extends Animal>

El método List.add (captura n.° 1) no es aplicable (El argumento real Cat no se puede convertir a capture#1 mediante la conversión de invocación del método)donde captura#1 es una nueva variable de tipo: capture#1 extiende Animal desde la captura de ? se extiende Animal

Dado que tanto las anotaciones de sitio de declaración como las de sitio de uso pueden ser útiles, algunos sistemas de tipos proporcionan ambas. [16] [23]

Etimología

These terms come from the notion of covariant and contravariant functors in category theory. Consider the category whose objects are types and whose morphisms represent the subtype relationship ≤. (This is an example of how any partially ordered set can be considered as a category.) Then for example the function type constructor takes two types p and r and creates a new type pr; so it takes objects in to objects in . By the subtyping rule for function types this operation reverses ≤ for the first parameter and preserves it for the second, so it is a contravariant functor in the first parameter and a covariant functor in the second.

See also

References

  1. ^ This only happens in a pathological case. For example, I<T> = int: any type can be put in for T and the result is still int.
  2. ^ a b c Skeet, Jon (23 March 2019). C# in Depth. Manning. ISBN 978-1617294532.
  3. ^ Func<T, TResult> Delegate - MSDN Documentation
  4. ^ a b c d e f g h Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (third ed.). Addison-Wesley. ISBN 978-0134685991.
  5. ^ Reynolds, John C. (1981). The Essence of Algol. Symposium on Algorithmic Languages. North-Holland.
  6. ^ Cardelli, Luca (1984). A semantics of multiple inheritance (PDF). Semantics of Data Types (International Symposium Sophia-Antipolis, France, June 27–29, 1984). Lecture Notes in Computer Science. Vol. 173. Springer. pp. 51–67. doi:10.1007/3-540-13346-1_2. ISBN 3-540-13346-1.
    Longer version: — (February 1988). "A semantics of multiple inheritance". Information and Computation. 76 (2/3): 138–164. CiteSeerX 10.1.1.116.1298. doi:10.1016/0890-5401(88)90007-7.
  7. ^ Torgersen, Mads. "C# 9.0 on the record".
  8. ^ Allison, Chuck. "What's New in Standard C++?".
  9. ^ "Fixing Common Type Problems". Dart Programming Language.
  10. ^ Bertrand Meyer (October 1995). "Static Typing" (PDF). OOPSLA 95 (Object-Oriented Programming, Systems, Languages and Applications), Atlanta, 1995.
  11. ^ ab Howard, Mark; Bezault, Eric; Meyer, Bertrand; Colnet, Dominique; Stapf, Emmanuel; Arnout, Karine; Keller, Markus (abril de 2003). "Covarianza de tipo seguro: los compiladores competentes pueden captar todos los abucheos" (PDF) . Consultado el 23 de mayo de 2013 .
  12. ^ Franz Weber (1992). "Obtener el equivalente de corrección de clase y corrección del sistema: cómo lograr la covarianza correcta". TOOLS 8 (8.ª conferencia sobre tecnología de lenguajes y sistemas orientados a objetos), Dortmund, 1992 . CiteSeerX 10.1.1.52.7872 . 
  13. ^ Castagna, Giuseppe (mayo de 1995). "Covarianza y contravarianza: conflicto sin causa". Transacciones ACM sobre lenguajes y sistemas de programación . 17 (3): 431–447. CiteSeerX 10.1.1.115.5992 . doi :10.1145/203095.203096. S2CID  15402223. 
  14. ^ Lippert, Eric (3 de diciembre de 2009). "Reglas exactas para la validez de la variación" . Consultado el 16 de agosto de 2016 .
  15. ^ "Sección II.9.7". Estándar internacional ECMA ECMA-335 Infraestructura de lenguaje común (CLI) (6ª ed.). Junio ​​2012.
  16. ^ a b C Altidor, John; Shan, Huang Shan; Smaragdakis, Yannis (2011). "Dominar los comodines: combinar la variación del sitio de uso y definición". Actas de la 32ª conferencia ACM SIGPLAN sobre diseño e implementación de lenguajes de programación (PLDI'11) . ACM. págs. 602–613. CiteSeerX 10.1.1.225.8265 . doi :10.1145/1993316.1993569. ISBN  9781450306638.{{cite conference}}: CS1 maint: date and year (link)
  17. ^ Lippert, Eric (29 de octubre de 2007). "Covarianza y contravarianza en C#, parte siete: ¿Por qué necesitamos una sintaxis?" . Consultado el 16 de agosto de 2016 .
  18. ^ Bloch 2018, págs. 139-145, capítulo §5, punto 31: Utilice comodines acotados para aumentar la flexibilidad de la API.
  19. ^ Odersky, Marin; Cuchara, Lex (7 de septiembre de 2010). "La API de colecciones de Scala 2.8" . Consultado el 16 de agosto de 2016 .
  20. ^ Bloch, Joshua (noviembre de 2007). "La controversia de los cierres [vídeo]". Presentación en Javapolis'07. Archivado desde el original el 2 de febrero de 2014.{{cite web}}: CS1 maint: location (link)
  21. ^ Odersky, Martín; Zenger, Matías (2005). "Abstracciones de componentes escalables" (PDF) . Actas de la vigésima conferencia anual ACM SIGPLAN sobre programación, sistemas, lenguajes y aplicaciones orientados a objetos (OOPSLA '05) . ACM. págs. 41–57. CiteSeerX 10.1.1.176.5313 . doi :10.1145/1094811.1094815. ISBN  1595930310.
  22. ^ Venners, Bill; Sommers, Frank (18 de mayo de 2009). "El propósito del sistema de tipos de Scala: una conversación con Martin Odersky, parte III" . Consultado el 16 de agosto de 2016 .
  23. ^ ab Tate, Ross (2013). "Variación de sitios mixtos". FOOL '13: Actas informales del vigésimo taller internacional sobre fundamentos de los lenguajes orientados a objetos . CiteSeerX 10.1.1.353.4691 . 
  24. ^ Igarashi, Atsushi; Viroli, Mirko (2002). "Sobre la subtipificación basada en variaciones para tipos paramétricos". Actas de la 16ª Conferencia Europea sobre Programación Orientada a Objetos (ECOOP '02) . Apuntes de conferencias sobre informática. vol. 2374, págs. 441–469. CiteSeerX 10.1.1.66.450 . doi :10.1007/3-540-47993-7_19. ISBN  3-540-47993-7.
  25. ^ Thorup, Kresten Krab; Torgersen, Mads (1999). "Unificación de genericidad: combinación de los beneficios de tipos virtuales y clases parametrizadas". Programación Orientada a Objetos (ECOOP '99) . Apuntes de conferencias sobre informática. vol. 1628. Saltador. págs. 186-204. CiteSeerX 10.1.1.91.9795 . doi :10.1007/3-540-48743-3_9. ISBN  3-540-48743-3.{{cite conference}}: CS1 maint: date and year (link)
  26. ^ "Tutoriales de Java™, genéricos (actualizados), comodines ilimitados" . Consultado el 17 de julio de 2020 .
  27. ^ Tate, Ross; Leung, Alan; Lerner, Sorin (2011). "Dominar los comodines en el sistema de tipos de Java". Actas de la 32ª conferencia ACM SIGPLAN sobre diseño e implementación de lenguajes de programación (PLDI '11) . págs. 614–627. CiteSeerX 10.1.1.739.5439 . ISBN  9781450306638.
  28. ^ Grigore, Radu (2017). "Los genéricos de Java se están completando". Actas del 44º Simposio ACM SIGPLAN sobre principios de lenguajes de programación (POPL'17) . págs. 73–85. arXiv : 1605.05274 . Código Bib : 2016arXiv160505274G. ISBN 9781450346603.

enlaces externos