Pregunta ¿Cuáles son los beneficios del borrado de tipos de Java?


yo leo un Pío hoy que dijo:

Es curioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java acierta, mientras ignoran todas las cosas que se equivocaron.

Por lo tanto, mi pregunta es:

¿Hay beneficios del borrado de tipos de Java? ¿Cuáles son los beneficios de estilo técnico o de programación que (posiblemente) ofrece, aparte de las implementaciones de JVM, preferencia por compatibilidad con versiones anteriores y rendimiento en tiempo de ejecución?


73
2018-01-04 08:20


origen


Respuestas:


Tipo Erasure es bueno

Mantengamos los hechos

Muchas de las respuestas hasta ahora están demasiado preocupadas por el usuario de Twitter. Es útil mantenerse enfocado en los mensajes y no en el mensajero. Hay un mensaje bastante consistente incluso con solo los extractos mencionados hasta ahora:

Es curioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java acierta, mientras ignoran todas las cosas que se equivocaron.

Obtengo grandes beneficios (por ejemplo, parametricidad) y ningún costo (el supuesto costo es un límite de imaginación).

nuevo T es un programa roto. Es isomorfo a la afirmación de que "todas las proposiciones son verdaderas". No soy grande en esto.

Un objetivo: programas razonables

Estos tweets reflejan una perspectiva que no está interesada en si podemos hacer que la máquina lo haga alguna cosa, pero más si podemos razonar que la máquina hará algo que realmente queremos. Un buen razonamiento es una prueba. Las pruebas pueden especificarse en notación formal o algo menos formal. Independientemente del lenguaje de especificación, deben ser claros y rigurosos. Las especificaciones informales no son imposibles de estructurar correctamente, pero a menudo tienen fallas en la programación práctica. Terminamos con remedios como pruebas automáticas y exploratorias para compensar los problemas que tenemos con el razonamiento informal. Esto no quiere decir que las pruebas sean intrínsecamente una mala idea, pero el usuario de Twitter citado sugiere que hay una manera mucho mejor.

Entonces, nuestro objetivo es tener programas correctos con los que podamos razonar clara y rigurosamente de manera que correspondan con la forma en que la máquina realmente ejecutará el programa. Esto, sin embargo, no es el único objetivo. También queremos que nuestra lógica tenga un grado de expresividad. Por ejemplo, hay mucho que podemos expresar con lógica proposicional. Es bueno tener una cuantificación universal (∀) y existencial (from) a partir de algo así como la lógica de primer orden.

Usando sistemas de tipo para razonar

Estos objetivos pueden ser abordados muy bien por los sistemas de tipos. Esto es especialmente claro debido a la Correspondencia de Curry-Howard. Esta correspondencia a menudo se expresa con la siguiente analogía: los tipos son para los programas como los teoremas son para las pruebas.

Esta correspondencia es algo profunda. Podemos tomar expresiones lógicas y traducirlas mediante la correspondencia a tipos. Entonces, si tenemos un programa con la misma firma de tipo que compila, hemos demostrado que la expresión lógica es universalmente cierta (una tautología). Esto se debe a que la correspondencia es bidireccional. La transformación entre los mundos de tipo / programa y teorema / prueba es mecánica, y en muchos casos puede ser automática.

Curry-Howard juega muy bien con lo que nos gustaría hacer con las especificaciones de un programa.

¿Los sistemas de tipos son útiles en Java?

Incluso con una comprensión de Curry-Howard, algunas personas les resulta fácil descartar el valor de un sistema de tipo, cuando

  1. es extremadamente difícil trabajar con
  2. corresponde (a través de Curry-Howard) a una lógica con expresividad limitada
  3. está roto (lo que lleva a la caracterización de sistemas como "débil" o "fuerte").

Con respecto al primer punto, quizás los IDE hagan que el sistema de tipos de Java sea lo suficientemente fácil de trabajar (eso es muy subjetivo).

En cuanto al segundo punto, Java pasa a casi corresponden a una lógica de primer orden. Los genéricos utilizan el sistema de tipo equivalente a la cuantificación universal. Desafortunadamente, los comodines solo nos dan una pequeña fracción de cuantificación existencial. Pero la cuantificación universal es un buen comienzo. Es agradable poder decir que las funciones para List<A> trabajar universalmente para todo posible listas porque A no tiene ninguna restricción. Esto lleva a lo que el usuario de Twitter está hablando con respecto a la "parametricidad".

Un documento frecuentemente citado sobre la parametricidad es el de Philip Wadler. Teoremas gratis!. Lo interesante de este trabajo es que solo con la firma de tipo, podemos probar algunas invariantes muy interesantes. Si tuviéramos que escribir pruebas automatizadas para estas invariantes, estaríamos perdiendo nuestro tiempo. Por ejemplo, para List<A>, desde la firma tipo solo para flatten

<A> List<A> flatten(List<List<A>> nestedLists);

podemos razonar eso

flatten(nestedList.map(l -> l.map(any_function)))
    ≡ flatten(nestList).map(any_function)

Es un ejemplo simple, y probablemente puedas razonar sobre ello de manera informal, pero es aún más agradable cuando obtenemos formalmente esas pruebas de forma gratuita del sistema de tipo y las verifica el compilador.

No borrar puede conducir a abusos

Desde la perspectiva de la implementación del lenguaje, los genéricos de Java (que corresponden a los tipos universales) juegan un papel muy importante en la parametricidad utilizada para obtener pruebas sobre lo que hacen nuestros programas. Esto llega al tercer problema mencionado. Todas estas ganancias de prueba y corrección requieren un sistema de tipo de sonido implementado sin defectos. Java definitivamente tiene algunas características de lenguaje que nos permiten romper nuestro razonamiento. Estos incluyen pero no están limitados a:

  • efectos secundarios con un sistema externo
  • reflexión

Los genéricos no borrados están relacionados de muchas maneras con la reflexión. Sin borrado, hay información de tiempo de ejecución que se lleva con la implementación que podemos usar para diseñar nuestros algoritmos. Lo que esto significa es que estáticamente, cuando razonamos sobre los programas, no tenemos la imagen completa. La reflexión amenaza seriamente la corrección de cualquier prueba que razonamos sobre estáticamente. No es coincidencia que la reflexión también conduzca a una variedad de defectos complicados.

Entonces, ¿cuáles son las formas en que los genéricos no borrados pueden ser "útiles"? Consideremos el uso mencionado en el tweet:

<T> T broken { return new T(); }

¿Qué sucede si T no tiene un constructor no-arg? En algunos idiomas, lo que obtienes es nulo. O tal vez omita el valor nulo y vaya directamente a generar una excepción (que los valores nulos parecen conducir de todos modos). Debido a que nuestro lenguaje está completo, es imposible razonar sobre qué llamadas brokenimplicará tipos "seguros" con constructores no-arg y cuáles no. Perdimos la certeza de que nuestro programa funciona universalmente.

Borrar significa que hemos razonado (así que borremos)

Por lo tanto, si queremos razonar acerca de nuestros programas, se recomienda encarecidamente que no empleen funciones de lenguaje que amenacen nuestro razonamiento. Una vez que hacemos eso, ¿por qué no simplemente descartar los tipos en tiempo de ejecución? No son necesarios. Podemos obtener cierta eficiencia y simplicidad con la satisfacción de que ningún lanzamiento fallará o que faltarán métodos en la invocación.

Borrar borra el razonamiento.


75
2018-02-18 03:51



Los tipos son una construcción utilizada para escribir programas de una manera que permite al compilador verificar la corrección de un programa. Un tipo es una proposición sobre un valor: el compilador verifica que esta proposición es verdadera.

Durante la ejecución de un programa, no debería haber necesidad de información de tipo; esto ya ha sido verificado por el compilador. El compilador debe tener la libertad de descartar esta información para realizar optimizaciones en el código, hacer que funcione más rápido, generar un binario más pequeño, etc. El borrado de los parámetros de tipo facilita esto.

Java interrumpe el tipado estático al permitir que se consulte la información de tipo en el tiempo de ejecución: reflexión, instancia de etc. Esto le permite construir programas que no se pueden verificar estáticamente, omiten el sistema de tipos. También pierde oportunidades para la optimización estática.

El hecho de que los parámetros de tipo se borren impide que se creen algunas instancias de estos programas incorrectos, sin embargo, se rechazarían más programas incorrectos si se borrase más información de tipo y se eliminaran la reflexión y la instancia de las instalaciones.

La eliminación es importante para mantener la propiedad de "parametricidad" de un tipo de datos. Digamos que tengo un tipo "List" parametrizado sobre el componente tipo T. es decir, List <T>. Ese tipo es una proposición de que este tipo de lista funciona de forma idéntica para cualquier tipo T. El hecho de que T sea un parámetro de tipo abstracto, ilimitado significa que no sabemos nada sobre este tipo, por lo que no se puede hacer nada especial para casos especiales de T.

p.ej. decir que tengo una lista xs = asList ("3"). Agrego un elemento: xs.add ("q"). Termino con ["3", "q"]. Como esto es paramétrico, puedo suponer que List xs = asList (7); xs.add (8) termina con [7,8] Sé por el tipo que no hace una cosa por String y una cosa por Int.

Además, sé que la función List.add no puede inventar valores de T de la nada. Sé que si mi asList ("3") tiene un "7" agregado, las únicas respuestas posibles se construirían con los valores "3" y "7". No hay posibilidad de agregar un "2" o "z" a la lista porque la función no podría construirlo. Ninguno de estos otros valores sería razonable agregar, y la parametricidad evita que se construyan estos programas incorrectos.

Básicamente, el borrado evita algunos medios de violar la parametricidad, eliminando así las posibilidades de programas incorrectos, que es el objetivo de la tipada estática.


35
2018-02-17 22:45



(Aunque ya escribí una respuesta aquí, revisando esta pregunta dos años más tarde, me doy cuenta de que hay otra forma completamente diferente de responderla, así que dejo intacta la respuesta anterior y agrego esta).


Es altamente discutible si el proceso realizado en Java Generics merece el nombre "borrado de tipo". Como los tipos genéricos no se borran sino que se reemplazan por sus contrapartes en bruto, una mejor opción parece ser "mutilación tipo".

La característica esencial del borrado de tipos en su sentido común es forzar al tiempo de ejecución a mantenerse dentro de los límites del sistema de tipo estático al "cegar" la estructura de los datos a los que accede. Esto le da plena potencia al compilador y le permite probar teoremas basados ​​solo en tipos estáticos. También ayuda al programador restringiendo los grados de libertad del código, dando más poder al razonamiento simple.

El borrado de tipos de Java no lo consigue; daña el compilador, como en este ejemplo:

void doStuff(List<Integer> collection) { 
}

void doStuff(List<String> collection) // ERROR: a method cannot have 
                   // overloads which only differ in type parameters

(Las dos declaraciones anteriores colapsan en la misma firma de método después del borrado).

Por otro lado, el tiempo de ejecución aún puede inspeccionar el tipo de objeto y razonar al respecto, pero dado que su percepción del verdadero tipo está paralizada por el borrado, las infracciones de tipo estático son triviales y difíciles de evitar.

Para complicar aún más las cosas, las firmas de tipo original y borrado coexisten y se consideran en paralelo durante la compilación. Esto se debe a que todo el proceso no se trata de eliminar información de tipo del tiempo de ejecución, sino de utilizar un sistema de tipo genérico en un sistema de tipo sin formato heredado para mantener la compatibilidad con versiones anteriores. Esta joya es un ejemplo clásico:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

(El redundante extends Object tuvo que ser agregado para preservar la compatibilidad con versiones anteriores de la firma borrada).

Ahora, con eso en mente, volvamos a la cita:

Es gracioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java tiene derecho

Qué exactamente Java hizo bien? ¿Es la palabra misma, independientemente de su significado? Por contraste, eche un vistazo a los humildes int tipo: nunca se realiza una comprobación del tipo de tiempo de ejecución, o incluso es posible, y la ejecución es siempre perfectamente segura. Eso es qué tipo de borrado se ve cuando se hace bien: ni siquiera sabes que está allí.


11
2018-06-21 09:26



Una publicación posterior del mismo usuario en la misma conversación:

nuevo T es un programa roto. Es isomorfo a la afirmación de que "todas las proposiciones son verdaderas". No soy grande en esto.

(Esto fue en respuesta a una declaración de otro usuario, a saber, que "parece que en algunas situaciones 'la nueva T' sería mejor", y la idea es que new T() es imposible debido al borrado de tipo. (Esto es discutible, incluso si T estaban disponibles en tiempo de ejecución, podría ser una clase o interfaz abstracta, o podría ser Void, o podría carecer de un constructor no-arg, o su constructor no-arg podría ser privado (por ejemplo, porque se supone que es una clase singleton), o su constructor no-arg podría especificar una excepción marcada que el método genérico no atrapar o especificar, pero esa era la premisa. De todos modos, es verdad que sin borrado podrías al menos escribir T.class.newInstance(), que maneja esos problemas.))

Este punto de vista, que los tipos son isomorfos a las proposiciones, sugiere que el usuario tiene antecedentes en la teoría de tipos formal. (S) es muy probable que no le gusten los "tipos dinámicos" o los "tipos de tiempo de ejecución" y preferiría un Java sin downcasts y instanceof y reflexión, etc. (Piense en un lenguaje como Standard ML, que tiene un sistema de tipos muy rico (estático) y cuya semántica dinámica no depende de ningún tipo de información).

Vale la pena tener en cuenta, por cierto, que el usuario está trolling: mientras que (s) es probable que prefiera sinceramente (estáticamente) los idiomas escritos, (s) que es no sinceramente tratando de persuadir a otros de ese punto de vista. Más bien, el objetivo principal del tuit original era burlarse de los que no estaban de acuerdo, y después de que algunos de los que estaban en desacuerdo intervinieron, el usuario publicó tweets de seguimiento tales como "la razón por la cual java tiene borrado tipo es que Wadler y otros saben qué lo están haciendo, a diferencia de los usuarios de Java ". Desafortunadamente, esto hace que sea difícil saber qué está pensando en realidad; pero, afortunadamente, también significa que no es muy importante hacerlo. Las personas con una profundidad real en sus puntos de vista generalmente no recurren a trolls que son bastante esto sin contenido.


9
2018-01-04 08:29



Lo único que no veo considerado aquí es que los OOP polimorfismo en tiempo de ejecución es fundamentalmente dependiente de la reificación de tipos en tiempo de ejecución. Cuando un lenguaje cuya columna vertebral se mantiene en su lugar por los tipos devueltos introduce una extensión importante a su sistema de tipos y lo basa en el borrado de tipos, la disonancia cognitiva es el resultado inevitable. Esto es precisamente lo que le sucedió a la comunidad Java; es por eso que la borradura de tipo ha atraído tanta controversia y, en última instancia, por qué hay planea deshacerlo en una futura versión de Java. Encontrar algo gracioso en esa queja de los usuarios de Java delata una sincera incomprensión del espíritu de Java o una broma deliberadamente desaprobadora.

El reclamo "borrado es lo único que Java tiene derecho" implica la afirmación de que "todos los lenguajes basados ​​en el despacho dinámico contra el tipo de argumento de función en tiempo de ejecución son fundamentalmente defectuosos". Aunque sin duda es un reclamo legítimo por sí mismo, e incluso puede considerarse como una crítica válida de todos los lenguajes de programación orientada a objetos, incluido Java, no puede presentarse como un punto fundamental desde el cual evaluar y criticar características. dentro del contexto de Java, donde el polimorfismo en tiempo de ejecución es axiomático.

En resumen, aunque se puede afirmar válidamente que "el borrado de tipos es el camino a seguir en el diseño del lenguaje", las posiciones que soportan el borrado de tipos dentro de Java se extravían simplemente porque es mucho, demasiado tarde para eso y ya lo había sido en el momento histórico en que Oak fue adoptado por Sun y renombrado como Java.



En cuanto a si la tipificación estática en sí misma es la dirección correcta en el diseño de lenguajes de programación, esto encaja en un contexto filosófico mucho más amplio de lo que creemos que constituye la actividad de programación. Una escuela de pensamiento, claramente derivada de la tradición clásica de las matemáticas, ve los programas como instancias de un concepto matemático u otro (proposiciones, funciones, etc.), pero hay una clase de enfoques completamente diferente, que ven la programación como una forma de habla con la máquina y explica lo que queremos de ella. En esta visión, el programa es una entidad dinámica, de crecimiento orgánico, un opuesto dramático del edifico cuidadosamente erigido de un programa estáticamente estátizado.

Parecería natural considerar los lenguajes dinámicos como un paso en esa dirección: la consistencia del programa surge de abajo hacia arriba, sin a priori restricciones que lo impondrían de arriba hacia abajo. Este paradigma se puede ver como un paso hacia el modelado del proceso por el cual nosotros, los humanos, nos convertimos en lo que somos a través del desarrollo y el aprendizaje.


8
2018-02-23 11:02



Una cosa buena es que no hubo necesidad de cambiar JVM cuando se introdujeron los genéricos. Java implementa genéricos solo a nivel de compilador.


5
2018-01-04 08:24



La razón por la que el borrado de tipos es bueno es que las cosas que hace imposibles son perjudiciales. La prevención de la inspección de los argumentos de tipo en el tiempo de ejecución facilita la comprensión y el razonamiento sobre los programas.

Una observación que encontré un poco contra-intuitiva es que cuando las firmas de funciones son Más genéricos, se vuelven más fáciles de entender. Esto se debe a que se reduce el número de posibles implementaciones. Considere un método con esta firma, que de alguna manera sabemos que no tiene efectos secundarios:

public List<Integer> XXX(final List<Integer> l);

¿Cuáles son las posibles implementaciones de esta función? Muchisimo. Puede decir muy poco sobre lo que hace esta función. Podría estar revertiendo la lista de entrada. Podría combinar cosas juntas, sumarlas y devolver una lista de la mitad del tamaño. Hay muchas otras posibilidades que podrían imaginarse. Ahora considera:

public <T> List<T> XXX(final List<T> l);

¿Cuántas implementaciones de esta función hay? Dado que la implementación no puede conocer el tipo de elementos, ahora se puede excluir una gran cantidad de implementaciones: los elementos no se pueden combinar, agregar a la lista o filtrar, etc. Estamos limitados a cosas como: identidad (sin cambios en la lista), elementos caídos o inversión de la lista. Esta función es más fácil de razonar solo en función de su firma.

Excepto ... en Java siempre puedes engañar al sistema de tipos. Porque la implementación de ese método genérico puede usar cosas como instanceof verifica y / o lanza a tipos arbitrarios, nuestro razonamiento basado en la firma del tipo puede volverse fácilmente inútil. La función podría inspeccionar el tipo de elementos y hacer cualquier cantidad de cosas en función del resultado. Si se permiten estos hacks de tiempo de ejecución, las firmas de métodos parametrizados se vuelven mucho menos útiles para nosotros.

Si Java no tenía borrado de tipo (es decir, los tipos de argumentos se reificaron en tiempo de ejecución), esto simplemente permitiría Más engaños que rayan en el razonamiento de este tipo. En el ejemplo anterior, la implementación solo puede violar las expectativas establecidas por la firma de tipo si la lista tiene al menos un elemento; pero si Tfue reificado, podría hacerlo incluso si la lista estuviera vacía. Los tipos reificados solo aumentarían las (ya muchas) posibilidades para impedir nuestra comprensión del código.

La borradura de tipo hace que el idioma sea menos "poderoso". Pero algunas formas de "poder" son realmente dañinas.


5
2018-02-17 23:34