¿Por qué es útil la inferencia de tipos?

37

Leo el código con mucha más frecuencia que escribo código, y supongo que la mayoría de los programadores que trabajan en software industrial hacen esto. La ventaja de la inferencia de tipos, supongo, es menos verbosidad y menos código escrito. Pero, por otro lado, si lee el código con más frecuencia, probablemente querrá un código legible.

El compilador infiere el tipo; Hay viejos algoritmos para esto. Pero la verdadera pregunta es ¿por qué yo, el programador, querría inferir el tipo de mis variables cuando leo el código? ¿No es más rápido para alguien leer el tipo que pensar qué tipo hay?

Editar: Como conclusión, entiendo por qué es útil. Pero en la categoría de características del lenguaje, lo veo en un cubo con sobrecarga del operador, útil en algunos casos pero que afecta la legibilidad si se abusa.

m3th0dman
fuente
55
En mi experiencia, los tipos son mucho más importantes al escribir código que al leerlo. Al leer el código, busco algoritmos y bloques específicos a los que normalmente me dirigen variables bien nombradas. Realmente no necesito escribir el código de verificación solo para leer y comprender lo que está haciendo a menos que esté terriblemente mal escrito. Sin embargo, cuando leo código que está lleno de detalles innecesarios adicionales que no estoy buscando (como muchas anotaciones de tipo), a menudo hace que sea más difícil encontrar los bits que estoy buscando. La inferencia de tipos, diría, es una gran ayuda para leer mucho más que escribir código.
Jimmy Hoffa
Una vez que encuentre el fragmento de código que estoy buscando, entonces podría comenzar a verificarlo, pero en cualquier momento no debería centrarse en más de quizás 10 líneas de código, momento en el cual no es complicado hacer el inferenciate a ti mismo porque, en primer lugar, estás separando mentalmente todo el bloque y, de todos modos, es probable que uses herramientas para ayudarte a hacerlo. Averiguar los tipos de las 10 líneas de código en las que intenta ubicarse rara vez toma mucho tiempo, pero esta es la parte en la que ha cambiado de leer a escribir de todos modos, que es mucho menos común de todos modos.
Jimmy Hoffa
Recuerde que a pesar de que un programador lee el código con más frecuencia de lo que lo escribe, no significa que un fragmento de código se lea con más frecuencia que lo escrito. Una gran cantidad de código puede ser de corta duración o, de lo contrario, nunca volver a leerse, y puede que no sea fácil saber qué código sobrevivirá y debería escribirse con la máxima legibilidad.
jpa
2
Desarrollando el primer punto de @JimmyHoffa, considere leer en general. ¿Es más fácil analizar y leer una oración, y mucho menos entenderla, cuando se enfoca en la parte del discurso de sus palabras individuales? "La (artículo) vaca (sustantivo-singular) saltó (verbo-pasado) sobre (preposición) la (artículo) luna (sustantivo). (Período de puntuación)".
Zev Spitz

Respuestas:

46

Echemos un vistazo a Java. Java no puede tener variables con tipos inferidos. Esto significa que con frecuencia tengo que deletrear el tipo, incluso si es perfectamente obvio para un lector humano cuál es el tipo:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

Y a veces es simplemente molesto explicar todo el tipo.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Este tipeo estático detallado se interpone en mi camino, el programador. La mayoría de las anotaciones de tipo son rellenos repetitivos de línea, regurgitaciones sin contenido de lo que ya sabemos. Sin embargo, me gusta la escritura estática, ya que realmente puede ayudar a descubrir errores, por lo que usar la escritura dinámica no siempre es una buena respuesta. La inferencia de tipos es lo mejor de ambos mundos: puedo omitir los tipos irrelevantes, pero aún así estar seguro de que mi programa (type-) se desprotege.

Si bien la inferencia de tipos es realmente útil para las variables locales, no debe usarse para las API públicas que deben documentarse sin ambigüedades. Y a veces los tipos son realmente críticos para comprender lo que está sucediendo en el código. En tales casos, sería una tontería confiar solo en la inferencia de tipos.

Hay muchos idiomas que admiten la inferencia de tipos. Por ejemplo:

  • C ++. La autopalabra clave activa la inferencia de tipos. Sin él, deletrear los tipos para lambdas o para entradas en contenedores sería un infierno.

  • DO#. Puede declarar variables con var, lo que desencadena una forma limitada de inferencia de tipos. Todavía administra la mayoría de los casos en los que desea inferencia de tipos. En ciertos lugares, puede omitir el tipo por completo (por ejemplo, en lambdas).

  • Haskell y cualquier idioma de la familia ML. Si bien el sabor específico de la inferencia de tipos que se usa aquí es bastante poderoso, a menudo se ven anotaciones de tipo para funciones, y por dos razones: la primera es documentación, y la segunda es una verificación de que la inferencia de tipo realmente encontró los tipos que esperaba. Si hay una discrepancia, es probable que haya algún tipo de error.

amon
fuente
13
Tenga en cuenta también que C # tiene tipos anónimos, es decir, tipos sin nombre, pero C # tiene un sistema de tipos nominal, es decir, un sistema de tipos basado en nombres. Sin inferencia de tipos, ¡esos tipos nunca podrían usarse!
Jörg W Mittag
10
Algunos ejemplos son un poco artificiales, en mi opinión. La inicialización a 42 no significa automáticamente que la variable sea an int, podría ser cualquier tipo numérico, incluso par char. Además, no entiendo por qué querrías deletrear todo el tipo para Entrycuando solo puedes escribir el nombre de la clase y dejar que tu IDE haga la importación necesaria. El único caso cuando tiene que deletrear el nombre completo es cuando tiene una clase con el mismo nombre en su propio paquete. Pero me parece un mal diseño de todos modos.
Malcolm
10
@ Malcolm Sí, todos mis ejemplos son artificiales. Sirven para ilustrar un punto. Cuando escribí el intejemplo, estaba pensando en (en mi opinión, un comportamiento bastante sensato) de la mayoría de los idiomas que presentan inferencia de tipos. Por lo general, infieren un into Integercomo se llame en ese idioma. La belleza de la inferencia de tipos es que siempre es opcional; aún puede especificar un tipo diferente si lo necesita. En cuanto al Entryejemplo: buen punto, lo reemplazaré con Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java ni siquiera tiene alias de tipo :(
amon
44
@ m3th0dman Si el tipo es importante para comprender, aún puede mencionarlo explícitamente. La inferencia de tipos es siempre opcional. Pero aquí, el tipo de colKeyes obvio e irrelevante: solo nos importa que sea adecuado como segundo argumento de doSomethingWith. Si (key1, key2, value)tuviera que extraer ese bucle en una función que produce un Iterable de -triples, la firma más general sería <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Dentro de esa función, el tipo real de colKey( Integer, no K2) es absolutamente irrelevante.
amon
44
@ m3th0dman es una declaración general bastante amplia, sobre "la mayoría " del código es esto o aquello. Estadísticas anecdóticas. Ciertamente hay ningún punto en la escritura del tipo dos veces en inicializadores: View.OnClickListener listener = new View.OnClickListener(). Todavía sabría el tipo incluso si el programador era "vago" y lo acortaba var listener = new View.OnClickListener(si esto fuera posible). Este tipo de redundancia es común - No voy a arriesgar un cálculo aproximado aquí - y retirarlo no se derivan de pensar en futuros lectores. Cada característica del lenguaje debe usarse con cuidado, no estoy cuestionando eso.
Konrad Morawski
26

Es cierto que el código se lee con mucha más frecuencia de lo que se escribe. Sin embargo, la lectura también lleva tiempo, y dos pantallas de código son más difíciles de navegar y leer que una pantalla de código, por lo que debemos priorizar para empacar la mejor relación de información útil / esfuerzo de lectura. Este es un principio general de UX: demasiada información a la vez abruma y en realidad degrada la efectividad de la interfaz.

Y es mi experiencia que a menudo , el tipo exacto no es (tan) importante. Seguramente usted a veces anidar expresiones: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Cada uno de estos contiene subexpresiones que evalúan un valor cuyo tipo no está escrito. Sin embargo, son perfectamente claros. Estamos de acuerdo en dejar el tipo sin declarar porque es bastante fácil de entender del contexto, y escribirlo haría más daño que bien (desordenar la comprensión del flujo de datos, ocupar una valiosa pantalla y espacio de memoria a corto plazo).

Una razón para no anidar expresiones como si no hubiera un mañana es que las líneas se alargan y el flujo de valores se vuelve poco claro. La introducción de una variable temporal ayuda con esto, impone un orden y le da un nombre a un resultado parcial. Sin embargo, no todo lo que se beneficia de estos aspectos también se beneficia de tener su tipo enunciado:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

¿Importa si useres un objeto de entidad, un entero, una cadena u otra cosa? Para la mayoría de los propósitos, no es suficiente, es suficiente saber que representa a un usuario, proviene de la solicitud HTTP y se utiliza para buscar el nombre que se mostrará en la esquina inferior derecha de la respuesta.

Y cuando se hace materia, el autor es libre de escribir el tipo. Esta es una libertad que debe usarse de manera responsable, pero lo mismo es cierto para todo lo demás que puede mejorar la legibilidad (nombres de variables y funciones, formato, diseño de API, espacio en blanco). Y, de hecho, la convención en Haskell y ML (donde todo se puede inferir sin esfuerzo adicional) es escribir los tipos de funciones no locales, y también de las variables y funciones locales cuando sea apropiado. Solo los novatos permiten inferir cada tipo.


fuente
2
+1 Esta debería ser la respuesta aceptada. Va directo al corazón de por qué la inferencia de tipos es una gran idea.
Christian Hayter
El tipo exacto de userimporta si está intentando extender la función, porque determina lo que puede hacer con el user. Esto es importante si desea agregar algún control de cordura (debido a una vulnerabilidad de seguridad, por ejemplo), u olvidó que realmente necesita hacer algo con el usuario además de solo mostrarlo. Es cierto que estos tipos de lectura para expansión son menos frecuentes que solo leer el código, pero también son una parte vital de nuestro trabajo.
cmaster
@cmaster Y siempre puede encontrar ese tipo con bastante facilidad (la mayoría de los IDE le dirán, y existe la solución de baja tecnología de causar intencionalmente un error de tipo y dejar que el compilador imprima el tipo real), está fuera del camino por lo que no te molesta en el caso común.
4

Creo que la inferencia de tipos es bastante importante y debería admitirse en cualquier idioma moderno. Todos nos desarrollamos en IDE y podrían ayudarnos mucho en caso de que desee conocer el tipo inferido, solo algunos de nosotros pirateamos vi. Piense en el código de verbosidad y ceremonia en Java, por ejemplo.

  Map<String,HashMap<String,String>> map = getMap();

Pero puedes decir que está bien, mi IDE me ayudará, podría ser un punto válido. Sin embargo, algunas características no estarían allí sin la ayuda de la inferencia de tipos, por ejemplo, tipos anónimos de C #.

 var person = new {Name="John Smith", Age = 105};

Linq no sería tan bueno como lo es ahora sin la ayuda de la inferencia de tipos, Selectpor ejemplo

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Este tipo anónimo se deducirá claramente a la variable.

No me gusta la inferencia de tipos en los tipos de retorno Scalaporque creo que su punto se aplica aquí, debería estar claro para nosotros qué devuelve una función para que podamos usar la API con más fluidez

Sleiman Jneidi
fuente
Map<String,HashMap<String,String>>? Claro, si no está usando tipos, deletrearlos tiene poco beneficio. Table<User, File, String>es más informativo y hay un beneficio en escribirlo.
MikeFHay
4

Creo que la respuesta a esto es realmente simple: ahorra leer y escribir información redundante. Particularmente en lenguajes orientados a objetos donde tiene un tipo en ambos lados del signo igual.

Lo que también le dice cuándo debe o no usarlo, cuando la información no es redundante.

jmoreno
fuente
3
Bueno, técnicamente, la información siempre es redundante cuando es posible omitir firmas manuales: de lo contrario, el compilador no podría inferirlas. Pero entiendo lo que quieres decir: cuando duplicas una firma en múltiples puntos en una sola vista, es realmente redundante para el cerebro , mientras que algunos tipos bien ubicados brindan información que tendrías que buscar mucho tiempo, posiblemente con un buenas muchas transformaciones no obvias.
Leftaroundabout
@leftaroundabout: redundante cuando lo lee el programador.
jmoreno
3

Supongamos que uno ve el código:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Si someBigLongGenericTypees asignable por el tipo de retorno de someFactoryMethod, ¿qué tan probable sería que alguien que lea el código se dé cuenta si los tipos no coinciden con precisión, y con qué facilidad podría alguien que notó la discrepancia reconocer si fue intencional o no?

Al permitir la inferencia, un lenguaje puede sugerir a alguien que está leyendo el código que cuando se establece explícitamente el tipo de una variable, la persona debe tratar de encontrar una razón para ello. Esto a su vez permite a las personas que leen código enfocar mejor sus esfuerzos. Si, por el contrario, la gran mayoría de las veces cuando se especifica un tipo, resulta ser exactamente el mismo que se habría inferido, entonces alguien que está leyendo el código puede ser menos propenso a notar las veces que es sutilmente diferente .

Super gato
fuente
2

Veo que ya hay una serie de buenas respuestas. Algunas de las cuales repetiré, pero a veces solo quieres poner las cosas en tus propias palabras. Comentaré con algunos ejemplos de C ++ porque ese es el lenguaje con el que estoy más familiarizado.

Lo que es necesario nunca es imprudente. La inferencia de tipos es necesaria para hacer prácticas otras características del lenguaje. En C ++ es posible tener tipos indescriptibles.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 agregó lambdas que también son indescriptibles.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

La inferencia de tipos también sustenta las plantillas.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Pero sus preguntas fueron "¿por qué yo, el programador, querría inferir el tipo de mis variables cuando leo el código? ¿No es más rápido para alguien leer el tipo que pensar qué tipo hay?"

La inferencia de tipos elimina la redundancia. Cuando se trata de leer el código, a veces puede ser más rápido y fácil tener información redundante en el código, pero la redundancia puede eclipsar la información útil . Por ejemplo:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

No hace falta mucha familiaridad con la biblioteca estándar para que un programador de C ++ identifique que soy un iterador, por i = v.begin()lo que la declaración de tipo explícita es de valor limitado. Por su presencia, oculta detalles que son más importantes (como los que iapuntan al comienzo del vector). La buena respuesta de @amon proporciona un ejemplo aún mejor de verbosidad que eclipsa detalles importantes. En contraste, el uso de la inferencia de tipos le da mayor importancia a los detalles importantes.

std::vector<int> v;
auto i = v.begin();

Si bien la lectura del código es importante, no es suficiente, en algún momento tendrá que dejar de leer y comenzar a escribir código nuevo. La redundancia en el código hace que la modificación del código sea más lenta y difícil. Por ejemplo, supongamos que tengo el siguiente fragmento de código:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

En el caso de que necesite cambiar el tipo de valor del vector para cambiar el código dos veces a:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

En este caso, tengo que modificar el código en dos lugares. Contraste con la inferencia de tipos donde el código original es:

std::vector<int> v;
auto i = v.begin();

Y el código modificado:

std::vector<double> v;
auto i = v.begin();

Tenga en cuenta que ahora solo tengo que cambiar una línea de código. Extrapolar esto a un programa grande y la inferencia de tipos puede propagar los cambios a los tipos mucho más rápido que con un editor.

La redundancia en el código crea la posibilidad de errores. Cada vez que su código depende de que dos datos se mantengan equivalentes, existe la posibilidad de error. Por ejemplo, hay una inconsistencia entre los dos tipos en esta declaración que probablemente no se pretende:

int pi = 3.14159;

La redundancia hace que la intención sea más difícil de discernir. En algunos casos, la inferencia de tipos puede ser más fácil de leer y comprender porque es más simple que la especificación de tipo explícita. Considere el fragmento de código:

int y = sq(x);

En el caso de que sq(x)devuelva un int, no es obvio si yes un intporque es el tipo de retorno de sq(x)o porque se ajusta a las declaraciones que utilizan y. Si cambio otro código de modo que sq(x)ya no regrese int, es incierto de esa línea solo si el tipo de ydebe actualizarse. Contraste con el mismo código pero usando inferencia de tipos:

auto y = sq(x);

En esto, la intención es clara, ydebe ser del mismo tipo que devuelto por sq(x). Cuando el código cambia el tipo de retorno de sq(x), el tipo de ycambios para que coincida automáticamente.

En C ++ hay una segunda razón por la cual el ejemplo anterior es más simple con la inferencia de tipos, la inferencia de tipos no puede introducir la conversión de tipos implícita. Si el tipo de retorno de sq(x)no es int, el compilador con silenciosamente inserta una conversión implícita a int. Si el tipo de retorno de sq(x)es un tipo complejo de tipo que define operator int(), esta llamada de función oculta puede ser arbitrariamente compleja.

Bowie Owens
fuente
Un buen punto sobre los tipos indecibles en C ++. Sin embargo, creo que es menos una razón para agregar inferencia de tipos, que una razón para arreglar el idioma. En el primer caso que presente, el programador solo tendría que darle un nombre a la cosa para evitar el uso de inferencia de tipos, por lo que este no es un buen ejemplo. El segundo ejemplo solo es sólido porque C ++ prohíbe explícitamente que los tipos lambda sean pronunciables, incluso la definición de tipos con el uso typeofse vuelve inútil por el lenguaje. Y ese es un déficit del lenguaje en sí mismo que debería solucionarse en mi opinión.
cmaster