¿Cuál es la compensación para la inferencia de tipos?

29

Parece que todos los nuevos lenguajes de programación o al menos los que se hicieron populares usan la inferencia de tipos. Incluso Javascript obtuvo tipos e inferencia de tipos a través de diversas implementaciones (Acscript, mecanografiado, etc.). Me parece genial, pero me pregunto si hay compensaciones o por qué digamos que Java o los viejos buenos idiomas no tienen inferencia de tipos

  • Al declarar una variable en Go sin especificar su tipo (usando var sin un tipo o la sintaxis: =), el tipo de la variable se deduce del valor en el lado derecho.
  • D permite escribir fragmentos de código grandes sin especificar tipos de forma redundante, como hacen los lenguajes dinámicos. Por otro lado, la inferencia estática deduce tipos y otras propiedades de código, dando lo mejor de los mundos estático y dinámico.
  • El motor de inferencia de tipos en Rust es bastante inteligente. Hace más que mirar el tipo del valor r durante una inicialización. También se ve cómo se usa la variable después para inferir su tipo.
  • Swift usa la inferencia de tipos para calcular el tipo apropiado. La inferencia de tipos permite que un compilador deduzca el tipo de una expresión particular automáticamente cuando compila su código, simplemente examinando los valores que proporciona.
El usuario sin sombrero
fuente
3
En C #, las pautas generales dicen que no siempre se usa varporque a veces puede dañar la legibilidad.
Mephy
55
"... o por qué digamos que Java o los viejos buenos idiomas no tienen inferencia de tipos" Las razones son probablemente históricas; ML apareció 1 año después de C según Wikipedia y tenía inferencia de tipos. Java intentaba atraer a los desarrolladores de C ++ . C ++ comenzó como una extensión de C, y las principales preocupaciones de C eran ser un contenedor portátil sobre el ensamblaje y ser fácil de compilar. Para lo que sea que valga la pena, he leído que el subtipo hace que la inferencia de tipos sea indecidible también en el caso general.
Doval
3
@Doval Scala parece hacer un trabajo bastante bueno al inferir tipos, para un lenguaje que admite la herencia de subtipos. No es tan bueno como cualquiera de los idiomas de la familia ML, pero probablemente sea tan bueno como podría pedir, dado el diseño del idioma.
KChaloux
12
Vale la pena establecer una distinción entre la deducción de tipo (monodireccional, como C # vary C ++ auto) y la inferencia de tipo (bidireccional, como Haskell let). En el primer caso, el tipo de un nombre puede inferirse únicamente de su inicializador; sus usos deben seguir el tipo del nombre. En el último caso, el tipo de un nombre también puede inferirse de sus usos, lo cual es útil porque puede escribir simplemente []para una secuencia vacía, independientemente del tipo de elemento, o newEmptyMVarpara una nueva referencia mutable nula, independientemente del referente tipo.
Jon Purdy
La inferencia de tipos puede ser increíblemente complicada de implementar, especialmente de manera eficiente. La gente no quiere que su complejidad de compilación sufra una explosión exponencial de expresiones más complejas. Lectura
Alexander - Restablece a Monica el

Respuestas:

43

El sistema de tipos de Haskell es completamente inferible (dejando de lado la recursividad polimórfica, ciertas extensiones de lenguaje y la temida restricción del monomorfismo ), sin embargo, los programadores con frecuencia proporcionan anotaciones de tipo en el código fuente, incluso cuando no es necesario. ¿Por qué?

  1. Las anotaciones de tipo sirven como documentación . Esto es especialmente cierto con tipos tan expresivos como los de Haskell. Dado el nombre de una función y su tipo, generalmente puede adivinar bastante bien lo que hace la función, especialmente cuando la función es paramétricamente polimórfica.
  2. Las anotaciones de tipo pueden impulsar el desarrollo . Escribir una firma tipo antes de escribir el cuerpo de una función se siente como un desarrollo basado en pruebas. En mi experiencia, una vez que compila una función de Haskell, generalmente funciona la primera vez. (¡Por supuesto, esto no elimina la necesidad de pruebas automáticas!)
  3. Los tipos explícitos pueden ayudarlo a verificar sus suposiciones . Cuando intento entender algún código que ya funciona, con frecuencia lo agrego con lo que creo que son las anotaciones de tipo correctas. Si el código aún se compila, sé que lo he entendido. Si no es así, leí el mensaje de error.
  4. Las firmas tipográficas le permiten especializar funciones polimórficas . Muy ocasionalmente, una API es más expresiva o útil si ciertas funciones no son polimórficas. El compilador no se quejará si le da a una función un tipo menos general del que se deduciría. El ejemplo clásico es map :: (a -> b) -> [a] -> [b]. Su forma más general ( fmap :: Functor f => (a -> b) -> f a -> f b) se aplica a todos los Functors, no solo a las listas. Pero se consideró que mapsería más fácil de entender para los principiantes, por lo que sigue viviendo junto a su hermano mayor.

En general, los inconvenientes de un sistema estáticamente tipado pero inferible son muy parecidos a los inconvenientes del tipeo estático en general, una discusión muy usada en este sitio y otros (buscar en Google "desventajas de tipeo estático" le dará cientos de páginas de guerras de llamas). Por supuesto, algunas de dichas desventajas se mejoran por la menor cantidad de anotaciones de tipo en un sistema inferible. Además, la inferencia de tipos tiene sus propias ventajas: el desarrollo impulsado por agujeros no sería posible sin la inferencia de tipos.

Java * demuestra que un lenguaje que requiere demasiadas anotaciones de tipo se vuelve molesto, pero con muy poco pierde las ventajas que describí anteriormente. Los idiomas con inferencia de tipo de exclusión voluntaria logran un equilibrio agradable entre los dos extremos.

* Incluso Java, ese gran chivo expiatorio, realiza una cierta cantidad de inferencia de tipo local . En una declaración como Map<String, Integer> = new HashMap<>();, no tiene que especificar el tipo genérico del constructor. Por otro lado, los lenguajes de estilo ML son típicamente inferibles globalmente .

Benjamin Hodgson
fuente
2
Pero no eres un principiante;) Debes tener una comprensión de las clases y tipos antes de que realmente "entiendas" Functor. Listas y mapes más probable que sean familiares para los Haskellers no experimentados.
Benjamin Hodgson
1
¿Consideraría que C # es varun ejemplo de inferencia de tipos?
Benjamin Hodgson
1
Sí, C # vares un buen ejemplo.
Doval
1
El sitio ya está marcando esta discusión como demasiado larga, por lo que esta será mi última respuesta. En el primero, el compilador debe determinar el tipo de x; en este último no hay ningún tipo para determinar, todos los tipos son conocidos y solo hay que verificar que la expresión tenga sentido. La diferencia se vuelve más importante a medida que pasa ejemplos triviales y xse usa en varios lugares; el compilador tiene que verificar las ubicaciones donde xse usa para determinar 1) ¿es posible asignar xun tipo tal que el código escriba check? 2) Si es así, ¿cuál es el tipo más general que podemos asignarle?
Doval
1
Tenga en cuenta que la new HashMap<>();sintaxis solo se agregó en Java 7, y las lambdas en Java 8 permiten una gran cantidad de inferencia de tipo "real".
Michael Borgwardt
8

En C #, la inferencia de tipos se produce en tiempo de compilación, por lo que el costo de tiempo de ejecución es cero.

Como cuestión de estilo, varse utiliza para situaciones en las que es inconveniente o innecesario especificar manualmente el tipo. Linq es una de esas situaciones. Otro es:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

sin el cual estaría repitiendo su nombre de tipo realmente largo (y los parámetros de tipo) en lugar de simplemente decirlo var.

Usar el nombre real del tipo cuando sea explícito mejora la claridad del código.

Hay algunas situaciones en las que no se puede utilizar la inferencia de tipos, como las declaraciones de variables miembro cuyos valores se establecen en el momento de la construcción, o donde realmente desea que intellisense funcione correctamente (el IDE de Hackerrank no intelisense los miembros de la variable a menos que declare el tipo explícitamente) .

Robert Harvey
fuente
16
"En C #, la inferencia de tipos ocurre en tiempo de compilación, por lo que el costo de tiempo de ejecución es cero". La inferencia de tipos ocurre en tiempo de compilación por definición, por lo que ese es el caso en todos los idiomas.
Doval
Mucho mejor.
Robert Harvey
1
@Doval es posible realizar inferencia de tipos durante la compilación JIT, que claramente tiene un costo de tiempo de ejecución.
Jules
66
@Jules Si ha llegado tan lejos como para ejecutar el programa, entonces ya se ha verificado su tipo; No queda nada más que inferir. Lo que estás hablando no suele llamarse inferencia de tipos, aunque no estoy seguro de cuál es el término adecuado.
Doval
7

¡Buena pregunta!

  1. Dado que el tipo no está anotado explícitamente, a veces puede hacer que el código sea más difícil de leer, lo que genera más errores. Utilizado correctamente, por supuesto, hace que el código sea más limpio y más legible. Si usted es pesimista y cree que la mayoría de los programadores son malos (o trabaja donde la mayoría de los programadores son malos), esto será una pérdida neta.
  2. Si bien el algoritmo de inferencia de tipos es relativamente simple, no es gratuito . Este tipo de cosas aumenta ligeramente el tiempo de compilación.
  3. Dado que el tipo no está anotado explícitamente, su IDE no puede adivinar lo que está tratando de hacer, perjudicando el autocompletado y ayudantes similares durante el proceso de declaración.
  4. Combinado con sobrecargas de funciones, ocasionalmente puede entrar en situaciones en las que el algoritmo de inferencia de tipos no puede decidir qué camino tomar, lo que lleva a anotaciones de estilo de lanzamiento más feas. (Esto sucede bastante con la sintaxis de función anónima de C #, por ejemplo).

Y hay más lenguajes esotéricos que no pueden hacer su rareza sin una anotación explícita de tipo. Hasta ahora, no hay ninguno que yo sepa que sea lo suficientemente común / popular / prometedor como para mencionar, excepto de pasada.

Telastyn
fuente
1
autocompletar no le importa una mierda la inferencia de tipos. No importa cómo se decidió el tipo, solo lo que tiene el tipo. Y los problemas con las lambdas de C # no tienen nada que ver con la inferencia, se deben a que las lambdas son polimórficas pero el sistema de tipos no puede manejarlo, lo que no tiene nada que ver con la inferencia.
DeadMG
2
@deadmg: claro, una vez que se declara la variable no tiene ningún problema, pero mientras escribe la declaración de la variable, no puede limitar las opciones del lado derecho al tipo declarado ya que no hay ningún tipo declarado.
Telastyn
@deadmg: en cuanto a las lambdas, no tengo muy claro lo que quieres decir, pero estoy bastante seguro de que no es del todo correcto según mi comprensión de cómo funcionan las cosas. Incluso algo tan simple como var foo = x => x;falla porque el lenguaje necesita inferir xaquí y no tiene nada para continuar. Cuando se construyen las lambdas, se construyen como delegados tipados explícitamente Func<int, int> foo = x => xen CIL como Func<int, int> foo = new Func<int, int>(x=>x);donde se construye la lambda como una función generada, tipada explícitamente. Para mí, inferir el tipo de xes parte integral de la inferencia de tipo ...
Telastyn 03 de
3
@Telastyn No es cierto que el lenguaje no tenga nada para continuar. Toda la información necesaria para escribir la expresión x => xestá contenida dentro de la propia lambda; no hace referencia a ninguna variable del ámbito circundante. Cualquier lenguaje funcional sano juicio inferir correctamente que su tipo es "para todos los tipos a, a -> a". Creo que lo que DeadMG está tratando de decir es que el sistema de tipos de C # carece de la propiedad de tipo principal, lo que significa que siempre es posible descubrir el tipo más general para cualquier expresión. Es una propiedad muy fácil de destruir.
Doval
@Doval - enh ... no hay tal cosa como objetos de función genéricos en C #, lo que creo que es sutilmente diferente de lo que ambos están hablando, incluso si conlleva el mismo problema.
Telastyn
4

Me parece genial, pero me pregunto si hay compensaciones o por qué digamos que Java o los viejos buenos idiomas no tienen inferencia de tipos

Java resulta ser la excepción en lugar de la regla aquí. Incluso C ++ (que creo que califica como un "buen lenguaje antiguo" :)) admite la inferencia de tipos con la autopalabra clave desde el estándar C ++ 11. No solo funciona con declaración de variables, sino también como tipo de retorno de función, lo que es especialmente útil con algunas funciones de plantilla complejas.

La escritura implícita y la inferencia de tipos tienen muchos casos de uso buenos, y también hay algunos casos de uso en los que realmente no debería hacerlo. Esto es a veces cuestión de gustos y también tema de debate.

Pero que haya indudablemente buenos casos de uso, es en sí mismo una razón legítima para que cualquier lenguaje lo implemente. Tampoco es una característica difícil de implementar, sin penalización de tiempo de ejecución y no afecta significativamente el tiempo de compilación.

No veo un inconveniente real en darle una oportunidad al desarrollador para usar la inferencia de tipos.

Algunos responden que a veces la tipificación explícita es buena, y ciertamente tienen razón. Pero no admitir la tipificación implícita significaría que un lenguaje impone la tipificación explícita todo el tiempo.

Entonces, el verdadero inconveniente es cuando un lenguaje no admite la tipificación implícita, porque con esto se afirma que no hay una razón legítima para que el desarrollador lo use, lo cual no es cierto.

Gábor Angyal
fuente
1

La distinción principal entre un sistema de inferencia de tipo Hindley-Milner y una inferencia de tipo estilo Go es la dirección del flujo de información. En HM, la información de tipo fluye hacia adelante y hacia atrás a través de la unificación; en Ir, la información de tipo fluye solo hacia delante: solo calcula las sustituciones hacia adelante.

La inferencia de tipo HM es una espléndida innovación que funciona bien con lenguajes funcionales con tipos polimórficos, pero los autores de Go probablemente argumentarán que intenta hacer demasiado:

  1. El hecho de que la información fluya hacia adelante y hacia atrás significa que la inferencia de tipo HM es un asunto muy no local; en ausencia de anotaciones de tipo, cada línea de código en su programa podría estar contribuyendo a escribir una sola línea de código. Si solo tiene sustitución hacia adelante, sabe que la fuente de un error de tipo debe estar en el código que precede a su error; No es el código que viene después.

  2. Con la inferencia de tipo HM, tiende a pensar en restricciones: cuando usa un tipo, restringe los tipos posibles que puede tener. Al final del día, puede haber algunas variables de tipo que se dejan totalmente sin restricciones. La idea de la inferencia de tipos HM es que esos tipos realmente no importan, por lo que se convierten en variables polimórficas. Sin embargo, este polimorfismo adicional puede ser indeseable por varias razones. Primero, como algunas personas han señalado, este polimorfismo adicional podría ser indeseable: HM concluye un tipo polimórfico falso para algún código falso y conduce a errores extraños más tarde. En segundo lugar, cuando un tipo se deja polimórfico, esto puede tener consecuencias para el comportamiento en tiempo de ejecución. Por ejemplo, un resultado intermedio demasiado polimórfico es la razón por la cual 'mostrar. leer 'se considera ambiguo en Haskell; como otro ejemplo,

Edward Z. Yang
fuente
2
Desafortunadamente, esto parece una buena respuesta a una pregunta diferente. OP pregunta acerca de la "inferencia de tipo Go-style" versus los idiomas que no tienen ninguna inferencia de tipo, no "Go-style" versus HM.
Ixrec
-2

Duele la legibilidad.

Comparar

var stuff = someComplexFunction()

vs

List<BusinessObject> stuff = someComplexFunction()

Es especialmente un problema al leer fuera de un IDE como en github.

miguel
fuente
44
Esto no parece ofrecer nada sustancial sobre los puntos hechos y explicados en 6 respuestas anteriores
mosquito
Si usa un nombre propio en lugar de "complejo ilegible", varpuede usarse sin perjudicar la legibilidad. var objects = GetBusinessObjects();
Fabio
Ok, pero luego estás filtrando el sistema de tipos en los nombres de las variables. Es como la notación húngara. Después de las refactorizaciones, es más probable que termines con nombres que no coinciden con el código. En general, el estilo funcional lo empuja a perder las ventajas naturales del espacio de nombres que proporcionan las clases. Entonces terminas con más squareArea () en lugar de square.area ().
miguel