1 puntos por GN⁺ 2025-10-05 | 1 comentarios | Compartir por WhatsApp
  • Se comparan las diferencias y características surgidas al resolver problemas de Advent of Code usando Ada y Rust
  • Se analizan las diferencias entre el diseño de ambos lenguajes, centrados en la seguridad y la confiabilidad, y sus formas reales de escribir programas
  • Las diferencias se hacen evidentes desde varios ángulos, como la biblioteca estándar de cada lenguaje, qué funcionalidades vienen integradas, el rendimiento y el estilo de manejo de errores
  • Se explican casos concretos encontrados al escribir y operar código real con ejemplos prácticos sobre modularidad, genéricos, bucles, manejo de errores y más
  • La experiencia de desarrollo muestra diferencias marcadas por el tipado estático, el manejo de arreglos y las interfaces de tratamiento de errores

Introducción y objetivo

  • Tras usar solo Ada para resolver problemas de Advent of Code (AoC), desde 2023 el autor también empezó a escribir soluciones en Rust y Modula-2, lo que permitió compararlos directamente
  • Al trasladar soluciones antes centradas en Ada hacia Rust, se hicieron palpables las diferencias estructurales y los enfoques propios de cada lenguaje
  • El objetivo es dejar en claro las diferencias prácticas de uso desde la perspectiva de la seguridad del código, la confiabilidad y el diseño del lenguaje

Versiones de lenguaje usadas en la comparación

  • Ada 2022 (con referencia parcial a algunas reglas de Spark 2014 cuando fue necesario)
  • Rust 2021 (comparación principal basada en Rust 1.81.0)

Funcionalidades excluidas y criterios de comparación

  • Las funciones más representativas de cada lenguaje (= killer features) se mencionan brevemente como comentarios dentro del texto
  • Algunas capacidades no se trataron, según la experiencia personal y las necesidades reales de cada solución
  • Se intenta, en la medida de lo posible, dejar de lado opiniones personales y enfocarse en las características principales

Contexto y perspectiva del autor

  • Como usuario no nativo tanto de Ada como de Rust, el autor parte de experiencia con lenguajes de los años 80 como C/C++, Pascal y Modula-2
  • Como resultado, el estilo de código puede diferir de los usos modernos o idiomáticos
  • Puede que no siempre se trate de la implementación óptima, y según el problema a veces se eligieron soluciones intuitivas o poco convencionales

Posicionamiento de Ada y Rust

  • Ada sigue siendo un lenguaje para sistemas y desarrollo embebido muy seguro y confiable, con énfasis en la legibilidad del código
  • Rust destaca por la seguridad de memoria y la programación de sistemas, y durante varios años ha sido nombrado como el “lenguaje más admirado” en la encuesta de desarrolladores de Stack Overflow
  • Ada, como lenguaje de alto nivel de propósito general, ofrece un espectro orientado a la lectura y el mantenimiento
  • Rust apunta al desarrollo de programas de sistema de bajo nivel y ha establecido una cultura de programación segura basada en gestión explícita de memoria y tipos de error/opción

Comparación de seguridad y características estructurales

  • Ada

    • Estándar ISO (especificación rigurosa)
    • Facilita definir tipos adecuados a las características del problema (rangos, cantidad de dígitos, etc.)
    • Permite índices de arreglos que no sean numéricos
    • Existe una especificación aún más estricta llamada Spark
  • Rust

    • La especificación está centrada en la documentación oficial (Reference) y el compilador
    • Las declaraciones de tipos dependen de tipos de máquina (por ejemplo, f64, u32)
    • El indexado de arreglos resulta natural solo con tipos numéricos

Resumen principal de la tabla de funciones/integración

  • Hay diferencias en soporte para verificación de rangos en arreglos, contenedores genéricos, concurrencia, bucles etiquetados y pattern matching
  • Ada usa manejo de errores basado en Exception (excepciones), mientras que Rust usa un enfoque de retorno mediante tipos Result/Option
  • Rust se diferencia especialmente por su soporte de macros, pattern matching y pureza funcional
  • Ada ofrece diseño por contrato y verificación en tiempo de compilación mediante DBC (Design By Contract) en Spark
  • En términos de seguridad de memoria, Rust y Spark la imponen, mientras que Ada permite el uso de punteros nulos

Comparación de rendimiento y tiempo de ejecución

  • Rust suele tener la reputación de ofrecer ejecución rápida pero compilación lenta, mientras que Ada tiene la reputación opuesta: compilación rápida y ejecución algo más lenta según las verificaciones activadas en tiempo de ejecución
  • Según los benchmarks, Rust sufrió overflow en el problema day24 por la limitación del tipo f64, mientras que Ada pudo usar tipos de alto nivel como digits 18, permitiendo selección automática del tipo de máquina y evitando overflow con muy buen rendimiento
  • Rust necesita usar un f128 no estable o bibliotecas externas, mientras que Ada puede obtener ventaja solo con declarar tipos adecuados a la especificación del compilador

Manejo de archivos y tratamiento de errores (Caso de estudio 1)

Manejo de archivos en Ada

  • Usa Ada.Text_IO por defecto
  • Permite abrir archivos de forma explícita, leer línea por línea y manejar líneas por rango o posición de forma relativamente intuitiva
  • Cuando ocurre un error, este se maneja mediante excepciones en lugar de mensajes explícitos, y la posibilidad de error no aparece en la firma de la función

Manejo de archivos en Rust

  • Usa std::fs::File y BufReader
  • Al abrir un archivo, devuelve un tipo Result, dejando clara la posibilidad de error
  • No ofrece acceso directo a índices de caracteres; debe procesarse necesariamente con Iterator
  • Se apoya principalmente en herramientas funcionales e iterativas como map, filter, collect, sum, además de varias macros (por ejemplo, include_str!)
  • Al declarar explícitamente los errores en el tipo de retorno, se logra una propagación de errores clara a nivel de función

Modularidad y genéricos (Caso de estudio 2)

Modularidad en Ada

  • Se basa en packages, con una separación clara entre especificación (interfaz) e implementación
  • Para reforzar la modularización, combina subpackages y sintaxis use/rename para ajustar la legibilidad
  • Soporta generic en packages: permite generalizar tipos, constantes e incluso subpackages completos

Modularidad en Rust

  • Organiza módulos con el sistema mod/crate, y la distinción entre especificación e implementación se automatiza mediante el generador de documentación
  • Usa control de acceso pub/private con permisos declarativos
  • Combina use/as para importar y renombrar
  • Tiene soporte integrado para pruebas, lo que permite declarar módulos de test directamente en el código, compilarlos y ejecutarlos automáticamente

Genéricos

  • Ada solo admite genéricos a nivel de package/procedure (no para tipos por sí solos)
  • Rust sí permite aplicar genéricos al tipo mismo (basado en plantillas)
  • Ada puede expresar con claridad propiedades adicionales como rangos de tipos mediante tipos de rango y subtipos; Rust recurre al uso de constantes de instancia

Comparación de tipos enumerados (Caso de estudio 3)

  • Ada ofrece declaraciones concisas y además soporte automático para uso discreto, ordenado y en bucles/índices
  • Los enum de Rust tienen una declaración similar, pero su uso con pattern matching e iteración requiere un enfoque más explícito

Conclusión

  • Ada ofrece un control más estricto en tipos de especificación de alto nivel, capacidad de verificación y comprobaciones en ejecución
  • Rust tiene ventaja en estilo de programación funcional, metaprogramación con macros y manejo de errores asistido por el compilador, tanto en facilidad de desarrollo como en seguridad
  • En la resolución práctica de problemas, Ada muestra fortalezas en compatibilidad con código antiguo y mantenimiento, mientras que Rust destaca por su ecosistema moderno de herramientas y su soporte para seguridad y paralelismo

1 comentarios

 
GN⁺ 2025-10-05
Opiniones en Hacker News
  • Aunque Ada tenía muchísimas ideas realmente buenas, da pena que en su mayoría solo se haya usado en áreas donde la seguridad es extremadamente importante. En particular, la capacidad de restringir el rango de valores de los tipos numéricos es muy útil para prevenir ciertos bugs. Spark Ada era fácil de aprender y también de aplicar en el desarrollo de software que cumple con SIL 4 (el estándar más estricto de seguridad de software). Durante las últimas décadas, la industria del software se fue por el camino de “crecer primero, la estabilidad después”, pero ahora se siente que hay una tendencia de regreso hacia el desarrollo de software seguro. Ojalá las lecciones acumuladas sobre seguridad hubieran llevado a lenguajes mejores. En la práctica, muchas buenas ideas solían quedar ocultas y desaparecer dentro de lenguajes minoritarios
    • Cuando llevas mucho tiempo desarrollando software, te das cuenta de que realmente se reinventa la rueda muchísimo. Ada y Rust se parecen en que ambos persiguen la seguridad, pero su definición y alcance son distintos. Rust persigue con muchísima intensidad una forma muy enfocada de seguridad importante, mientras que Ada tiene una definición de seguridad más amplia y concreta. Cuando aprendí Ada a principios de los 90, la crítica más común era que el lenguaje era demasiado grande y complejo, y que eso ralentizaba el desarrollo (en ese tiempo, un compilador certificado de Ada 83 costaba unas 20 mil dólares actuales por persona). Pero los tiempos cambiaron, y ahora todos reconocen que un lenguaje grande y complejo como Rust es necesario para una programación concurrente segura real
    • Nim también soporta subrange, inspirado en Ada y Modula, para limitar el rango de valores de un tipo
      type
        Age = range[0..200]
      
      let ageWorks = 200.Age
      let ageFails = 201.Age
      
      Al compilar aparece un error indicando que 201 no puede convertirse al tipo Age
      Enlace a la explicación de Nim Subranges
    • Ada (al menos en GNAT) soporta análisis de unidades físicas/dimensionalidad en tiempo de compilación, es decir, verificación de unidades. En ingeniería es algo muy práctico, así que cuesta entender por qué otros lenguajes solo ofrecen una función tan importante mediante librerías de terceros
      Documentación relacionada
    • En C++ también se pueden crear fácilmente con código tipos numéricos con rango de valores restringido (no está en la biblioteca estándar, pero implementarlo uno mismo es muy sencillo). Algunas verificaciones de seguridad pueden hacerse en tiempo de compilación en vez de en runtime. Sería bueno que todos los lenguajes soportaran esto de forma estándar
    • Lo que más me gustó de Ada fue su enfoque claro hacia la programación orientada a objetos (OOP). La mayoría de los lenguajes meten los conceptos de OOP en un solo paquete llamado “clase”, pero Ada te permite elegir y aplicar por separado paso de mensajes, despacho dinámico, subtipado, genéricos, etc. Me gustó mucho la forma en que cada una de esas funciones se combina elegantemente
  • El autor menciona diferencias como que Ada tiene una especificación oficial y Rust no, pero para los usuarios en la práctica importa más que eso la adopción/ecosistema del lenguaje (tooling, librerías, comunidad). Ada ha sido exitoso en áreas como aeroespacial/seguridad y sí encaja bien para AOC o trabajo embebido de bajo nivel, pero en proyectos reales del mundo real (sistemas distribuidos, componentes de SO, etc.) pesan mucho factores como formatos de datos, protocolos, soporte de IDE y colaboración con colegas. Al final, cuando eliges un lenguaje por primera vez, esos aspectos del entorno son decisivos
    • Recientemente Rust también recibió de Ferrocene una especificación inspirada en el estilo de especificación de Ada. Está pública, así que se puede consultar
      Especificación de Rust
    • Tanto Rust como Ada son débiles como especificaciones “formales” en el sentido estricto del término (documentos que puedan probarse mecánicamente). Incluso Spark Ada se apoya en supuestos semánticos del lenguaje, y aun así eso no está completamente formalizado ni en una forma legible por máquina
    • Incluso los desarrolladores de software de control de aeronaves responderían: “si no es algo importante en un entorno real, entonces sí, nuestro proceso probablemente sea excesivo”. De hecho, en los campos donde la seguridad es crítica, un lenguaje y proceso estrictos como los de Ada son más bien lo estándar
  • Me impresionó que, aunque Ada tenga menos funciones relacionadas con tipos que Rust, en legibilidad del código Ada a veces sale mejor parado. En la comparación faltó mencionar la velocidad del compilador; eso de que Ada se sentía como un lenguaje complejo era cosa del pasado, y hoy comparado con Rust quizá ya no sea necesariamente así. Este artículo me dio ganas de probar Ada en un proyecto de verdad
    • Me pregunto qué significa exactamente “desventajas relacionadas con tipos”. Según mi experiencia, el sistema de tipos de Ada es increíblemente expresivo. Puedes tener tipos definidos por el usuario con rangos de valores, arreglos indexables por enumeraciones arbitrarias, operadores definidos por tipo, y añadir a los tipos varias funciones complementarias como verificaciones en compilación/runtime, precondiciones y postcondiciones. También tiene registros discriminados y cláusulas de representación de estructuras. Más que una desventaja, es una función potentísima
  • Quiero hablar de la diferencia entre las cadenas en Ada y Rust. Cuando Ada se diseñó a inicios de los 80, tomó los arreglos de caracteres (char) como “cadenas”, así que es fácil indexarlos como simples arreglos de bytes. Rust, en cambio, fue diseñado con conciencia de Unicode desde el principio, por lo que sus cadenas son UTF-8 codificado, o sea, verdadero “texto”. Por eso Ada permite indexación aleatoria como arreglo, mientras que en Rust el concepto de cadena es distinto y siempre queda la opción de convertirla a un arreglo de bytes
    • Las cadenas Unicode integradas de Ada normalmente son arreglos UTF-32. A diferencia de Rust, no ofrece literales UTF-8 directamente, y hay que convertir desde arreglos de 8/16/32 bits
    • En Rust también se puede indexar una cadena. Solo que Rust no trata las cadenas como arreglos comunes, sino que normalmente usa slices de subcadenas. Si indexas cortando a la mitad de un carácter, ocurre un panic (en los casos en que violas los límites de un valor codificado Unicode). Si, como en AoC, siempre usas solo ASCII, entonces lo correcto es usar un slice de bytes con [u8] o el método str::as_bytes
  • Me pareció rara la afirmación del autor de que Rust “no soporta programación concurrente de forma nativa”. Rust tiene funcionalidad de threads integrada en el lenguaje y, de hecho, es más fácil de usar que async. Solo se vuelve problemático cuando necesitas tantísimos threads que topas con límites de recursos; para la gran mayoría del software, los threads built-in son suficientes
    • (Como alguien que no usa Rust, lo pregunto con genuina curiosidad) Quisiera saber cómo difiere en Rust el manejo de cancelación entre threads y async, y en qué se diferencia del async de otros lenguajes. En C++, Python y C#, el manejo de cancelación en async me pareció mucho mejor que con threads. He oído que en Rust, como esta cancelación/interrupción no se maneja con excepciones, puede ser incluso más difícil; me interesa saber cómo es eso en experiencia real de trabajo. También me gustaría escuchar cómo es este manejo de interrupciones en Ada
    • Tengo curiosidad por saber en qué punto un scheduler con work stealing como Tokio resulta realmente más rápido que simplemente correr varios threads. Me parece algo parecido a cómo un arreglo simple (por ejemplo, VecMap) puede ser rápido cuando hay pocos elementos, pero una vez pasas cierto número otra estructura de datos se vuelve más eficiente. Me pregunto a partir de qué punto el work stealing realmente compensa
    • En la práctica, la razón principal para usar Async es que el crate de terceros que usas es Async. (Por ejemplo: Reqwest necesita Tokrio) Si insistes en usar solo no-Async en desarrollo de aplicaciones de alto nivel, tarde o temprano te topas con límites
    • En plataformas con soporte débil para threads (WASM, embebidos, etc.), Async puede ser más apropiado. Hablar como si decenas de miles de personas fueran a entrar al mismo tiempo a un blog parece poco realista; más bien, defender la necesidad de Async con ese tipo de escenarios suena exagerado
  • Me pareció interesante que Ada también tenga compiladores open source. Antes pensaba que solo había compiladores propietarios, así que ni siquiera le presté atención a Ada, pero voy a tener que revisarlo otra vez
    • El compilador GNAT existe desde hace más de 30 años, y en algún momento hubo la confusión de que, al no tener excepción de runtime GPL, los binarios compilados también debían ser GPL, pero actualmente ese problema ya quedó resuelto
    • GNAT se construyó sobre GCC desde los 90, y en algunas universidades se usaba directamente en cursos prácticos, como programación en tiempo real. También recuerdo haber intentado usar Ada como lenguaje de introducción a la programación, antes de cambiar pronto a Pascal y C++
  • Entre los proyectos del área de impresión 3D que me llamaron la atención, recientemente hubo uno llamado Prunt, una placa de control y firmware para impresoras. Desarrollan el firmware en Ada, una elección bastante inusual pero que conceptualmente encaja muy bien
    Sitio de Prunt
    Prunt GitHub
  • Al final del Case Study 2 dice “si el cliente necesita conocer SIDE_LENGTH, agrega una función que lo devuelva”, pero en vez de una función sería más directo usar una declaración de constante como pub const SIDE_LENGTH: usize = ROW_LENGTH;
  • No estoy de acuerdo con la afirmación de que ambos lenguajes fomentan la programación centrada en stack. Ada más bien recomienda activamente la asignación estática
  • Me sorprendió que se presentara como una gran ventaja que los índices de arreglos en Ada puedan ser de tipos arbitrarios. Casi todos los lenguajes tienen diccionarios (hash maps) en la biblioteca estándar, y Rust ofrece dos
    • Aquí se habla de arreglos integrados en el lenguaje. Por ejemplo, en Ada puedes hacer que el índice de un arreglo “eggs” sea del tipo BirdSpecies; entonces eggs[Robin] y eggs[Seagull] tienen sentido, pero eggs[5] no está permitido. En Rust también puedes crear la estructura de datos que quieras (por ejemplo implementando Index<BirdSpecies>), y eggs[Robin] funciona pero eggs[5] da error. La diferencia es que Rust no lo soporta directamente como “arreglo” a nivel del lenguaje. Esto se vuelve especialmente valioso cuando, como en Ada, “un tipo definido por el usuario puede declararse como un entero que es subconjunto de otro”. En Rust todavía no se pueden crear enteros con rango restringido como tipos puramente definidos por el usuario (solo existen internamente cosas como NonZeroI16). Sería buenísimo que Rust llegara a soportar eso
    • Ada también trae soporte básico para hash maps y conjuntos. Estándar relacionado con contenedores en Ada (ver sección A.18). La ventaja de poder usar para índices de arreglos rangos típicos de “valores continuos” (por ejemplo, 0~N-1) es que, en situaciones donde importa un mapa denso o el acceso a memoria contigua, eso es mucho más rápido que un diccionario y además mejor para la caché
    • La restricción del tipo de índice de un arreglo en Ada (subtype) es un concepto estructuralmente completamente distinto de un diccionario. A nivel del lenguaje, incluso puedes restringir qué clase de valores son válidos como índices del arreglo