¿Cómo es realmente útil la escritura estática en proyectos más grandes?

9

Mientras curioseaba en la página principal del sitio de un lenguaje de programación de scripts, encontré este pasaje:

Cuando un sistema se vuelve demasiado grande para tenerlo en la cabeza, puede agregar tipos estáticos.

Esto me hizo recordar que en muchas guerras de religión entre lenguajes compilados estáticos (como Java) y lenguajes dinámicos e interpretados (principalmente Python porque es más utilizado, pero es un "problema" compartido entre la mayoría de los lenguajes de secuencias de comandos), una de las quejas de estática Los fanáticos de los idiomas escritos en vez de los idiomas escritos dinámicamente es que no se adaptan bien a proyectos más grandes porque "un día, olvidarás el tipo de retorno de una función y tendrás que buscarlo, mientras que con los idiomas estáticamente escritos todo se declara explícitamente ".

Nunca entendí declaraciones como esta. Para ser honesto, incluso si declara el tipo de retorno de una función, puede y lo olvidará después de haber escrito muchas líneas de código, y aún tendrá que volver a la línea en la que se declara utilizando la función de búsqueda de su editor de texto para verificarlo.

Además, a medida que se declaran las funciones con type funcname()..., sin saber typeque tendrá que buscar sobre cada línea en la que se llama la función, porque solo sabe funcname, mientras que en Python y similares simplemente puede buscar def funcnameo function funcnameque solo sucede una vez, en la declaracion.

Más aún, con REPLs es trivial probar una función para su tipo de retorno con diferentes entradas, mientras que con lenguajes tipados estáticamente necesitaría agregar algunas líneas de código y recompilar todo solo para saber el tipo declarado.

Entonces, aparte de conocer el tipo de retorno de una función que claramente no es un punto fuerte de lenguajes tipados estáticamente, ¿cómo es realmente útil el tipeo estático en proyectos más grandes?

usuario6245072
fuente
2
si lees las respuestas a la otra pregunta, probablemente obtendrás las respuestas que necesitas para esta, básicamente te están preguntando lo mismo desde diferentes perspectivas :)
Sara
1
Swift y parques infantiles son un REPL de un lenguaje estáticamente escrito.
daven11
2
Los idiomas no se compilan, las implementaciones sí. La forma de escribir un REPL para un lenguaje "compilado" es escribir algo que pueda interpretar el lenguaje, o al menos compilarlo y ejecutarlo línea por línea, manteniendo el estado necesario. Además, Java 9 se enviará con un REPL.
Sebastian Redl
2
@ user6245072: Aquí se explica cómo hacer un REPL para un intérprete: leer el código, enviarlo al intérprete e imprimir el resultado. Aquí le mostramos cómo hacer un REPL para un compilador: lea el código, envíelo al compilador, ejecute el código compilado , imprima el resultado. Muy fácil. Eso es exactamente lo que hacen FSi (FPL REPL), GHCi (GHC Haskell's REPL), Scala REPL y Cling.
Jörg W Mittag

Respuestas:

21

Más aún, con REPLs es trivial probar una función para su tipo de retorno con diferentes entradas

No es trivial No es trivial en absoluto . Es trivial hacer esto para funciones triviales.

Por ejemplo, podría definir trivialmente una función en la que el tipo de retorno depende completamente del tipo de entrada.

getAnswer(v) {
 return v.answer
}

En este caso, getAnswerrealmente no tiene un solo tipo de retorno. No hay ninguna prueba que pueda escribir que llame a esto con una entrada de muestra para saber cuál es el tipo de retorno. Será siempre dependerá del argumento actual. En tiempo de ejecución.

Y esto ni siquiera incluye funciones que, por ejemplo, realicen búsquedas de bases de datos. O hacer cosas basadas en la entrada del usuario. O busque variables globales, que por supuesto son de tipo dinámico. O cambie su tipo de retorno en casos aleatorios. Sin mencionar la necesidad de probar cada función individual manualmente cada vez.

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

Básicamente, probar el tipo de retorno de la función en el caso general es literalmente matemáticamente imposible (Problema de detención). La única forma de garantizar el tipo de retorno es restringir la entrada para que responder a esta pregunta no caiga dentro del dominio del Problema de detención al rechazar programas que no son demostrables, y esto es lo que hace la escritura estática.

Además, como las funciones se declaran con el tipo funcname () ..., sin conocer el tipo, tendrá que buscar sobre cada línea en la que se llama la función, porque solo conoce funcname, mientras que en Python y similares solo podría busque def funcname o function funcname, que solo ocurre una vez, en la declaración.

Los lenguajes estáticamente escritos tienen cosas llamadas "herramientas". Son programas que te ayudan a hacer cosas con tu código fuente. En este caso, simplemente haría clic derecho e Ir a definición, gracias a Resharper. O use el atajo de teclado. O simplemente pasa el mouse y me dirá cuáles son los tipos involucrados. No me importa en lo más mínimo los archivos grepping. Un editor de texto por sí solo es una herramienta patética para editar el código fuente del programa.

De memoria, def funcnameno sería suficiente en Python, ya que la función podría reasignarse arbitrariamente. O podría declararse repetidamente en múltiples módulos. O en clases. Etc.

y aún tendrá que volver a la línea en la que se declara utilizando la función de búsqueda de su editor de texto para verificarlo.

Buscar archivos para el nombre de la función es una operación primitiva terrible que nunca debería ser necesaria. Esto representa una falla fundamental de su entorno y herramientas. El hecho de que incluso consideres necesitar una búsqueda de texto en Python es un punto masivo contra Python.

DeadMG
fuente
2
Para ser justos, esas "herramientas" se inventaron en lenguajes dinámicos, y los lenguajes dinámicos las tenían mucho antes que los lenguajes estáticos. Ir a definición, finalización de código, refactorización automatizada, etc. existía en IDEs gráficas de Lisp y Smalltalk antes de que los lenguajes estáticos incluso tuvieran gráficos o IDEs, y mucho menos IDEs gráficos.
Jörg W Mittag
Conocer el tipo de retorno de funciones no siempre te dice qué funciones HACEN . En lugar de escribir tipos, podría haber escrito pruebas de documentos con valores de muestra. por ejemplo, compare (palabras 'algunas palabras oue') => ['algunas', 'palabras', 'oeu'] con (cadena de palabras) -> [cadena], (zip {abc} [1..3]) => [(a, 1), (b, 2), (c, 3)] con su tipo.
aoeu256
18

Piense en un proyecto con muchos programadores, que ha cambiado con los años. Tienes que mantener esto. Hay una función

getAnswer(v) {
 return v.answer
}

¿Qué demonios hace? ¿Qué es v? ¿De dónde answerviene el elemento ?

getAnswer(v : AnswerBot) {
  return v.answer
}

Ahora tenemos más información -; que necesita un tipo de AnswerBot.

Si vamos a un lenguaje basado en clases podemos decir

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Ahora podemos tener una variable de tipo AnswerBoty llamar al método getAnswery todos saben lo que hace. El compilador detecta los cambios antes de realizar cualquier prueba de tiempo de ejecución. Hay muchos otros ejemplos, pero ¿tal vez esto te da la idea?

daven11
fuente
1
Parece más claro, a menos que señale que una función como esa no tiene ninguna razón para existir, pero eso es, por supuesto, solo un ejemplo.
user6245072
Ese es el problema cuando tienes múltiples programadores en un proyecto grande, funciones como esa existen (y lo que es peor), son cosas de pesadillas. También considere que las funciones en lenguajes dinámicos se encuentran en el espacio de nombres global, por lo que con el tiempo podría tener un par de funciones getAnswer, y ambas funcionan y ambas son diferentes porque se cargan en momentos diferentes.
daven11
1
Supongo que es un malentendido de la programación funcional lo que causa eso. Sin embargo, ¿qué quieres decir con decir que están en el espacio de nombres global?
user6245072
3
"las funciones en lenguajes dinámicos están por defecto en el espacio de nombres global", este es un detalle específico del lenguaje, y no una restricción causada por tener un tipeo dinámico.
sara
2
@ daven11 "Estoy pensando en JavaScript aquí", de hecho, pero otros lenguajes dinámicos tienen espacios de nombres / módulos / paquetes reales y pueden advertirle sobre redefiniciones. Puede que estés generalizando un poco.
coredump
10

Parece tener algunas ideas erróneas sobre el trabajo con grandes proyectos estáticos que pueden estar nublando su juicio. Aquí hay algunos consejos:

incluso si declara el tipo de retorno de una función, puede y lo olvidará después de haber escrito muchas líneas de código, y aún tendrá que volver a la línea en la que se declara utilizando la función de búsqueda de su editor de texto para revisalo.

La mayoría de las personas que trabajan con idiomas de tipo estático usan un IDE para el idioma o un editor inteligente (como vim o emacs) que tiene integración con herramientas específicas del idioma. Por lo general, hay una forma rápida de encontrar el tipo de función en estas herramientas. Por ejemplo, con Eclipse en un proyecto Java, hay dos formas en que normalmente encontrará el tipo de método:

  • Si quiero usar un método en otro objeto que no sea 'esto', escribo una referencia y un punto (por ejemplo someVariable.) y Eclipse busca el tipo someVariabley proporciona una lista desplegable de todos los métodos definidos en ese tipo; A medida que me desplazo hacia abajo en la lista, se muestra el tipo y la documentación de cada uno mientras está seleccionado. Tenga en cuenta que esto es muy difícil de lograr con un lenguaje dinámico, porque es difícil (o en algunos casos imposible) para el editor determinar cuál es el tipo someVariable, por lo que no puede generar la lista correcta fácilmente. Si quiero usar un método this, puedo presionar ctrl + espacio para obtener la misma lista (aunque en este caso no es tan difícil de lograr para los lenguajes dinámicos).
  • Si ya tengo una referencia escrita para un método específico, puedo mover el cursor del mouse sobre él y el tipo y la documentación del método se muestran en una información sobre herramientas.

Como puede ver, esto es algo mejor que las herramientas típicas disponibles para lenguajes dinámicos (no es que esto sea imposible en lenguajes dinámicos, ya que algunos tienen una funcionalidad IDE bastante buena; smalltalk es uno que se me ocurre), pero es más difícil para un lenguaje dinámico y, por lo tanto, es menos probable que esté disponible).

Además, como las funciones se declaran con el tipo funcname () ..., sin conocer el tipo, tendrá que buscar sobre cada línea en la que se llama la función, porque solo conoce funcname, mientras que en Python y similares solo podría busque def funcname o function funcname, que solo ocurre una vez, en la declaración.

Las herramientas de lenguaje estático generalmente proporcionan capacidades de búsqueda semántica, es decir, pueden encontrar definiciones y referencias a símbolos particulares con precisión, sin necesidad de realizar una búsqueda de texto. Por ejemplo, usando Eclipse para un proyecto Java, puedo resaltar un símbolo en el editor de texto y hacer clic con el botón derecho y elegir 'ir a definición' o 'buscar referencias' para realizar cualquiera de estas operaciones. No necesita buscar el texto de una definición de función, porque su editor ya sabe exactamente dónde está.

Sin embargo, lo contrario es que la búsqueda de una definición de método por texto realmente no funciona tan bien en un proyecto dinámico grande como usted sugiere, ya que fácilmente podría haber múltiples métodos con el mismo nombre en dicho proyecto, y es probable que no tenga herramientas disponibles para desambiguarte a cuál de ellas estás invocando (porque tales herramientas son difíciles de escribir en el mejor de los casos o imposibles en el caso general), por lo que tendrás que hacerlo a mano.

Más aún, con REPLs es trivial probar una función para su tipo de retorno con diferentes entradas

No es imposible tener un REPL para un lenguaje de tipo estático. Haskell es el ejemplo que me viene a la mente, pero también hay REPL para otros lenguajes de tipo estático. Pero el punto es que no necesita ejecutar código para encontrar el tipo de retorno de una función en un lenguaje estático ; se puede determinar mediante un examen sin necesidad de ejecutar nada.

mientras que con los idiomas tipados estáticamente necesitaría agregar algunas líneas de código y volver a compilar todo solo para saber el tipo declarado.

Es probable que incluso si necesitara hacer esto, no tuviera que volver a compilar todo . La mayoría de los lenguajes estáticos modernos tienen compiladores incrementales que solo compilarán la pequeña porción de su código que ha cambiado, para que pueda obtener comentarios casi instantáneos para errores de tipo si hace uno. Eclipse / Java, por ejemplo, resaltará los errores de escritura mientras los sigue escribiendo .

Jules
fuente
44
You seem to have a few misconceptions about working with large static projects that may be clouding your judgement.Bueno, solo tengo 14 años y solo programo desde menos de un año en Android, así que es posible, supongo.
user6245072
1
Incluso sin un IDE, si elimina un método de una clase en Java y hay cosas que dependen de ese método, cualquier compilador de Java le dará una lista de cada línea que estaba usando ese método. En Python, falla cuando el código de ejecución llama al método faltante. Utilizo Java y Python regularmente y me encanta Python por la rapidez con la que puedes hacer que las cosas funcionen y las cosas geniales que puedes hacer que Java no admite, pero la realidad es que tengo problemas en los programas de Python que simplemente no suceden (derecho) Java. Refactorizar en particular es mucho más difícil en Python.
JimmyJames
6
  1. Porque los verificadores estáticos son más fáciles para los idiomas tipados estáticamente.
    • Como mínimo, sin características de lenguaje dinámico, si se compila, en el tiempo de ejecución no hay funciones sin resolver. Esto es común en proyectos ADA y C en microcontroladores. (Los programas de microcontroladores se vuelven grandes a veces ... como cientos de kloc en grande).
  2. Las comprobaciones de referencia de compilación estática son un subconjunto de invariantes de funciones, que en un lenguaje estático también se pueden verificar en tiempo de compilación.
  3. Los lenguajes estáticos suelen tener más transparencia referencial. El resultado es que un nuevo desarrollador puede sumergirse en un solo archivo y comprender algo de lo que está sucediendo, y corregir un error o agregar una pequeña característica sin tener que saber todas las cosas extrañas en la base de código.

Compare con say, javascript, Ruby o Smalltalk, donde los desarrolladores redefinen la funcionalidad del lenguaje central en tiempo de ejecución. Esto dificulta la comprensión del gran proyecto.

Los proyectos más grandes no solo tienen más personas, sino que también tienen más tiempo. Tiempo suficiente para que todos lo olviden o sigan adelante.

Como anécdota, un conocido mío tiene una programación segura de "Job For Life" en Lisp. Nadie, excepto el equipo, puede entender el código base.

Tim Williscroft
fuente
Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.¿Es realmente tan malo? ¿La personalización que agregaron no les ayuda a ser más productivos?
user6245072
@ user6245072 Puede ser una ventaja para las personas que trabajan actualmente allí, pero dificulta la contratación de nuevas personas. Se necesita más tiempo para encontrar a alguien que ya conoce un idioma no convencional o para enseñarle uno que aún no conoce. Esto puede dificultar que el proyecto se amplíe cuando tenga éxito o que se recupere de la fluctuación: las personas se mudan, son promovidas a otros puestos ... Después de un tiempo, también puede ser una desventaja para los propios especialistas. una vez que solo haya escrito algo de idioma nich durante una década más o menos, puede ser difícil pasar a algo nuevo.
Hulk
¿No puedes usar un marcador para crear pruebas unitarias desde el programa Lisp en ejecución? Al igual que en Python, puede crear un decorador (adverbio) llamado print_args que toma una función y devuelve una función modificada que imprime su argumento. Luego puede aplicarlo a todo el programa en sys.modules, aunque una forma más fácil de hacerlo es usar sys.set_trace.
aoeu256
@ aoeu256 No estoy familiarizado con las capacidades del entorno de tiempo de ejecución de Lisp. Pero sí usaron macros en gran medida, por lo que ningún programador normal de lisp pudo leer el código; Es probable que tratar de hacer cosas "simples" en el tiempo de ejecución no funcione debido a que las macros cambian todo sobre Lisp.
Tim Williscroft el
@TimWilliscroft Puede esperar hasta que todas las macros se expandan antes de hacer ese tipo de cosas. Emacs tiene muchas teclas de acceso directo para permitirle expandir macros en línea (y funciones en línea tal vez).
aoeu256
4

Nunca entendí declaraciones como esta. Para ser honesto, incluso si declara el tipo de retorno de una función, puede y lo olvidará después de haber escrito muchas líneas de código, y aún tendrá que volver a la línea en la que se declara utilizando la función de búsqueda de su editor de texto para verificarlo.

No se trata de que olvide el tipo de retorno; esto siempre va a suceder. Se trata de que la herramienta pueda hacerle saber que olvidó el tipo de devolución.

Además, a medida que las funciones se declaran con tipo funcname()..., sin conocer el tipo tendrá que buscar sobre cada línea en la que se llama la función, porque solo sabe funcname, mientras que en Python y similares simplemente puede buscar def funcnameo function funcnameque solo sucede una vez , en la declaración.

Esta es una cuestión de sintaxis, que no tiene relación alguna con la escritura estática.

La sintaxis de la familia C es de hecho hostil cuando desea buscar una declaración sin tener herramientas especializadas a su disposición. Otros idiomas no tienen este problema. Ver la sintaxis de la declaración de Rust:

fn funcname(a: i32) -> i32

Más aún, con REPLs es trivial probar una función para su tipo de retorno con diferentes entradas, mientras que con lenguajes de tipo estático necesitaría agregar algunas líneas de código y recompilar todo solo para saber el tipo declarado.

Cualquier idioma puede ser interpretado y cualquier idioma puede tener un REPL.


Entonces, aparte de conocer el tipo de retorno de una función que claramente no es un punto fuerte de los lenguajes estáticamente tipados, ¿cómo es realmente útil el tipeo estático en proyectos más grandes?

Contestaré de manera abstracta.

Un programa consta de varias operaciones y esas operaciones se presentan como están debido a algunas suposiciones que hace el desarrollador.

Algunos supuestos son implícitos y otros explícitos. Algunos supuestos se refieren a una operación cerca de ellos, algunos se refieren a una operación lejos de ellos. Una suposición es más fácil de identificar cuando se expresa explícitamente y lo más cerca posible de los lugares donde su valor de verdad es importante.

Un error es la manifestación de una suposición que existe en el programa pero que no se cumple en algunos casos. Para rastrear un error, necesitamos identificar la suposición errónea. Para eliminar el error, necesitamos eliminar esa suposición del programa o cambiar algo para que la suposición realmente se mantenga.

Me gustaría clasificar los supuestos en dos tipos.

El primer tipo son los supuestos que pueden o no ser válidos, dependiendo de las entradas del programa. Para identificar una suposición errónea de este tipo, necesitamos buscar en el espacio todas las entradas posibles del programa. Usando conjeturas educadas y pensamiento racional, podemos reducir el problema y buscar en un espacio mucho más pequeño. Pero aún así, a medida que un programa crece incluso un poco, su espacio de entrada inicial crece a un ritmo enorme, hasta el punto en que puede considerarse infinito para todos los fines prácticos.

El segundo tipo son los supuestos que definitivamente son válidos para todas las entradas, o son definitivamente erróneos para todas las entradas. Cuando identificamos una suposición de este tipo como errónea, ni siquiera necesitamos ejecutar el programa o probar ninguna entrada. Cuando identificamos una suposición de este tipo como correcta, tenemos que preocuparnos de un sospechoso menos cuando estamos rastreando un error ( cualquier error). Por lo tanto, es valioso tener tantos supuestos como sea posible pertenecer a este tipo.

Para poner un supuesto en la segunda categoría (siempre verdadero o siempre falso, independiente de las entradas), necesitamos una cantidad mínima de información para estar disponible en el lugar donde se realiza el supuesto. A través del código fuente de un programa, la información se vuelve obsoleta bastante rápido (por ejemplo, muchos compiladores no hacen análisis interprocediales, lo que hace que cualquier llamada sea un límite difícil para la mayoría de la información). Necesitamos una manera de mantener actualizada la información requerida (válida y cercana).

Una forma es tener la fuente de esta información lo más cerca posible del lugar donde se va a consumir, pero eso puede ser poco práctico para la mayoría de los casos de uso. Otra forma es repetir la información con frecuencia, renovando su relevancia en todo el código fuente.

Como ya puede adivinar, los tipos estáticos son exactamente eso: balizas de información de tipo dispersas en el código fuente. Esa información se puede utilizar para colocar la mayoría de los supuestos sobre la corrección de tipo en la segunda categoría, lo que significa que casi cualquier operación se puede clasificar como siempre correcta o siempre incorrecta con respecto a la compatibilidad de tipos.

Cuando nuestros tipos son incorrectos, el análisis nos ahorra tiempo al llamar la atención sobre el error más temprano que tarde. Cuando nuestros tipos son correctos, el análisis nos ahorra tiempo al garantizar que cuando se produce un error, podemos descartar inmediatamente los errores de tipo.

Theodoros Chatzigiannakis
fuente
3

Usted recuerda el viejo adagio "basura adentro, basura afuera", bueno, esto es lo que ayuda a prevenir la escritura estática. No es una panacea universal, pero la rigurosidad sobre qué tipo de datos acepta y devuelve una rutina significa que tiene la seguridad de que está trabajando correctamente con ella.

Por lo tanto, una rutina getAnswer que devuelve un número entero no será útil cuando intente usarlo en una llamada basada en cadenas. La escritura estática ya te dice que tengas cuidado, que probablemente estés cometiendo un error. (y claro, puedes anularlo, pero tendrías que saber exactamente qué es lo que estás haciendo, y especificarlo en el código usando un yeso. Sin embargo, en general, no quieres estar haciendo esto - pirateando un la clavija redonda en un agujero cuadrado nunca funciona bien al final)

Ahora puede llevarlo más allá al usar tipos complejos, al crear una clase que tenga funcionalidad adicional, puede comenzar a pasarlos y de repente obtendrá mucha más estructura en su programa. Los programas estructurados son aquellos que son mucho más fáciles de hacer que funcionen correctamente y que también se mantengan.

gbjbaanb
fuente
No tiene que hacer inferencia de tipo estático (pylint), puede hacer inferencia de tipo dinámico chrislaffra.blogspot.com/2016/12/… que también se realiza mediante el compilador JIT de PyPy. También hay otra versión de inferencia de tipo dinámico en la que una computadora coloca aleatoriamente objetos simulados en los argumentos y ve qué causa un error. El problema de detención no importa en el 99% de los casos, si toma demasiado tiempo, simplemente detenga el algoritmo (así es como Python maneja la recursión infinita, tiene un límite de recursión que se puede establecer).
aoeu256