1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • La razón por la que la popularidad de los tipos estáticos bajó desde los 2000 hasta inicios de los 2010 y luego volvió a subir a mediados y finales de los 2010 se explica por la mejora en la calidad de los sistemas de tipos estáticos
  • Un sistema de tipos dinámicos se compara con cavar la tierra con las manos, porque una persona debe juzgar directamente el estado y contenido de las variables y campos, y la computadora ni ayuda ni estorba
  • Los sistemas de tipos estáticos del pasado, como Java inicial o C++98, se comparan con una pala de papel, porque ni siquiera ayudaban a distinguir entre punteros nullable y non-nullable, y obligaban a repetir los nombres de tipos
  • Los sistemas de tipos modernos como TypeScript, Haskell, MyPy, Swift y Rust ofrecen mejor soporte para verificar errores del programa y representar estados mediante manejo de null, tipos suma y unión, e inferencia de tipos
  • A medida que se popularizaron funciones como el autocompletado de nombres de métodos en los IDE, la información puesta en el sistema de tipos estáticos empezó a traducirse en ventajas de productividad además de la verificación de errores

Argumento central

  • La popularidad de los tipos estáticos no es solo una moda, sino que volvió a crecer porque mejoró la calidad de los sistemas de tipos estáticos disponibles para uso general
  • Se usa la analogía de que, si hay una buena pala, es mejor cavar con una pala que con las manos, pero si solo hay una pala hecha de papel, es mejor usar las manos
  • En un sistema de tipos dinámicos, una persona debe pensar directamente qué estado y contenido tienen las variables y campos del programa, y la computadora no ayuda con ese juicio
  • Un mal sistema de tipos estáticos puede generar más carga que ayuda, y eso se compara con intentar cavar la tierra con una pala de papel

Limitaciones de los sistemas de tipos estáticos del pasado

  • Los sistemas de tipos estáticos de Java inicial o C++98, muy usados en los 90 y a inicios de los 2000, ni siquiera ayudaban bien con la tarea simple de distinguir entre punteros nullable y non-nullable
  • Los sistemas de tipos del pasado se describen como estructuras que no tenían tipos suma y solo tenían tipos producto
  • Los sistemas de tipos del pasado obligaban a escribir manualmente los nombres de tipos por todas partes
  • Código como BufferedReader bufferedReader = new BufferedReader(new FileReader(filename)); se describe como un pequeño desastre

Mejoras de los sistemas de tipos modernos

  • Los sistemas de tipos modernos como TypeScript, Haskell, MyPy, Swift y Rust ofrecen formas de distinguir entre tipos nullable y non-nullable
  • Se ponen como ejemplo Maybe t en Haskell, T | null en TypeScript, T? en Swift y Optional<T> en Rust, mostrando que el sistema de tipos puede indicar dónde hace falta revisar null y dónde esa revisión fue omitida
  • Se explica que en la práctica casi se deja de ver errores de null pointer en tiempo de ejecución
  • Los sistemas de tipos modernos ofrecen al menos uno entre tipos suma o tipos unión, y permiten poner en práctica "Make invalid states unrepresentable"
  • Este enfoque permite expresar, en objetos que representan máquinas de estados, que cada campo exista solo cuando corresponde a un estado relacionado
  • Los sistemas de tipos modernos ofrecen inferencia de tipos, así que si el compilador puede determinar que let x = 5; es un número, no hace falta escribir let x: number = 5;

Funciones del IDE y conclusión

  • A medida que se popularizaron funciones de IDE como el autocompletado de nombres de métodos, la utilidad de los sistemas de tipos estáticos creció aún más
  • En los 90, Intellisense era una función central de Visual Studio, pero en los 2020 funciones parecidas están disponibles en casi todos los IDE y editores
  • La información puesta en el sistema de tipos estáticos no solo sirve para revisar errores del programa, sino que también genera ventajas adicionales de productividad
  • Un buen sistema de tipos dinámicos es mejor que un mal sistema de tipos estáticos, pero hoy se pueden usar sistemas de tipos estáticos mucho mejores que los del pasado

1 comentarios

 
GN⁺ 4 시간 전
Opiniones de Lobste.rs
  • El texto está bueno, pero no estoy completamente de acuerdo. Aunque los sistemas de tipos estáticos de inicios de los 2000 no eran excelentes, igual eran mucho mejores que no tener tipos estáticos en absoluto
    No había tipos suma cerrados, pero se podía modelar bastante con subtipado, y aunque no existían los tipos no anulables, las referencias y tipos no puntero de C++, y los tipos primitivos de Java, cubrían parte de eso. En Ruby o JavaScript la situación era peor, porque no solo todos los tipos podían ser null, sino que además podían tratarse como string, como entero o como cualquier otro tipo del programa
    Creo que una razón importante del cambio de rumbo respecto a los tipos estáticos fue que, durante el boom de las redes sociales de la Web 2.0, el efecto de ser el primero importaba más que cualquier otra cosa. Aunque acumularas deuda técnica con Ruby o Python, era mejor lanzar rápido e iterar que quedar desplazado como Friendster o Digg, y si algo iba lento, en esa época bastaba con comprar más servidores con el capital barato que se conseguía fácilmente
    Después, con el boom móvil, el software pasó a ejecutarse en dispositivos de usuario limitados y fuera de tu control, y las apps lentas con tipado dinámico simplemente eran lentas de verdad, mientras que un error de tipo no podía recuperarse elegantemente con un manejador de respuesta de nivel superior como en un servidor. En ese entorno, la seguridad y el rendimiento del tipado estático se volvieron mucho más convincentes

    • Hay bastantes artículos que comparan Java y el C++ estilo noventero con codebases en lenguajes de tipado dinámico y concluyen que tienen tasas de bugs parecidas, y los partidarios de los lenguajes dinámicos suelen citarlos como prueba de que el tipado estático no sirve de mucho
      A principios de los 2000 yo también estaba de acuerdo, porque los sistemas de tipos de entonces solían imponer restricciones que ayudaban poco a estructurar el código mientras solo hacían cumplir propiedades que casi nunca fallaban. En particular, la combinación de subtipado y herencia de implementación no era flexible
      Mi opinión cambió al usar sistemas de tipos más modernos. En snmalloc, el sistema de tipos de C++ hace cumplir una máquina de estados de propiedad de memoria, y en otros codebases verifica el comportamiento correcto de overflow de contadores de buffer circular. En ambos casos, si eso sale mal, depurarlo es molesto y es una fuente común de errores; de hecho, el compilador rechazó código que yo creía correcto y evitó que bugs entraran al árbol
    • Creo que desarrollar en lenguajes de tipado dinámico es más lento que hacerlo en lenguajes de tipado estático. Sigo viendo la afirmación contraria, pero no la entiendo
      En el IDE, presionar . y teclear un poco el nombre del método para luego dar Enter en la opción correcta te ahorra 2 segundos cada pocos segundos, y también te ahorra esos 30 segundos de ir a buscar la definición de la clase cuando no sabes qué métodos tiene. Ese principio también queda muy bien explicado en https://grugbrain.dev/#grug-on-type-systems
      Uno escribe líneas que llaman métodos con mucha más frecuencia de la que escribe tipos de parámetros de funciones, así que el intercambio sale abrumadoramente mal para el tipado dinámico. Lo que sí tenía valor no era permitir código absurdo que iba a fallar en runtime, sino omitir el tipo de las variables locales, y los lenguajes estáticos nunca tuvieron por qué prohibir eso
    • Los sistemas de tipos populares de principios de los 2000 no eran simplemente “no tan increíbles”; eran malos, y muy verbosos
      Los pocos codebases que usaban el sistema de tipos en serio acumulaban páginas enteras de código que no decía nada, y aun así tenían montones de condicionales en runtime; además, en Java el programa de hecho se volvía más lento a medida que crecía la jerarquía de tipos. La mayoría de los codebases usaban los tipos a medias y agregaban muchos condicionales en runtime, por lo que tampoco ahorraban mucho frente a sistemas dinámicos en cuanto al alcance de pruebas necesarias
      Los lenguajes dinámicos no daban recompensas estáticas, pero eran concisos, fáciles de leer y revisar, y también fáciles de probar. Eso era especialmente cierto en entornos como los frameworks de inyección de dependencias de finales de los 90 e inicios de los 2000, donde agregar un servicio nuevo implicaba editar varios archivos XML. Además, podías trabajar sin un IDE que se comiera la mitad de la RAM
      Mis primeros años de carrera fueron exactamente así, así que coincido bastante con el texto. La relación costo-beneficio de Java 1.4 a Java 6 era tan mala que casi me hizo abandonar por completo los lenguajes con tipado estático; solo años después, cuando probé Haskell como hobby, entendí que el tipado estático sí podía tener una relación costo-beneficio razonable, y que el problema era Java. El ensayo “python is not java” también retrata bien esa época oscura
    • El subtipado basado en herencia era todavía más pobre. No ofrecía la usabilidad del pattern matching con verificación de exhaustividad, y la implementación quedaba fragmentada en varios lugares
    • La explicación de que era importante lanzar un sitio antes que la competencia, ponerlo frente a los usuarios y asegurar economías de escala suena bastante familiar incluso hoy
  • Dudo que, desde que el tipado estático se volvió el espíritu de la época, hayamos visto de verdad una ganancia de confiabilidad en nuestro software
    Siempre pensé que la ventaja del tipado estático estaba mucho más en la retroalimentación inmediata durante el desarrollo y en reducir fallas fatales en tiempo de ejecución, pero aunque en teoría esas fallas siempre pueden pasar, en la práctica no parecía que ocurrieran tan seguido

    • Sí lo he visto. Cuando empezamos a apuntar a 0 errores de TypeScript en un codebase de TypeScript nada trivial, se desplomaron los intentos de llamar métodos sobre undefined y null
      Al principio, los junior y algunos senior eran escépticos y pensaban que iban a aparecer @ts-ignore por todos lados, pero en la práctica fueron como tres, incluso contando los causados por tipos rotos en dependencias. Antes, más o menos una vez por semana la app se caía en la rama de desarrollo por confusiones de tipos y me bloqueaba el trabajo; ahora ni recuerdo cuándo fue la última vez que me pasó algo así
      Con solo dejar satisfecho a tsc, incluso en casos donde yo no escribí el código, bajaron los bugs relacionados con tipos. En cambio, los linters de hoy son demasiado entusiastas, y tratando de dejar contentas herramientas como Sonar vi roturas reales por refactors. El 95% de las advertencias eran falsas, 3% eran bugs de la herramienta, y el 2% que sí ayudó tampoco era la causa real del bug. En vez de dedicar una semana a ajustar el codebase y atrapar un bug, metí dos más en el proceso
      El trabajo de dejar satisfecho a tsc producía más o menos 2 correcciones de bugs puros por día y 1 regresión, pero la regresión normalmente era menos grave, de comportamiento incorrecto y no de crash total
      Si además le sumas pruebas basadas en propiedades, en promedio tomaban entre 2 y 4 horas y siempre destapaban al menos un bug. Si tu código se puede probar con pruebas basadas en propiedades, hay que hacerlo
      Al ampliar la cobertura de pruebas con un modelo barato como DeepSeek V4 Flash, cuidando que no salieran pruebas basura, arreglábamos entre 2 y 3 bugs lógicos por día, y no hubo crashes. Eso sí, el conjunto de pruebas apenas era mantenible
      Cuando dejamos que un junior armara pruebas más o menos a la rápida con modelos de la línea Sonnet y Opus 4.5, 4.6, los modelos solo generaban pruebas que “documentaban el comportamiento actual”, así que el efecto de las correcciones fue pequeño, y el conjunto de pruebas era tan inmantenible que hubo que descartarlo
      Las pruebas basadas en modelos son muy buenas para atrapar bugs, pero configurarlas es complicado, y lograr que exploren a fondo las esquinas sin gastar ciclos en funcionalidades superficiales es muy engorroso. Algo como un fuzzer basado en modelos y guiado por perfiles sería interesante
      En resumen: los verificadores de tipos atrapan bien fallas fatales y varias confusiones, y las pruebas basadas en propiedades son excelentes. Las pruebas comunes requieren mucha disciplina para dar recompensas de forma constante
    • En lo personal, diría que sí. En el JavaScript que uso, los bugs de puntero nulo se volvieron casi despreciables después de migrar a TypeScript, y mis colegas vieron algo parecido
  • Lo que más me cuesta aceptar aquí es meter a TypeScript en el mismo saco que un buen sistema de tipos

    • Sí. TypeScript no es sound, y en particular la forma en que estrecha tipos a través de await me ha mordido varias veces. Aun así, es verdad que mejoró la situación de forma dramática
      Siendo sincero, al final también terminé aceptando el tipado estructural, y creo que va a influir positivamente en el diseño de lenguajes a futuro
  • Este argumento convence poco. Los lenguajes de programación decentes con tipos algebraicos de datos e inferencia de tipos existen desde mediados de los 90
    Los sistemas de tipos de Java y C++ eran muy pobres, pero SML, OCaml y Haskell ya existían y se sentían bastante parecidos a los de hoy. Si la gente no usaba esos lenguajes, eso tiene que ver con cultura, adopción y requisitos no expresados, no se puede explicar solo con que “los sistemas de tipos utilizables no eran lo bastante buenos”
    O, si la afirmación es “los sistemas de tipos de los lenguajes populares de entonces eran malos, y los de los lenguajes populares de hoy son mejores, así que los sistemas de tipos se volvieron más populares”, suena a razonamiento circular
    También hay muchos matices en la diferencia entre lenguajes diseñados junto con un sistema de tipos y lenguajes que originalmente se diseñaron sin tipos y después recibieron uno encima

  • Incluso viniendo de alguien que siempre ha preferido el tipado dinámico, me parece que este texto es bastante justo. Hoy trabajo con C#, uso Lisp como hobby y antes también usé Python
    Cuando me tocó usar Java 5, casi siempre estaba peleando con el sistema de tipos por malas decisiones de los autores de librerías. Después de pasarme a C# alrededor de 2010, el sistema de tipos ya no era activamente dañino, pero en general era redundante, y tampoco evitaba la excepción de puntero nulo, que era la confusión de tipos más común en Python
    El sistema de tipos de C# recién empezó a ayudar de verdad alrededor de 2020, cuando llegaron los tipos de referencia no anulables. Este año también llegan los tipos unión nativos, aunque las librerías de tipos unión que fuerzan exhaustividad existen al menos desde 2016, y yo las empecé a usar en 2020
    Creo que la moda sigue influyendo, pero una parte de eso no es mala. Los lenguajes de moda con sistemas de tipos más expresivos también trajeron mejoras a los lenguajes comunes que usamos para ganarnos la vida

  • Haskell y su sistema de tipos ya existían desde los años 2000. No se usaban tan ampliamente como ahora, pero claramente existían, así que esta afirmación debería matizar esa parte.
    Personalmente, creo que TypeScript fue un factor importante para familiarizar a los usuarios de lenguajes mainstream con un mejor sistema de tipos. Además de su calidad y el respaldo de Microsoft, tenía la ventaja de aplicarse a JavaScript, y JavaScript necesitaba mucho más los tipos que Python. Por “Undefined is not a function.” y “The good parts.”

    • Ojalá saliera un libro de “good parts” adaptado a la versión moderna de JavaScript y que mantuviera la concisión.
      “Real World Haskell” salió en 2008 y su objetivo era hacer que Haskell pareciera más atractivo para los programadores mainstream. No sé cuánto ayudó a difundir la buena nueva.
      En el mundo Java, Scala trajo tipos geniales en 2004, y en .NET apareció F# en 2005. Puede que Scala haya conseguido los usuarios más visibles, como Twitter, pero no estaba en una posición para absorber una gran parte de los usuarios de esa plataforma como sí lo hizo TypeScript, ni era lo bastante atractivo como Rust o Go para atraer en masa a usuarios de otros lenguajes.
    • El artículo ya trata este problema. Compara los sistemas de tipos estáticos pobres de Java temprano o C++98, populares en los 90 y principios de los 2000, con palas de papel.
      En el párrafo siguiente menciona a Haskell como un “sistema de tipos moderno”, pero en la práctica la gente con experiencia en Haskell a fines de los 90 y principios de los 2000, incluso contando a quienes solo lo tocaron un poco, era prácticamente 0%. El artículo habla de cómo la gran mayoría de los desarrolladores experimentó los lenguajes con tipos estáticos en esa época, y de por qué esa mayoría terminó evitando colectivamente los lenguajes de tipos estáticos.
    • Creo que Haskell y OCaml sufren en cierta medida por la debilidad de su ecosistema de herramientas. El lenguaje en sí es excelente, pero pierden adopción por las innumerables pequeñas molestias del lado de las herramientas.
      Por ejemplo, para usar dune en OCaml hay que entender archivos opam, archivos dune, la sintaxis de ocaml module y la sintaxis de ocaml. Las extensiones opcionales del compilador en Haskell también se sienten igual de intimidantes.
      Contrasta con cargo, donde basta con conocer toml y Rust.