Tipo de borrado es bueno
Vamos a ceñirnos a los hechos
Muchas de las respuestas hasta ahora están demasiado relacionadas con el usuario de Twitter. Es útil concentrarse en los mensajes y no en el mensajero. Hay un mensaje bastante consistente incluso con solo los extractos mencionados hasta ahora:
Es gracioso cuando los usuarios de Java se quejan del borrado de tipos, que es lo único que Java hizo bien, ignorando todas las cosas que hizo mal.
Obtengo enormes beneficios (por ejemplo, parametricidad) y costo nulo (el supuesto costo es un límite de imaginación).
new T es un programa roto. Es isomórfico a la afirmación "todas las proposiciones son verdaderas". No me gusta mucho esto.
Un objetivo: programas razonables
Estos tweets reflejan una perspectiva que no está interesada en si podemos hacer que la máquina haga algo , sino más bien en si podemos razonar que la máquina hará algo que realmente queremos. El buen razonamiento es una prueba. Las pruebas se pueden especificar 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 soluciones como pruebas automatizadas 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 está sugiriendo que hay una manera mucho mejor.
Entonces, nuestro objetivo es tener programas correctos sobre los que podamos razonar de manera clara y rigurosa de una manera que se corresponda con la forma en que la máquina ejecutará el programa. Este, sin embargo, no es el único objetivo. También queremos que nuestra lógica tenga cierto grado de expresividad. Por ejemplo, hay mucho que podemos expresar con lógica proposicional. Es bueno tener una cuantificación universal (∀) y existencial (∃) a partir de algo como la lógica de primer orden.
Usar sistemas de tipos para razonar
Estos objetivos se pueden abordar muy bien mediante sistemas de tipos. Esto es especialmente claro debido a la correspondencia Curry-Howard . Esta correspondencia se expresa a menudo con la siguiente analogía: los tipos son para los programas como los teoremas para las demostraciones.
Esta correspondencia es algo profunda. Podemos tomar expresiones lógicas y traducirlas a través de la correspondencia a tipos. Entonces, si tenemos un programa con el mismo tipo de firma que compila, hemos probado que la expresión lógica es universalmente verdadera (una tautología). Esto se debe a que la correspondencia es bidireccional. La transformación entre el tipo / programa y los mundos de teorema / prueba es mecánica y, en muchos casos, puede automatizarse.
Curry-Howard juega muy bien con lo que nos gustaría hacer con las especificaciones de un programa.
¿Son útiles los sistemas de tipos en Java?
Incluso con una comprensión de Curry-Howard, a algunas personas les resulta fácil descartar el valor de un sistema de tipos, cuando
- es extremadamente difícil trabajar con
- corresponde (a través de Curry-Howard) a una lógica con expresividad limitada
- está roto (lo que lleva a la caracterización de sistemas como "débiles" o "fuertes").
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).
Con respecto al segundo punto, Java pasa a corresponder casi a una lógica de primer orden. Los genéricos dan uso al sistema de tipos 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 bueno poder decir que las funciones List<A>
funcionan universalmente para todas las listas posibles porque A no tiene restricciones. Esto lleva a lo que el usuario de Twitter está hablando con respecto a la "parametricidad".
¡Un artículo que se cita con frecuencia sobre la parametricidad es Philip Wadler's Theorems gratis! . Lo interesante de este artículo es que solo con la firma de tipos, podemos probar algunas invariantes muy interesantes. Si tuviéramos que escribir pruebas automatizadas para estos invariantes, estaríamos perdiendo mucho el tiempo. Por ejemplo, para List<A>
, desde la firma de tipo solo paraflatten
<A> List<A> flatten(List<List<A>> nestedLists);
podemos razonar que
flatten(nestedList.map(l -> l.map(any_function)))
≡ flatten(nestList).map(any_function)
Ese es un ejemplo simple, y probablemente pueda razonar al respecto de manera informal, pero es aún mejor cuando obtenemos tales pruebas de forma formal y gratuita del sistema de tipos y el compilador las verifica.
No borrar puede dar lugar a abusos
Desde la perspectiva de la implementación del lenguaje, los genéricos de Java (que corresponden a tipos universales) influyen mucho 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 sólido implementado sin defectos. Java definitivamente tiene algunas características de lenguaje que nos permiten romper nuestro razonamiento. Estos incluyen pero no se limitan a:
- efectos secundarios con un sistema externo
- reflexión
Los genéricos no borrados están relacionados de muchas formas con la reflexión. Sin borrado, hay información de tiempo de ejecución que se transporta con la implementación que podemos usar para diseñar nuestros algoritmos. Lo que esto significa es que, estadísticamente, cuando razonamos sobre los programas, no tenemos la imagen completa. La reflexión amenaza gravemente la corrección de las pruebas sobre las que razonamos estáticamente. No es una coincidencia que la reflexión también conduzca a una variedad de defectos delicados.
Entonces, ¿de qué maneras los genéricos no borrados podrían ser "útiles"? Consideremos el uso mencionado en el tweet:
<T> T broken { return new T(); }
¿Qué sucede si T no tiene un constructor sin argumentos? En algunos idiomas, lo que obtienes es nulo. O tal vez omita el valor nulo y vaya directamente a generar una excepción (a la que los valores nulos parecen conducir de todos modos). Debido a que nuestro lenguaje es Turing completo, es imposible razonar sobre qué llamadas a broken
involucrarán tipos "seguros" con constructores sin argumentos y cuáles no. Hemos perdido la certeza de que nuestro programa funciona universalmente.
Borrar significa que hemos razonado (así que borremos)
Entonces, si queremos razonar sobre nuestros programas, se recomienda encarecidamente que no empleemos características del lenguaje que amenacen fuertemente nuestro razonamiento. Una vez que hacemos eso, ¿por qué no descartar los tipos en tiempo de ejecución? No son necesarios. Podemos obtener algo de eficiencia y simplicidad con la satisfacción de que no fallarán los moldes o de que pueden faltar métodos en la invocación.
Borrar fomenta el razonamiento.