Bienvenido a nuestro segundo mes temático: Mechanical March. Este mes nos centramos en los lenguajes de sistemas, esos que se compilan a código máquina.
Esto es en parte una entrada y en parte la transcripción del vídeo de Mechanical March. Te haré una breve introducción al mes y después veremos un poco los lenguajes de sistemas, su evolución a lo largo de la historia, las ventajas y desventajas de compilar a código máquina y un rápido repaso de cada uno de los lenguajes destacados. Me acompaña de nuevo Erik, que será quien hable en la mayor parte de la segunda mitad. Pero empezaré yo con un poco de la información práctica.
Así que, en primer lugar, los lenguajes destacados de este mes. Son C, C++, D, Go, Nim, Rust, V y Zig. Para conseguir la insignia de Mechanical March tienes que completar cinco ejercicios en uno de esos lenguajes. Nuestro track de Go tiene uno de los mejores temarios de Exercism, así que te recomiendo mucho que lo pruebes. Además, aquí en Exercism somos muy fans de Nim, porque es un lenguaje relativamente sencillo con el que empezar y muy fácil de escribir, así que sin duda te recomiendo que también lo pruebes.
También tenemos cinco ejercicios destacados para probar:
- linked-list o simple-linked-list (según el lenguaje): asignar y liberar memoria, punteros
- secret-handshake: operaciones bit a bit
- pangram: bucles
for, strings y caracteres - sieve: arrays, bucles
for - binary-search: arrays, bucles
Hay una nueva insignia que anuncié en el vídeo de novedades que hice hace unos días, y que se consigue completando los cinco ejercicios destacados en los lenguajes temáticos. Así que, para conseguir esa insignia, tendrás que completar todos esos ejercicios en algún lenguaje de sistemas en algún momento del año.
También tenemos un montón de cosas divertidas en marcha: entrevistas con un par de personas del equipo principal de Go y, con suerte, también de Rust y de algunos de los otros lenguajes. Además, haremos muchas retransmisiones en directo a lo largo del mes. ¡Y pronto llegarán también artículos de merchandising de Mechanical March!
Así que vamos a profundizar un poco más en la parte técnica.
¿Para qué se usan estos lenguajes?
Pues los usamos en todo Exercism. Nuestra CLI está escrita en Go, nuestra herramienta interna de gestión de tracks llamada configlet está escrita en Nim, y la biblioteca clave que cuenta las líneas de código de tus soluciones está escrita en Rust. Erik, ¿por qué elegimos esos lenguajes para esas partes de las herramientas?
CLI:
- Creo que usamos Go porque era con lo que Katrina estaba más familiarizada.
- Go es genial para estas herramientas de línea de comandos más bien pequeñas.
- El código de Go es relativamente sencillo, lo que facilita las contribuciones.
- Los binarios de Go son fáciles de desplegar porque no necesitan un runtime.
- Go hace bien la compilación cruzada.
Nim
- Nim tiene casi las mismas ventajas que Go.
- No teníamos mucha gente que conociera Go y pudiera mantener la CLI, así que nos pasamos a Nim.
¿En qué otros tipos de sitios esperarías encontrar estos lenguajes?
- En cualquier sitio donde el rendimiento sea importante (controladores, juegos, sistemas operativos, sistemas de compilación o compiladores).
- En cualquier sitio donde los recursos sean limitados (por ejemplo, software embebido).
- En cualquier cosa que necesite ser muy portable, es decir, que se ejecute en muchas plataformas distintas.
¿Qué es el código máquina?
Como decía antes, lo que distingue a estos lenguajes de Mechanical March es que compilan a código máquina. ¿Puedes explicar un poco qué es el código máquina y, en contraste, qué es el bytecode?
- El código máquina es código que puede ejecutarse directamente en la máquina.
- El bytecode, en cambio, necesita otro código que lo interprete o lo compile a código máquina. Por lo tanto, el bytecode necesita un paso intermedio antes de poder ejecutarse.
¿Cuáles son las ventajas y desventajas del código máquina frente al bytecode?
Ventajas:
- Arranque más rápido (sin paso de compilación JIT).
- Menor consumo de memoria (no se carga un runtime, no hay bytecode en memoria, ideal para sistemas embebidos).
- La máquina de destino no necesita tener instalado un runtime (importante para mantener pequeños los contenedores de Docker).
Desventajas:
- No es portable. El bytecode es portable, pero el código máquina compilado es específico de una plataforma.
- No se pueden hacer optimizaciones avanzadas como la optimización guiada por perfiles (determinar la mejor manera de compilar el (byte)código después de haberlo ejecutado durante un rato).
Nota: existen enfoques híbridos, en los que el lenguaje compila a bytecode y luego usa otra herramienta para compilar ese bytecode a código máquina.
La evolución de la programación de sistemas
Vale. Vamos a ver un poco la evolución de algunos de los lenguajes de este mes. Para empezar, empecemos por C y por cómo evolucionó C++ a partir de ahí. Cuéntanos un poco sobre esos lenguajes.
C es un lenguaje de muy bajo nivel. Da la sensación de que estás solo un poquito por encima del código máquina. Eso lo hace muy potente y muy optimizable, pero también algo propenso a errores (por ejemplo, excepciones de puntero nulo y desbordamientos de búfer). La gestión de memoria es totalmente manual y, por tanto, responsabilidad de quien programa, lo que puede dar lugar a errores o fugas de memoria. C++ es como C, pero con soporte de programación orientada a objetos. Sigue siendo bastante de bajo nivel y te obliga a gestionar la memoria a mano. ¡Tanto C como C++ te permiten escribir ensamblador en línea (ASM)!
Y ¿qué pasa con los lenguajes de sistemas más nuevos? ¿Cómo han evolucionado?
Todos los lenguajes de programación de sistemas modernos admiten la gestión automática de memoria, ya sea mediante conteo de referencias, un recolector de basura o algún otro mecanismo.
Todos los primeros lenguajes de programación de sistemas admitían punteros nulos (a lo que Tony Hoare llamó su «error de mil millones de dólares»). Son famosos por provocar errores en tiempo de ejecución y vulnerabilidades. Muchos lenguajes modernos eliminan el null o, al menos, obligan a esforzarse para usarlo.
Otro cambio es el paso de que los valores mutables sean lo predeterminado a que lo sean los valores inmutables. Por ejemplo, tanto Rust como VLang tienen valores inmutables por defecto, y hay que optar explícitamente por la mutabilidad.
Todos los lenguajes más nuevos tienen soporte para interoperar con C (o C++), porque se ha escrito muchísimo código en esos lenguajes.
Otra cosa interesante es que algunos de los lenguajes más nuevos no compilan directamente a código máquina, sino que usan otras herramientas para hacerlo. Por ejemplo, Rust y Zig usan LLVM, mientras que Nim permite usar una gran variedad de compiladores. Esto se conoce como transpilación.
Y ¿qué hay de cosas como las macros y la metaprogramación?
Hay una división interesante en cuanto a las macros y la metaprogramación. Las macros de C/C++ son potentes, pero tienen cierta fama dudosa de ser difíciles de manejar. Rust, Nim y D ofrecen una potente metaprogramación, pero lo hacen de una forma mucho más agradable. En cambio, VLang y Zig mencionan explícitamente que el hecho de no tener macros es una característica de su lenguaje, y Go tiene un enfoque distinto con go generate.
Los lenguajes de sistemas tienen fama de ser bastante de bajo nivel. ¿Sigue siendo justo eso?
Los lenguajes más nuevos funcionan con niveles de abstracción más altos que C/C++. Por ejemplo, Rust, D y Nim también permiten una forma muy funcional de escribir código. Nim y D incluso tienen el concepto de funciones «puras».
Introducción a los lenguajes del mes
Así que estaría bien ver cada uno de los lenguajes por turnos. Todos los lenguajes tienen similitudes: todos tienen tipado fuerte y estático. Pero veamos en qué se diferencian. ¿Empezamos por C?
C
- Desarrollado por Dennis Ritchie
- Uno de los lenguajes más antiguos y, probablemente, el más usado del mundo
- Hay muchísimo software escrito en C, como Unix y Linux
- Muy influyente (piensa, por ejemplo, en la existencia de los lenguajes al estilo de C)
- Gestión manual de la memoria
- Muy eficiente (cerca «del metal»)
- Se ejecuta en cualquier sitio
- Perfecto para sistemas embebidos
- Un lenguaje bastante pequeño
C++
- Desarrollado por Bjarne Stroustrup
- Sucesor de C, pero con orientación a objetos añadida (C con clases)
- Ayudó a popularizar la programación orientada a objetos
- Más características de lenguaje de alto nivel que C
- Admite programación genérica mediante plantillas
- Añade soporte de módulos mediante espacios de nombres
- Muchos juegos (y motores de juego) están escritos en C++, y grandes partes de Windows también
- Gestión manual de la memoria
- Sigue evolucionando, con nuevas características que se añaden con regularidad (una especificación enorme)
D
- Desarrollado por Walter Bright y, más tarde, con la incorporación de Andrei Alexandrescu
- En origen se planteó como un C++ rediseñado (aprendiendo de sus «errores»), y se inspira en muchos otros lenguajes
- Multiparadigma: admite programación imperativa, orientada a objetos y funcional
- Interoperabilidad sencilla con C/C++
- Sintaxis uniforme de llamada a función
- Evaluación de funciones en tiempo de compilación (por ejemplo, generar una máquina de estados de Regex en tiempo de compilación)
- Admite programación funcional y funciones «puras»
- Muchas características de seguridad
- Seguridad de memoria mediante @safe
- Contratos (precondiciones y poscondiciones, invariantes)
- Funciones puras
- Enfoque en las pruebas unitarias, con las pruebas junto al código fuente que prueban (lo que obligó a hacer una excepción en la web de Exercism :))
- Unicode
Rust
- Desarrollado por Graydon Hoare, empleado de Mozilla Research, y adoptado después oficialmente por Mozilla; ahora forma parte de la Rust Foundation
- Multiparadigma: admite programación orientada a objetos (pero con una postura marcada, por ejemplo, sin herencia), imperativa y funcional (tipos Option/Result, coincidencia de patrones)
- Muchas herramientas nuevas están escritas en Rust (por ejemplo, SWC, pero también Gleam, y es el segundo lenguaje admitido en el kernel de Linux; dependemos de él para el contador de líneas de código)
- Centrado en la fiabilidad y el rendimiento
- Construido sobre LLVM
- El lenguaje más querido en la encuesta de StackOverflow durante los últimos 7 años
- Rápido, en parte gracias a un núcleo y una biblioteca estándar mínimos
- Seguro: seguridad tanto de memoria como de hilos gracias al modelo de propiedad y a los tiempos de vida; inmutable por defecto
- Sistema de tipos potente, que detecta muchos errores en tiempo de compilación (especialmente los relacionados con la memoria). El compilador muestra errores realmente útiles
- Todo incluido: compilador, herramienta de compilación, formateador, gestor de paquetes e integraciones con el IDE
- Una documentación estupenda (incluso hay un documento enorme sobre cómo funciona el compilador)
- Portable: compila a un único binario estático y no requiere que haya un runtime instalado
- Interoperabilidad sencilla con código C
- Abstracciones sin coste
- Concurrencia sin miedo
- Macros
Nim
- Desarrollado por Andreas Rumpf (originalmente llamado Nimrod)
- Sintaxis inspirada en Python
- Multiparadigma
- Se usa en Exercism, en configlet
- Gran rendimiento: iteradores sin sobrecarga, preferencia por la asignación en la pila de tipos basados en valores
- Sistema de tipos moderno y expresivo: inferencia de tipos, tuplas, genéricos, tipos suma, async/await
- Recolector de basura, pero con soporte de gestión de memoria determinista (varias opciones de gestión de memoria)
- Ejecución de código en tiempo de compilación
- Sintaxis uniforme de llamada
- Macros: es fácil ampliar el lenguaje
- Sistema de efectos: codificar los efectos secundarios en el sistema de tipos
Go
- Desarrollado por Robert Griesemer, Rob Pike y Ken Thompson en Google
- Mayormente imperativo y procedimental, con un enfoque parecido a la orientación a objetos (pero sin herencia)
- Se usa en proyectos grandes como Docker y Kubernetes. También es ideal para backends y CLIs (por ejemplo, esbuild)
- Pretende ser lo bastante sencillo como para tenerlo todo en la cabeza (poca sintaxis)
- Seguridad de memoria gracias al recolector de basura
- Rápido: compilación rápida, pruebas rápidas y ejecución rápida. ¡Incluye soporte para escribir benchmarks!
- Con una postura marcada: se dedica mucho esfuerzo a influir en el estilo del código Go: poca sintaxis, formateo del código con
go fmt, herramientas para comprobar si el código es idiomático, errores en cosas que en otros lenguajes son solo advertencias (por ejemplo, variables sin usar), documentación que enumera los modismos de Go que hay que seguir - Sistema de tipos ligero que hace que Go sea muy flexible (admite inferencia de tipos)
- Portable: compila a un único binario estático y no requiere que haya un runtime instalado. Fácil compilación cruzada. Concurrencia mediante goroutines (ligeras) y comunicación mediante canales
- Tipado estructural mediante interfaces (parecido al duck typing, pero comprobado de forma estática)
- Manejo de errores: el lenguaje anima a comprobar los errores y a gestionarlos
VLang
- Desarrollado por Alexander Medvednikov y Delyan Angelov
- Inspirado en Go:
- La misma estrategia de «solo hay una forma de hacer las cosas»
- La misma estrategia de «sintaxis mínima»
- Corrutinas
- Distinto de Go:
- Sin nil/null, pero usa un tipo resultado
- Inmutable por defecto
- Tipos suma (funcional)
- Interpolación de strings
- Runtime y binarios más pequeños
- Coincidencia de patrones
- Trabajando para que el recolector de basura sea opcional (autofree)
- Interoperabilidad con C sin coste
- Generación de documentación a partir del código
- Compilador rápido que usa poca memoria
Zig
- Desarrollado por Andrew Kelley
- Bastante poca sintaxis (un archivo de gramática PEG de 500 líneas)
- Ejecución de código y reflexión en tiempo de compilación
- Pretende ser «obvio»: sin flujo de control oculto, sin asignaciones ocultas ni macros o metaprogramación
- Asignación manual de memoria
- Admite distintos asignadores
- Las funciones de la biblioteca estándar que asignan memoria tienen un parámetro de asignador
- El framework de pruebas puede detectar fugas de memoria
- Seguridad:
- Los errores son valores y hay que gestionarlos
- Sin null, usa un tipo opcional
- Las pruebas pueden escribirse en el mismo archivo que el código fuente (como en D)
- Usa LLVM como backend
- Puede compilar código C/C++
- Interoperabilidad sencilla con C
- Compilación cruzada sencilla
Conclusión
Vamos a dejarlo aquí, porque imagino que a estas alturas tienes la cabeza bastante llena.
Espero que te haya parecido una introducción útil y divertida a los lenguajes destacados de este mes. Espero que te lo pases genial explorando estos lenguajes. A Erik y a mí nos encantaría saber cuáles eliges y qué te parecen, así que no dudes en escribir en los comentarios o en el foro.
¡Gracias por vernos!