Un buen sistema de tipos genéricos

29

Es comúnmente aceptado que los genéricos de Java fallaron de algunas maneras importantes. La combinación de comodines y límites condujo a un código seriamente ilegible.

Sin embargo, cuando miro otros idiomas, realmente parece que no puedo encontrar un sistema de tipo genérico con el que los programadores estén contentos.

Si tomamos lo siguiente como objetivos de diseño de un sistema de este tipo:

  • Siempre produce declaraciones de tipo fácil de leer
  • Fácil de aprender (no es necesario repasar la covarianza, la contravarianza, etc.)
  • maximiza el número de errores en tiempo de compilación

¿Hay algún idioma que lo haya entendido bien? Si busco en Google, lo único que veo son quejas sobre cómo el sistema de tipos es un asco en el lenguaje X. ¿Es este tipo de complejidad inherente a la escritura genérica? ¿Deberíamos renunciar a intentar verificar la seguridad del tipo al 100% en tiempo de compilación?

Mi pregunta principal es cuál es el lenguaje que "acertó" mejor con respecto a estos tres objetivos. Me doy cuenta de que eso es subjetivo, pero hasta ahora ni siquiera puedo encontrar un lenguaje en el que no todos sus programadores estén de acuerdo en que el sistema de tipos genéricos es un desastre.

Anexo: como se señaló, la combinación de subtipo / herencia y genéricos es lo que crea la complejidad, por lo que realmente estoy buscando un lenguaje que combine ambos y evite la explosión de la complejidad.

Peter
fuente
2
¿Qué quieres decir con easy-to-read type declarations? El tercer criterio también es ambiguo: por ejemplo, puedo convertir las excepciones fuera de límites del índice de matriz en errores de tiempo de compilación al no permitirle indexar matrices a menos que pueda calcular el índice en tiempo de compilación. Además, el segundo criterio descarta el subtipo. Eso no es necesariamente algo malo, pero debes saber lo que estás preguntando.
Doval
9
@gnat, esto definitivamente no es una protesta contra Java. Programo casi exclusivamente en Java. Mi punto es que, en general, se acepta dentro de la comunidad de Java que los genéricos tienen fallas (no es una falla total, sino probablemente parcial), por lo que es una pregunta lógica preguntar cómo deberían haberse implementado. ¿Por qué están equivocados y otros lo hicieron bien? ¿O es realmente imposible obtener genéricos absolutamente correctos?
Peter
1
Si todos robaran de C #, habría menos quejas. Especialmente Java está en condiciones de ponerse al día copiando. En su lugar, deciden sobre soluciones inferiores. Muchas de las preguntas que aún discuten los comités de diseño de Java ya se han decidido e implementado en C #. Ni siquiera parecen mirar.
usr
2
@emodendroket: creo que mis dos mayores quejas sobre los genéricos de C # son que no hay forma de aplicar una restricción de "supertipo" (por ejemplo Foo<T> where SiameseCat:T) y que no hay posibilidad de tener un tipo genérico que no sea convertible Object. En mi humilde opinión, .NET se beneficiaría de los tipos agregados que eran similares a las estructuras, pero aún más deshuesados. Si se KeyValuePair<TKey,TValue>tratara de un tipo de este tipo, se IEnumerable<KeyValuePair<SiameseCat,FordFocus>>podría emitir un IEnumerable<KeyValuePair<Animal,Vehicle>>, pero solo si el tipo no se puede encuadrar.
supercat

Respuestas:

24

Si bien los genéricos se han generalizado en la comunidad de programación funcional durante décadas, agregar genéricos a los lenguajes de programación orientados a objetos ofrece algunos desafíos únicos, específicamente la interacción de subtipos y genéricos.

Sin embargo, incluso si nos centramos en los lenguajes de programación orientados a objetos, y Java en particular, podría haberse diseñado un sistema genérico mucho mejor:

  1. Los tipos genéricos deben ser admisibles en cualquier otro lugar. En particular, si Tes un parámetro de tipo, las siguientes expresiones deben compilarse sin advertencias:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Sí, esto requiere que los genéricos sean reificados, como cualquier otro tipo de idioma.

  2. La covarianza y la contravarianza de un tipo genérico deben especificarse (o inferirse de) su declaración, en lugar de cada vez que se usa el tipo genérico, para que podamos escribir

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    más bien que

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Como los tipos genéricos pueden ser bastante largos, no deberíamos necesitar especificarlos de forma redundante. Es decir, deberíamos poder escribir

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    más bien que

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Cualquier tipo debe ser admisible como parámetro de tipo, no solo tipos de referencia. (Si podemos tener un int[], ¿por qué no podemos tener un List<int>)?

Todo esto es posible en C #.

meriton - en huelga
fuente
1
¿Esto también eliminaría los genéricos autorreferenciales? ¿Qué sucede si quiero decir que un objeto comparable puede compararse con cualquier cosa del mismo tipo o una subclase? ¿Se puede hacer eso? O si escribo un método de clasificación que acepta listas con objetos comparables, todos deben ser comparables entre sí. Enum es otro buen ejemplo: Enum <E extiende Enum <E>>. No digo que un sistema de tipos pueda hacer esto, solo tengo curiosidad por saber cómo C # maneja estas situaciones.
Peter
1
La inferencia de tipos genéricos de Java 7 y la ayuda automática de C ++ con algunas de estas preocupaciones, pero son azúcar sintáctica y no cambian los mecanismos subyacentes.
Sin embargo, la inferencia de tipos de @Snowman Java tiene algunos casos de esquina realmente desagradables, como no trabajar con clases anónimas y no encontrar los límites correctos para los comodines cuando evalúa un método genérico como argumento para otro método genérico.
Doval
@Doval, es por eso que dije que ayuda con algunas de las preocupaciones: no soluciona nada y no aborda todo. Los genéricos de Java tienen muchos problemas: aunque son mejores que los tipos sin formato, ciertamente causan muchos dolores de cabeza.
34

El uso de subtipos crea muchas complicaciones al hacer programación genérica. Si insiste en usar un lenguaje con subtipos, debe aceptar que existe una cierta complejidad inherente en la programación genérica que viene con él. Algunos idiomas lo hacen mejor que otros, pero solo puedes llevarlo tan lejos.

Contrasta eso con los genéricos de Haskell, por ejemplo. Son lo suficientemente simples como para que si usa la inferencia de tipos, puede escribir una función genérica correcta por accidente . De hecho, si se especifica un solo tipo, el compilador dice a menudo a sí mismo, "Bueno, se va a hacer de este genérico, pero me pidió para que sea sólo para enteros, por lo que sea."

Es cierto que las personas usan el sistema de tipos de Haskell de maneras sorprendentemente complejas, lo que lo convierte en la pesadilla de todo novato, pero el sistema de tipos subyacente es elegante y muy admirado.

Karl Bielefeldt
fuente
1
Gracias por esta respuesta Este artículo comienza con algunos de los ejemplos de Joshua Bloch sobre dónde los genéricos se vuelven demasiado complicados: artima.com/weblogs/viewpost.jsp?thread=222021 . ¿Es esta una diferencia en la cultura entre Java y Haskell, donde tales construcciones se considerarían bien en Haskell, o hay una diferencia real en el sistema de tipos de Haskell que evita tales situaciones?
Peter
10
@Peter Haskell no tiene subtipos, y como Karl dijo que el compilador puede inferir los tipos automáticamente, incluidas restricciones como "el tipo adebe ser algún tipo de número entero".
Doval
En otras palabras , covarianza , en idiomas como Scala.
Paul Draper
14

Hubo bastante investigación sobre la combinación de genéricos con subtipos hace unos 20 años. El lenguaje de programación Thor desarrollado por el grupo de investigación de Barbara Liskov en el MIT tenía una noción de cláusulas "dónde" que le permiten especificar los requisitos del tipo sobre el que está parametrizando. (Esto es similar a lo que C ++ intenta hacer con Concepts ).

El artículo que describe los genéricos de Thor y cómo interactúan con los subtipos de Thor es: Day, M; Gruber, R; Liskov, B; Myers, AC: subtipos frente a cláusulas where: polimorfismo paramétrico restrictivo , ACM Conf en Obj-Oriented Prog, Sys, Lang y Apps , (OOPSLA-10): 156-158, 1995.

Creo que, a su vez, se basaron en el trabajo realizado en Emerald a fines de la década de 1980. (No he leído ese trabajo, pero la referencia es: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribución y tipos abstractos en esmeralda , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Tanto Thor como Emerald eran "lenguajes académicos", por lo que probablemente no obtuvieron suficiente uso para que la gente realmente entendiera si las cláusulas (conceptos) realmente resuelven algún problema real. Es interesante leer el artículo de Bjarne Stroustrup sobre por qué falló el primer intento en Conceptos en C ++: Stroustrup, B: La decisión "Eliminar conceptos" C ++ 0x , Dr. Dobbs , 22 de julio de 2009. (Más información en la página de inicio de Stroustrup . )

Otra dirección que la gente parece estar intentando es algo llamado rasgos . Por ejemplo, el lenguaje de programación Rust de Mozilla usa rasgos. Según tengo entendido (que puede estar completamente equivocado), declarar que una clase satisface un rasgo es muy parecido a decir que una clase implementa una interfaz, pero usted dice "se comporta como un" en lugar de "es un". Parece que los nuevos lenguajes de programación Swift de Apple están utilizando un concepto similar de protocolos para especificar restricciones en los parámetros a los genéricos .

Lógica Errante
fuente
Los rasgos de óxido son similares a las clases de tipo Haskell. Pero hay dos ángulos que uno puede ver: 1. Véalo como un medio para establecer restricciones. 2. Defina una interfaz genérica, que se puede asociar a un tipo concreto o una clase genérica de tipos.
BitTickler hace