1 puntos por GN⁺ 2023-10-05 | 1 comentarios | Compartir por WhatsApp
  • Con más de 20 años escribiendo software, el tipado estático fuerte casi siempre vale la pena, salvo excepciones como un REPL o scripts de un solo uso
  • Los tipos dejan un contrato en el código entre quien llama y quien es llamado, filtrando parámetros o valores de retorno incorrectos en tiempo de compilación o de verificación de tipos
  • El ejemplo de la cadena "20" proveniente de una entrada HTML que se usa como número y termina convirtiéndose en "201" muestra la diferencia entre errores atrapados antes de ejecución y errores expuestos al cliente
  • Svix busca meter en el sistema de tipos las claves de Redis, valores en caché, identificadores como PersonId y PetId, y la validación de entradas de API para reducir errores tipográficos y el paso de IDs incorrectos
  • Omitir tipos puede acelerar la implementación inicial, pero aumenta los costos de documentación, pruebas y depuración; con inferencia de tipos y soporte del IDE, refactorizar e incorporar gente nueva se vuelve más fácil

Por qué insisto en los tipos estáticos

  • El tipado estático fuerte no solo es una buena idea; en la mayoría del software está más cerca de ser la opción correcta por defecto
  • Los lenguajes o variantes sin tipos también tienen utilidad
    • Uso de REPL
    • Scripts de un solo uso en entornos ya casi sin tipos, por ejemplo el shell
  • Fuera de eso, en la mayoría de los casos se prefieren tipos fuertes
  • No usar tipos puede acelerar el desarrollo en el corto plazo, pero se parece más a “ir a toda velocidad hacia un precipicio”
  • Al final, la elección es una de estas dos
    • Trabajar más para verificar invariantes en tiempo de compilación o de verificación de tipos
    • Trabajar menos y verificarlas en tiempo de ejecución, o ni siquiera verificarlas ahí
  • Los errores en tiempo de ejecución no siempre se atrapan durante el desarrollo y, aun si se detectan, pueden manifestarse de forma visible para el cliente
  • Las pruebas ayudan, pero es difícil probar todos los tipos incorrectos posibles de parámetros de funciones, y bloquear tipos incorrectos con tipos resulta más fácil

Los tipos se conectan directamente con contratos de código y menos bugs

  • Los tipos son comentarios de código útiles tanto para personas como para herramientas, y además endurecen el contrato entre piezas de código
  • Incluso con la misma función para felicitar cumpleaños, la claridad del contrato cambia mucho
    • birthdayGreeting1(...params) ni siquiera revela cuántos parámetros tiene, así que sin leer la documentación es difícil saber cómo funciona
    • birthdayGreeting2(name, age) da la pista de que hay un nombre y una edad, pero no sus tipos
    • birthdayGreeting3(name: string, age: number): string incluye en el contrato tanto los tipos de entrada como el de retorno
  • Si la función cambia para usar age + 1, la versión sin tipos falla con entradas de texto
    • Los valores que vienen de una entrada HTML siempre pueden ser cadenas
    • birthdayGreeting2("John", "20") devuelve "John will turn 201 next year!"
    • La versión tipada exige que age sea numérico, así que la llamada incorrecta falla en compilación
  • El contrato entre quien llama y quien es llamado se vuelve más importante a medida que crece el código base
    • Permite saber cómo afecta a quienes llaman un cambio en quien es llamado
    • Es especialmente importante cuando quien llama y quien es llamado son personas distintas, como en bibliotecas open source
  • Sin ese contrato, es difícil entender hasta dónde impacta un cambio

Ventajas en experiencia de desarrollo, refactorización y onboarding

  • La información de tipos permite que el IDE y las herramientas de desarrollo mejoren mucho la experiencia de desarrollo
  • Mientras se escribe código, se puede saber enseguida cuando una expectativa es incorrecta, lo que reduce la carga cognitiva
  • El desarrollador no necesita recordar los tipos de todas las variables y funciones del contexto actual, porque el compilador indica dónde no encajan
  • Refactorizar también se vuelve más fácil
    • Si cambia la implementación de una función, el compilador puede avisar si se están rompiendo supuestos en otro lugar
  • También facilita que un nuevo ingeniero se adapte al código base o a una biblioteca
    • Puede seguir las definiciones de tipos para entender dónde se usan
    • Como los cambios generan errores de compilación, es más fácil experimentar
  • La diferencia se ve en el ejemplo de una función que recibe un tipo Person
    • birthdayGreeting3(person: Person) permite encontrar fácilmente en el IDE los puntos donde se usa Person
    • En la versión sin tipos birthdayGreeting2(person), solo leyendo todo el código base se puede saber que en realidad espera un Person
  • La documentación puede compensar parte de esto, pero tiende a desactualizarse, mientras que los tipos quedan como documentación dentro del propio código
  • Los tipos se parecen a una versión más fuerte de usar nombres de variables útiles

Cómo Svix mete información en el sistema de tipos

  • Svix intenta meter la mayor cantidad posible de información en el sistema de tipos para reducir errores que se pueden atrapar en compilación y mejorar la experiencia de desarrollo
  • Redis es, por naturaleza, un protocolo basado en cadenas y no tiene tipos integrados, así que en esa capa se pueden perder las ventajas de los tipos
  • El ejemplo de caché simple tiene dos bugs
    • Hay un error tipográfico en el nombre de la clave, como person-{id} frente a preson-{id}
    • Intenta cargar datos de una persona como si fueran del tipo Pet
  • Para evitar estos problemas, Svix aplica dos cosas
    • Exigir que la clave sea un tipo específico y no una cadena genérica
    • Emparejar de forma forzada clave y valor
  • Por ejemplo, si se usa una clave creada con PersonCacheKey::new(id), entonces el código que intente recibir como Pet el resultado de cache.get(PersonCacheKey::new(id)) fallará en compilación
  • Incluso un ID simple de tipo String facilita errores
    • do_something(id: String) no deja claro qué tipo de ID debería recibir
    • Se puede cometer el error de pasar pet.id cuando en realidad había que pasar pet.owner
  • Svix define un tipo separado para cada ID
    • PersonId(String)
    • PetId(String)
    • owner en Pet es de tipo PersonId
  • La validez del ID recibido por API también se conecta con la creación del tipo
    • Por ejemplo, un ID de mascota usa un formato con prefijo pet_ seguido de un Ksuid
    • PetId no puede crearse sin validación
    • Con este enfoque, cuando la base de datos no encuentra una mascota y devuelve 404 Not Found, se puede tener la certeza de que el formato del ID en sí era válido
    • Los IDs inválidos ya se manejaron en el handler de la API como 422 o 400

Argumentos en contra y el papel de las herramientas

  • Los principales argumentos en contra de los tipos son la velocidad de desarrollo, la curva de aprendizaje y complejidad de tipos, y el esfuerzo o boilerplate
  • Prototipar sin tipos sí puede ser más rápido
    • Se puede comentar código sin que el compilador se queje
    • Se pueden poner valores incorrectos en campos hasta decidir cuáles serán los correctos
  • Pero esto se considera deuda técnica agresiva e innecesaria, cuyo costo se paga varias veces al depurar en local, en la suite de pruebas y en producción
  • La curva de aprendizaje existe, pero la mayoría de la gente no necesita convertirse en experta en tipos
    • Con expresiones de tipos simples ya se puede trabajar bastante
    • Si uno se atasca, puede pedir ayuda
  • Ya de por sí los desarrolladores tienen que aprender muchas cosas, como programar o frameworks como React y Axum, así que se considera exagerada la carga atribuida al aprendizaje de tipos
  • Aprender tipos es un costo de una sola vez, y los beneficios que aportan durante el onboarding a un código base específico son mayores
  • Si no se usan tipos, hace falta bastante documentación y pruebas para lograr estabilidad básica
    • La documentación y las pruebas pueden quedar desactualizadas
    • Se considera que agregar los tipos correctos requiere menos esfuerzo
  • En lenguajes sin inferencia de tipos, escribir tipos puede ser engorroso
    • El ejemplo con Java genera repeticiones como Person person1 = newPerson();
    • Después se añade una corrección indicando que Java sí tiene inferencia de tipos
  • En lenguajes con inferencia de tipos, como Rust, se puede escribir de forma más concisa, por ejemplo let person1 = new_person();
  • Para obtener las ventajas de los tipos, hace falta un editor de código o IDE con autocompletado moderno que entienda el lenguaje
  • A diferencia de debates de preferencia como vim vs emacs o tabulaciones vs espacios, aquí se sostiene que los beneficios de los tipos frente a su costo son tan altos que cuesta entender por qué no usarlos
  • Hay un texto de seguimiento: using the type system effectively

1 comentarios

 
GN⁺ 2023-10-05
Opiniones en Hacker News
  • Lo más frustrante de esta discusión es que todo gira en torno a cómo se siente la gente, y falta evidencia empírica.
    Los estudios existentes sugieren que no hay una diferencia significativa entre ambos enfoques y, salvo que haya investigaciones nuevas, es difícil afirmar que la opción que cada quien prefiere sea claramente la correcta.
    En lo personal me gustan los lenguajes con tipos, pero sistemas de tipos como el de TypeScript se quedan cortos. Como no se pueden usar realmente los tipos en tiempo de ejecución, siguen quedando bugs de runtime, y como mucha lógica de runtime no puede codificarse en el sistema de tipos, uno todavía tiene que verificar manualmente casos imposibles.
    Si el sistema de tipos casi eliminara la necesidad de pensar en bugs de runtime, sería una ventaja abrumadora, pero la mayoría de los lenguajes no llega a ese nivel y se queda en un punto intermedio ambiguo entre overhead y algunos beneficios.
    La razón por la que no se ve una gran diferencia en cantidad de bugs o velocidad parece ser que al final las cosas se compensan. Si no hay una red de seguridad de tipos, se escriben más pruebas; en cambio, si se confía demasiado en el sistema de tipos, termina quedando una cantidad parecida de bugs de runtime. Me gustaría que hubiera estudios sólidos sobre este tema, pero es un problema difícil.

    • Creo que la mayoría estaría de acuerdo en que los tipos evitan muchos bugs, y en este hilo también se compartió un estudio así.
      El punto central se acerca más a cuáles son las razones subjetivas para decidir que no vale la pena invertir en tipos.
    • Al final creo que tendremos que conformarnos con que es un problema de criterio.
      Hace unos años revisé estudios sobre productividad de desarrolladores, y casi todos eran pésimos o solo aplicaban bien a juniors. Por ejemplo, los principiantes se benefician mucho de recibir feedback rápido sobre errores estáticos.
      Es casi imposible aplicar un buen diseño experimental a profesionales en lugar de estudiantes universitarios, y además hay que aislar muchísimas variables —diferencias individuales, tipo de desarrollo, forma de gestión, etc.—, así que es difícil extraer una señal. Es triste, pero muchas cosas en la vida son difíciles de medir eficazmente.
    • Pienso casi lo mismo.
      El artículo y muchos comentarios hablan de comodidad para el programador, productividad y “corrección”, pero la investigación actual no muestra resultados significativos de que los tipos estáticos mejoren o empeoren esas cosas. En la práctica, es subjetivo.
      Sin embargo, hay un efecto real de los tipos estáticos que se puede demostrar de forma trivial: permiten escribir código más eficiente. Ese debería ser el centro de la discusión sobre disciplina de tipos; lo demás, por ahora, es bastante especulativo.
      TypeScript, que se menciona en el artículo, en realidad no tiene tipado fuerte; tiene tipado estático, pero débil. Sus tipos se parecen más a anotaciones, sin garantías de rendimiento ni de distribución en memoria. Por eso, más allá de la documentación, se paga el costo de los tipos estáticos pero se obtienen muy pocos beneficios sustanciales.
      Me sorprende que la comunidad tecnológica ignore la evidencia real y acepte preferencias culturales y personales como si fueran hechos.
    • Los tipos estáticos son solo una rebanada de queso suizo para lograr software más confiable.
      Como otras técnicas, tienen agujeros, así que para maximizar la confiabilidad hay que combinar varias técnicas. Descartar los tipos estáticos porque no detectan todo es parecido a decir que no vas a cerrar la puerta con llave porque un ladrón puede romper la ventana. Si la seguridad realmente importa, cierras la puerta y también pones rejas en las ventanas; no eliges solo una de las dos cosas.
    • Parece como si se pensara que escribir programas en un lenguaje para expresar tipos hace que los “bugs de runtime” desaparezcan mágicamente.
      Si un lenguaje es lo bastante potente como para escribir programas generales, también es lo bastante potente como para crear bugs.
      Los tipos estáticos pueden ser eficaces para detectar ciertos tipos de bugs, pero no todos. A veces aumentan la legibilidad, como pruebas unitarias estáticas o lenguajes específicos de dominio para documentación ejecutable.
      En general, los lenguajes dinámicos son más ágiles y permiten escribir más pruebas con mayor facilidad. También hay pruebas que no haría falta escribir si fuera un lenguaje con tipos estáticos, así que los tipos siguen siendo útiles, pero no son tan universalmente poderosos como suele creerse.
  • Más allá de la presión social de que a uno le tengan que gustar los tipos estáticos, al final la razón por la que siempre terminé alejándome de ellos fue que a su alrededor siempre se levantaba una torre de marfil.
    He creado software durante 10 años en cada uno de los dos paradigmas, y ahora prefiero no usar sistemas de tipos.
    Siento que los tipos dinámicos son una fuerza que te obliga a escribir código simple, así como las pruebas unitarias obligan a escribir código componible. Código fácil de leer y de entender.
    Tampoco me convence el argumento de que hace que una base de código sea más accesible para desarrolladores principiantes. Es fácil que fomente un ciclo repetitivo de solo quitar las marcas rojas sin entender. Los sistemas de tipos hacen que en cada proyecto tengas que aprender, encima del lenguaje, otro lenguaje muy específico del dominio, y muchas veces entorpecen entender el comportamiento real.
    Los problemas planteados en el texto se pueden resolver de formas tan sólidas como con tipos y más fáciles de entender. Se pueden usar los tipos de manera simple, pero en mi experiencia casi nunca fue así en la práctica. Tampoco me gusta el autocompletado, así que tómenlo como tal.
    Puede que solo sea un desarrollador viejo gritando “el código es la documentación”, pero también puede ser una idea nacida de una profunda frustración con tantos desarrolladores que abundan hoy en la industria, de esos de “ChatGPT dijo que estaba bien y aun así cobro mucho”.

    • Si los tipos dinámicos realmente hicieran que la mayoría de los desarrolladores escribieran código simple, sería un argumento muy potente.
      Pero en general la evidencia en contra parece más fuerte. Los tipos creados después para documentar código dinámico real suelen ser mucho más complejos que la misma funcionalidad implementada desde el principio con tipos estáticos. DefinitelyTyped del ecosistema TypeScript ofrece muchísimos ejemplos.
      Es difícil decir que esos tipos se “usan de forma simple”, pero esa complejidad no viene del sistema de tipos en sí ni de la forma en que se proveen las definiciones de tipos, sino de la complejidad del código dinámico que describen.
      Un paquete equivalente creado desde el inicio con tipos estáticos suele tener una interfaz más simple. Porque los tipos se definen de entrada, no se encajan después en una API existente.
      Incluso diría que, si no se explicita una interfaz, no se puede saber si esa interfaz es simple o compleja. Estoy de acuerdo con el ideal de que “el código es la documentación”, pero si no hay código que explicite la interfaz, entonces esa interfaz, por definición, está poco documentada.
    • Lo de que “el sistema de tipos bloquea la comprensión de los desarrolladores principiantes y solo los hace quitar marcas rojas” de entrada me suena exactamente al revés.
      Tanto que me gustaría ver una escena así de cerca. En mi área, la lógica específica del dominio en bases de código con tipado dinámico es casi imposible de entender, mientras que el código con tipado estático le enseña al desarrollador la lógica de negocio.
      Lo de “el código es la documentación” también me confunde más bien en el sentido contrario. En mi experiencia, hace falta tipado estático para que el código sea documentación. Sin eso, no hay forma de saber qué propiedades tiene un objeto ni por qué se está verificando una propiedad que uno pensaba que no existía. Hay comentarios, sí, pero casi nunca veo a alguien dejar comentarios significativos.
    • Me cuesta entender esta lógica porque casi todo me parece al revés.
      Mi experiencia es la contraria. Los patrones muy dinámicos son difíciles de tipar bien, y un buen sistema de tipos fomenta patrones más simples, por lo que los tipos también se vuelven más simples.
      Quitar las marcas rojas es importante. Una marca roja significa que hay un problema, y es mucho más fácil que descubrir el error de otra manera. No entiendo por qué alguien querría descubrir ese error más tarde.
      Decir que tampoco te gusta el autocompletado me pone del lado de no confiar en los opositores al tipado estático. Un programador que no quiere que la computadora lo ayude a programar es muy sospechoso.
    • Es un buen punto que las torres de marfil y la presión social pueden hacer que uno no quiera adoptar cierta tecnología.
      Pero eso no reduce sus ventajas técnicas. Una tecnología puede ser excelente y, aun así, la gente a su alrededor puede estar llena de pretensión.
      El argumento de que los tipos dinámicos hacen que uno escriba código simple suena como decir “manejar con los ojos vendados es bueno porque te hace manejar despacio”. Si ese es el objetivo, usa un linter que limite el largo de las líneas o la cantidad de parámetros; no hace falta crear restricciones de forma indirecta.
      Los tipos no son la única solución, pero creo que son la primera herramienta que conviene sacar porque su retorno sobre la inversión es muy alto. La inversión es casi nula y el beneficio es grande.
      Estoy de acuerdo con que “el código es documentación”, pero los tipos también son parte del código. Por eso me gustaría decirlo como “el código es documentación, y los tipos son parte del código”.
    • Estaba a punto de escribir casi la misma respuesta.
      Nuestro campo es la ingeniería, no existe una única respuesta correcta y todo es una negociación de compromisos. De hecho, por eso nuestro trabajo no se automatiza y desaparece de inmediato.
      La atmósfera de este hilo, en la que se “menosprecia” a otros ingenieros por sus opiniones o experiencias, me resulta realmente desagradable.
  • En un contexto donde la mayoría de los datos viajan por la red como JSON, la pelea por aplicar tipado estático fuerte suele darse de manera bastante inconsistente.
    Hay que usar todas las herramientas disponibles, pero la mayoría de los “datos” son mucho más blandos de lo que uno cree. La gente deja los números de teléfono como strings no por flojera, sino porque alguna vez creyó que podía convertirlos en un tipo más fuerte y se golpeó demasiadas veces. Lo mismo pasa con nombres, direcciones y códigos postales.
    Esos valores hay que recibirlos de usuarios y, en la práctica, no hay otra opción que parsear texto. Si diseñas un sistema para no guardar el texto original antes del parseo, casi seguro algún día te vas a arrepentir.
    Creo que lo mejor es una capa que conserve el texto de entrada original y lo ofrezca a los usuarios del backend como un conjunto de datos tipado, pero hay que evaluar si en el pequeño ámbito de cada quien ese retorno sobre la inversión vale la pena.
    Si haces una evaluación pesada, es muy probable que necesites una capa que lo convierta a SAT o a otro modelo numérico. En ese mundo, los números son la abstracción. Si intentas hacerlo de otra forma, casi seguro vas a sufrir. Conviene tener una capa que traduzca el problema a una formulación formal y el espacio de soluciones a un dominio; los tipos pueden ayudar ahí, pero demasiadas veces los “tipos” que realmente reciben atención no son ese tipo de tipos.

    • Soy el autor. Hay algo de cómo lo hacemos en Svix que mencioné apenas en un párrafo y debería haber explicado más.
      Gracias a bibliotecas como Serde y Pydantic, seguimos el enfoque de que la deserialización es validación. Validamos todos los datos JSON antes de convertirlos en estructuras del código.
      Es parecido al ejemplo de Redis: aunque recibamos JSON por la red, después de validarlo por completo, cuando llega al código podemos estar tranquilos de que es un tipo bien formado. Así que en el código podemos asumir que un tipo email es un email válido y que un tipo ID es un ID válido.
    • Aunque al mirar en profundidad nombres y direcciones ambos sean strings, aun así deberían usarse como tipos separados, no simplemente como strings.
      Mezclar un campo de nombre con un campo de dirección casi siempre es un error, y el sistema de tipos puede imponerlo.
    • ¿De verdad es tan caótico solo porque la mayoría de los datos viajan como JSON? En ciertos casos, Map también es un tipo perfectamente razonable.
  • Si un typo se convierte en un error en tiempo de ejecución, eso no es “moverse más rápido”; y si al cambiar la firma de una función tienes que hacer grep en la base de código para encontrar todos los puntos de llamada y rezar para haberlos arreglado todos, eso tampoco es “ser más productivo”
    Los tipos son buenos, pero todo en exceso causa problemas. Si te propones como meta de vida codificar toda la lógica de negocio en el sistema de tipos, terminas con un caos más incomprensible que no tener tipos en absoluto. Si el nombre de un tipo no cabe en una sola línea dentro de un mensaje de error, ya te fuiste demasiado lejos

    • He visto definiciones de tipos de TypeScript demenciales
      Para ser justos, eran para encajar con código JS puro antiguo, y esa pobre variable podía contener toda clase de valores
      Le estaré eternamente agradecido a TypeScript, pero no me sorprendería que en el futuro ese código aparezca en un paper sobre “la era en que los tipos fueron demasiado lejos”
    • Antes pensaba que, como con from pdb import set_trace: set_trace() podía hacer edición interactiva, era mejor interactuar con el programa en ejecución que con el compilador
      Pero en cuanto el programa se vuelve aunque sea un poco complejo, la cosa cambia. Cuando pasas datos entre sistemas con colas, usas asincronía, threads y multiprocesamiento, y empleas bibliotecas binarias compiladas en las partes críticas de rendimiento, al final terminas deseando haber escrito todo en Erlang
    • De verdad me da curiosidad cómo trabajan las personas que efectivamente hacen así los cambios de firma de funciones
      ¿Tienen alguna metodología general, como pruebas extremadamente estrictas con 100% de cobertura de código?
    • Cada vez que veo este tipo de quejas sobre el tipado estático, me pregunto cómo demonios diseñaron sus definiciones de tipos y su arquitectura para que esto sea un problema
      Si en tiempo de build o compilación no te detecta los puntos de llamada, entonces no estás usando tipado estático
    • No veo bien cómo esas dos cosas se conectan con la discusión sobre tipos
      Los typos pueden producir código incorrecto incluso en el lenguaje con tipado estático más fuerte. Si no, ¿qué significaría siquiera escribir código? ¿Qué es peor: un error en tiempo de ejecución, o obtener un resultado incorrecto sin errores?
      Ejecutar un proyecto de Python puede ser más rápido que compilar C++, y los lenguajes de tipado dinámico también pueden ofrecer mejores formas que grep para encontrar llamadas a funciones
  • Eso de que no usar tipos tiene la ventaja de acelerar el desarrollo tampoco coincide con mi experiencia. El tipado estático hace más rápida la programación diaria
    Más adelante se mencionó que los IDE mejoran gracias al tipado estático, pero también se siente en el REPL. Los errores de tipo detectados estáticamente dan mensajes mucho más significativos y cercanos a la causa raíz real, y permiten corregirlos antes que los errores en tiempo de ejecución
    También reduce la carga de tener que pensar con demasiado cuidado en los tipos. Como el compilador mantiene la disciplina, yo puedo preocuparme menos. Puedo avanzar más rápido con la confianza de que una gran categoría de errores se detecta de inmediato
    En mi experiencia, los sistemas de tipos estáticos son fáciles de usar, aceleran el desarrollo y aumentan la confiabilidad. Hasta ahora solo les he visto dos costos: pueden ser más difíciles de aprender y más difíciles de implementar

    • Totalmente de acuerdo. El debate sobre mantenibilidad prácticamente queda resuelto con esto
      Un desarrollador junior que contrates dentro de 6 meses va a tardar mucho más en adaptarse a código sin tipos
      Acepto que para algunas personas escribirlo por primera vez puede ser “más rápido”, pero todos los desarrolladores que lean ese código después van a ir más lento
    • También se podría argumentar que justamente ese pensamiento cuidadoso hace que el software sea más limpio y mejor
      Puede ser bueno pensar exactamente qué entra y qué sale, y por qué, en lugar de producir un revoltijo que simplemente pasa el compilador
  • Creo que el autor se equivoca en casi todos los puntos. Yo también pensé así durante décadas, pero en los últimos años cambié por completo de opinión
    ¿Los tipos reducen bugs? No. Tal vez un poquito, pero no de forma significativa. Basta ver los estudios al respecto
    ¿Los tipos dan una mejor experiencia de desarrollo? No. Mi REPL y mi IDE tienen todas las definiciones y variables. Puedo hacer autocompletado de todos los símbolos, árboles de llamadas, navegación de usos, refactorizaciones con confianza, y ejecutar, reemplazar o envolver funciones de forma aislada dentro del REPL y de la aplicación
    ¿Codificar todo en el sistema de tipos? Imposible. Se necesita validación en tiempo de ejecución
    Y buena suerte desarmando las definiciones de tipos cuando cambien los requisitos. Este es el golpe decisivo. El tipado estático congela demasiado pronto el modelo de datos del dominio tal como lo entiendes en ese momento. Ese modelo cambia, y si tienes mala suerte debes soportar varias variantes del modelo de dominio dentro del mismo runtime. Sufres aún más si usaste herencia
    Es cierto que el tipado estático le da al compilador una gran palanca para optimizar, pero entre los lenguajes de tipado dinámico también hay algunos que ofrecen tipado estático como función opcional
    En muchos casos de uso, especialmente en desarrollo enterprise, un lenguaje funcional de tipado dinámico y con inmutabilidad primero da grandes beneficios a largo plazo
    El error de categoría que cometen a menudo los defensores fuertes de los tipos es asumir que se escribiría el mismo código, solo que sin tipos. En la práctica, no se escribe así

    • Al final es un debate inevitablemente frustrante, porque cada quien extrae conclusiones completamente distintas de su propia experiencia
      En casi todos los puntos, mis conclusiones son justo las opuestas. Por supuesto que es cierto que se necesita validación en tiempo de ejecución, pero la mayor parte de esa validación se puede evitar
      Sobre los cambios de requisitos, creo que el tipado estático más bien facilita adaptarse. En los sistemas de tipado dinámico que he usado, los supuestos importantes sobre las estructuras de datos estaban dispersos por todas partes; a veces se verificaban dinámicamente como precondiciones o poscondiciones, a veces solo estaban en las pruebas, o directamente no se verificaban
      Para cambiar un requisito había que razonar sobre todos los efectos en esos supuestos implícitos, así que modificar daba miedo. Levantar la app con el código nuevo era fácil, pero saber si habías roto una ruta de código rara que no se te ocurrió era muy difícil
      Prefiero muchísimo tener una etapa de análisis estático que me diga: “cambiaste esta interfaz; ¿sabías que esta ruta de código dependía de esa parte?”. El tipado estático no es la única forma, pero me parece mucho menos pesado que contar con el mismo nivel de validación dinámica y pruebas
    • Está muy bien ver definiciones y variables en el REPL y el IDE para hacer autocompletado y refactorización, pero eso solo funciona cuando realmente estás ejecutando el código que quieres inspeccionar. En mi experiencia, ese enfoque no escala bien
      El sistema de tipos no elimina la validación en tiempo de ejecución, pero si se usa correctamente la reduce drásticamente
      Cuando cambian los requisitos, lo mejor es que el compilador te diga exactamente qué debes arreglar para que vuelva a funcionar. Si haces lo mismo en un lenguaje dinámico, tienes que rastrearlo a mano, esperar a que fallen las pruebas unitarias y rezar para que no se haya escapado ninguna ruta
    • A mí, por el contrario, esta parte me resultó mucho más fácil con tipado estático
      Podía encontrar con mucha más confianza todos los lugares donde se usaba un tipo específico y ver si cada uno requería cambios. En un entorno de tipado dinámico, esta tarea era mucho más minuciosa y engorrosa
    • Esa puede ser tu experiencia, pero no es la mía. Creo que no hay una respuesta correcta para todos. Elige lo que te funcione y sigue adelante
  • Como desarrollador que ha escrito cientos de miles de líneas en C++, Python y JS, tampoco lo tengo claro. No es tan evidente
    Soy productivo en los tres, pero Python suele ganar. Eso sí, no escribiría un motor de videojuegos ni un códec de video en Python
    JavaScript es inconsistente y raro, pero el legado de Netscape ya nos dejó a todos atados a él
    En estilos muy orientados a objetos y con muchas clases anidadas enormes, los tipos estáticos en tiempo de compilación/parsing pueden reducir muchos errores. Pero llegué a ver la orientación a objetos como algo en general cercano a un desastre, y las funciones simples junto con datos estructurados casi siempre ganan en simplicidad y mantenibilidad
    Los servidores de lenguaje e IDE modernos pueden detectar muchos errores de tipeo incluso al desarrollar en JS/Python. Soy exigente con muchas partes de la programación, pero nunca tuve una postura fuerte sobre tipado estático vs. dinámico. Ambos tienen millones de proyectos exitosos

    • C++, más allá de su sistema de tipos, es en general un lenguaje bastante áspero para ser productivo
      Creo que la mayor mejora de productividad que puede ofrecer un lenguaje es la recolección de basura. Me da más curiosidad cómo se compararía Go, con una sintaxis y tipos mucho más simples. Java también podría ser mejor. Aunque sea verboso, en la mayoría de las tareas su carga cognitiva es apenas una fracción de la de C++
    • En Python suelo programar con un estilo funcional, pero aun así los tipos ayudan mucho
      Cometo muchos errores de tipeo y también me equivoco seguido con el orden de los argumentos. Los tipos ayudan muchísimo, especialmente al hacer machine learning. Que el código de entrenamiento se caiga después de pasar 30 minutos procesando datos es de las cosas que más quiero evitar
      Veo el tipado gradual de Python como un muy buen punto medio entre prototipar rápido y agregar anotaciones de tipo cuando una función ya está lo bastante madura
    • Puede que te guste este video de WWDC de hace unos años sobre programación orientada a protocolos (Swift): https://www.youtube.com/watch?v=p3zo4ptMBiQ
  • La discusión sobre si el tipado fuerte es mejor que el tipado débil ya quedó resuelta, pero no está resuelto si el tipado estático es mejor que el dinámico.
    Quienes apoyan el tipado estático creen que el compilador debe verificar las invariantes de tipos para confirmar la “corrección”, y quienes apoyan el tipado dinámico lo ven como una pérdida de tiempo.
    Yo claramente estoy en el segundo grupo. Porque el compilador solo puede comprobar la corrección de tipos, no la corrección del programa. La corrección de tipos es necesaria para la corrección del programa, pero no es suficiente. Quienes defienden el tipado estático no logran admitir esto y creen equivocadamente que el tipado estático garantiza más de lo que realmente garantiza.
    Veamos el ejemplo de birthdayGreeting del texto. El autor se alegra de que el tipado estático detecte un bug en birthdayGreeting("John", "20") porque "20" no es un número. Pero no detecta birthdayGreeting(" ", 123). " " no es un nombre. Tampoco detecta birthdayGreeting("Anna," -12335). En cambio, sí detecta birthdayGreeting("Anna" 4.5), aunque 4.5 puede considerarse una edad, así que incluso se podría decir que está mal.
    Esto es importante. Los “bugs de tipos” son trivialmente fáciles de detectar, pero los bugs semánticos pueden permanecer ocultos durante años. Por ejemplo, un desbordamiento en el saldo de una cuenta almacenado como uint, un número que debería ser primo en cierta posición pero no lo es, o una lista que no debería estar vacía. Ni siquiera los tipos dependientes pueden garantizar esas invariantes.
    Si cuesta creerlo, basta buscar grandes bugs que hayan causado explosiones de naves espaciales o accidentes automovilísticos. Hasta donde sé, ningún caso tuvo como causa un verdadero error de tipos; la abrumadora mayoría fueron errores semánticos.
    [1] La mayoría no entiende que los tipos deben verse al menos en dos ejes, fuerte/débil y estático/dinámico, y sigue confundiendo tipado débil con tipado dinámico. C es de tipado estático y débil; Python es de tipado fuerte y dinámico; JavaScript es de tipado débil y dinámico.

    • Muchos de esos ejemplos se pueden detectar perfectamente con tipado estático, dependiendo del sistema de tipos. Pero lo más importante es la parte de que “los bugs de tipos son trivialmente fáciles de detectar”.
      Justamente por eso estoy del lado del tipado estático. Son tan triviales que se pueden manejar de forma declarativa, justo al lado del código que se revisa, con feedback inmediato, en todos los puntos de llamada y en cada subexpresión y sentencia.
      Las anotaciones de tipos no significan que la semántica o la lógica de dominio sean correctas; eso igual hay que probarlo. Pero pueden reemplazar decenas de pruebas triviales que son ortogonales a la lógica que te interesa. Francamente, casi nadie escribe todas esas pruebas sin omitir ninguna.
    • Soy el autor. Creo que estos ejemplos no son contraejemplos, sino que más bien muestran mi punto.
      Más adelante en el texto dije que se valida al crear tipos a partir de lugares como la entrada del usuario. Por eso el tipo Name siempre es válido y " " no es un nombre. Como el tipo garantiza un nombre válido, en nuestra base de código eso sí se detecta con certeza.
      birthdayGreeting("Anna" 4.5) y birthdayGreeting("Anna," -12335) son válidos en la práctica en JS porque number es de punto flotante. Aunque al escribir el texto tenía enteros en mente. Es otro caso donde un tipo más estricto que TS, por ejemplo Rust, ayuda a definir mejor las invariantes.
      En resumen, intentando mostrar un ejemplo simple no definí todos los tipos con la rigurosidad habitual, y como resultado quedaron más expuestos bugs que se habrían detectado con tipos.
    • Lo interesante de estos ejemplos es que, en mi opinión, lo que se necesita es un tipo Name que represente un nombre siempre válido y un tipo Age que represente una edad siempre válida.
      Pones la validación en un solo lugar, en los constructores de esos tipos, y varios métodos como birthdayGreeting pueden usar valores de esos tipos sin tener que hacerse responsables.
      No sé cómo implementar bien este patrón sin chequeo de tipos o, al menos, hints de tipos opcionales y análisis estático. En cambio, validar las entradas en cada método es demasiada carga, y tampoco me satisface asumir que quien llama pasa valores válidos y luego probar que no ocurra un desastre grande.
    • Al mencionar explosiones de naves espaciales, enseguida se me viene a la mente [1]. Este famoso accidente estuvo relacionado con chequeo de tipos con rangos permitidos definidos de antemano para algunos tipos, y también conecta con tu opinión. Hacer que birthdayGreeting acepte un rango de 1 a 150 es algo fácil de hacer en ADA.
      Entre los problemas públicos relacionados con naves espaciales, hay algunos que probablemente podrían haberse detectado con mejor chequeo de tipos. La conversión entre sistema métrico y sistema anglosajón también se puede resolver poniendo las unidades en los tipos. Aunque en el caso de [2], es muy probable que el problema haya sido más bien de pruebas de integración.
      Por supuesto, el chequeo de tipos no puede encontrar todos los problemas de código, especialmente los algorítmicos, ni reemplaza las pruebas. Aun así, durante el desarrollo, el feedback inmediato y los hints de tipos son muy valiosos.
      En el ejemplo también se podría cambiar la cadena de nombre por un tipo u objeto de persona.
      [1]: https://en.m.wikipedia.org/wiki/Ariane_flight_V88
      [2]: https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter
    • No entiendo por qué el hecho de que el compilador solo verifique la corrección de tipos, y no la corrección del programa, sería una razón para decir que es una pérdida de tiempo.
      No hay soluciones perfectas, pero sí hay muchas soluciones valiosas.
  • Hubo una convergencia considerable en este problema. Hoy la mayoría de los lenguajes ofrece cierto grado de inferencia de tipos a nivel de sentencia. C++ también tiene auto
    Gracias a eso, el boilerplate de tipos en el código se redujo mucho. Ya quedaron atrás los días en que había que escribir todos esos tipos largos de iteradores en los for de C++
    Las declaraciones de funciones y los campos de las estructuras son lugares donde se necesita información de tipos para leer el código. Cuando un programa supera unos cientos de líneas o hay más de un desarrollador, cierto nivel de anotaciones es indispensable
    La oposición principal viene, por supuesto, de usuarios de Python y JavaScript. Python incorporó después un sistema de tipos consultivo bastante extraño, y JavaScript incorporó después TypeScript. Ambos son sistemas de tipos agregados encima y se usan en entornos donde se mezcla código tipado y no tipado. Eso es doloroso
    LISP también incorporó después un sistema de tipos hace décadas con “flavors” y el Common LISP Object System, y eso tampoco fue bonito. La lección es que agregar un sistema de tipos a posteriori termina en un desastre

    • Creo que el sistema de tipos de Python es bastante bueno, considerando las circunstancias
      Tiene funciones útiles, como que Optional obligue a verificar antes de usar None, o el subtipado estructural mediante typing.Protocol. Habría sido mejor si Python se hubiera diseñado desde el principio pensando en tipos, pero si se considera la exigencia de integrarse con el código Python existente y no romper nada, lo hicieron bastante bien
      El problema más grande de los tipos estáticos en Python es el ecosistema y las convenciones. Esto se agrava especialmente porque muchos desarrolladores usan Python, en la práctica, como científicos de datos. Abusan de *args/**kwargs porque les da pereza escribir firmas de métodos correctas
      Es muy común que los métodos se pasen DataFrames o diccionarios como bolsas de cosas varias. Puntos extra si el método agrega o elimina columnas o campos, de modo que no sabes qué hay dentro de esa bolsa de datos hasta ejecutar el código o leer todas las líneas
      Por supuesto, se puede hacer algo parecido en casi cualquier lenguaje. En C# puedes usar dynamic para todos los tipos, o hacer que todos los métodos de Go reciban interface{}. Pero Python fomentó activamente este enfoque durante mucho tiempo, y aún hoy muchos tutoriales para principiantes presentan “si aceptas *kwargs, no tienes que cambiar la firma de la función” como una funcionalidad avanzada para gente inteligente, no como una trampa horrible
    • Los “sistemas de tipos” de Python y TypeScript fueron diseñados para introducir tipos de forma gradual y en entornos que no son greenfield
      Son indispensables para una migración gradual, y es totalmente comprensible que funcionen de esa manera
    • Flavors y CLOS no son “sistemas de tipos”. Son “sistemas de objetos”, y tampoco son particularmente feos
      Flavors se introdujo en Lisp cuando no tenía sistema de tipos, y luego CLOS se agregó a Common Lisp, un Lisp que ya tenía sistema de tipos
    • El sistema de tipos de TypeScript es realmente sorprendentemente bueno. Ojalá más sistemas de tipos fueran igual de expresivos
  • La gente siempre se ha vuelto fanática con aquello a lo que se aferra emocionalmente más que racionalmente
    La afirmación de que “los tipos reducen los bugs” es más algo que suena plausible que un hecho
    https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
    No es que hayan faltado intentos. Se podría decir que esa afirmación fue refutada
    Aun así, personalmente me gustan los tipos estáticos. Principalmente por su efecto de documentación y, quizá no sea casualidad, ese también es el único efecto positivo que sí cuenta con evidencia empírica sólida

    • Creo que no hay mucho margen para discutir que “los tipos reducen bugs al modificar una base de código de larga vida”
      Evitar regresiones puede ser más importante que escribir código correcto desde el inicio, y cuando uno mira código que no evoluciona, no puede evaluar esa parte
      En concreto, eliminar campos de objetos en un proyecto grande de JavaScript puro es, por naturaleza, un campo minado y en el pasado generó muchos bugs. En cambio, en un proyecto completamente en TypeScript se puede hacer el mismo cambio con confianza
    • Como es una afirmación muy fuerte, veamos un caso extremo: la parte intermedia de CompCert, verificada formalmente: https://users.cs.utah.edu/~regehr/papers/pldi11-preprint.pdf
      Creo que esto puede considerarse evidencia empírica de que los tipos estáticos muy fuertes reducen bugs
      Al final, como dicen muchos comentarios, los tipos no son una cuestión binaria de sí/no, sino un gran espectro con varios ejes: estático/dinámico, fuerte/débil, etc. También hay grandes diferencias entre sistemas de tipos, y grandes diferencias en la forma en que la gente aplica esos sistemas de tipos a los problemas
      Incluso en un lenguaje estático y fuertemente tipado puedes representar todo como strings y convertir constantemente, lo que en la práctica es trabajar como en un lenguaje de tipado dinámico. En cambio, si aprovechas las herramientas que te da el sistema de tipos para crear clases que representen valores válidos y afirmar invariantes importantes, puedes obtener beneficios
    • Prefiero mucho más los tipos estáticos fuertes porque hacen que el código sea autodocumentado
      La productividad no aumenta unas pocas veces, sino por órdenes de magnitud. Lo digo habiendo usado bastante varios lenguajes que cubren una zona amplia de este espectro, como C, C++, Java, Python, JavaScript, TCL, etc.
      Se vuelve mucho más fácil razonar sobre código que no has tocado recientemente, sea del proyecto actual o de una dependencia. No tienes que desviarte todo el tiempo para averiguar qué puedes hacer exactamente con el objeto que devolvió cierta función, así que puedes concentrarte más en el problema que tienes delante
      También está el agradable alivio cuando la compilación pasa, pero eso es secundario
    • El argumento a favor de los tipos estáticos es que, por ser estáticos, vuelven imposibles los bugs de tipos
      No hay apego emocional, salvo la indignación al enfrentarse al “¡pero no eliminan todos los bugs!”
    • ¿Cómo te sentirías si alguien afirmara que “los argumentos emocionales se pueden construir más rápido que los argumentos racionales”?
      Este debate se siente exactamente así
      Solo quiero la racionalidad simple de que el compilador me diga “no” cuando intento tratar un hashmap como si fuera Apple o String