Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la familia entera. El mismo problema de la clase —etiquetar la versión que se despliega— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del núcleo.
Si entendiste la versión de Python, la de Ruby te resultará familiar aunque no la hayas visto nunca. Ese reconocimiento es exactamente lo que este curso quiere producir.
⚠️ Qué está verificado y qué no. Ruby, Perl y Lua se ejecutan en CI contra el mismo
casos.jsonque el núcleo, igual que las diez implementaciones de la clase (workflow Labs). Los otros 17 primos son material de lectura: su toolchain no está en el workflow, así que están escritos para ser correctos pero sin el sello de la máquina. Verificar tres de veinte no es verificarlos todos.
mayor.menor.parchedesplegado=v<versión>v| stdin | esperado |
|---|---|
1.2.3 |
desplegado=v1.2.3 |
0.9.0 |
desplegado=v0.9.0 |
2.1.5 |
desplegado=v2.1.5 |
Representantes del núcleo: Python · PHP. Lo que se despliega es el código fuente tal cual: no hay artefacto intermedio, y por eso la máquina de destino necesita el intérprete instalado.
version = STDIN.gets.strip
puts "desplegado=v#{version}"
use strict;
use warnings;
chomp(my $version = <STDIN>);
print "desplegado=v$version\n";
local version = io.read("l"):gsub("%s+$", "")
print("desplegado=v" .. version)
gets stdin version
puts "desplegado=v[string trim $version]"
version <- trimws(readLines("stdin", n = 1))
cat(sprintf("desplegado=v%s\n", version))
Qué reconocer: los cinco son una interpolación de cadena y poco más —#{}, $version, ..,
[], sprintf—, la misma operación que hace el f-string de Python. La consecuencia para el
despliegue es lo que los une de verdad: entregas archivos, no un programa. El servidor de destino
tiene que traer Ruby, Perl, Lua, R o Tcl de la versión adecuada, y si no coincide con la de
desarrollo el fallo aparece en producción, no en el build. De ahí que estas comunidades vivan de
gestores de versión (rbenv, rvm, renv) y de contenedores: la imagen es la forma práctica de
empaquetar el intérprete junto al código y recuperar la propiedad de que lo que se prueba es lo
que se despliega.
Representantes del núcleo: JavaScript · TypeScript.
import 'dart:io';
void main() {
final version = stdin.readLineSync()!.trim();
print('desplegado=v$version');
}
// ActionScript no tiene stdin: el artefacto era un .swf y el "despliegue" consistía
// en subirlo a un servidor web. Se ilustra solo la regla de etiquetado.
package {
public class Despliegue {
public static function etiqueta(version:String):String {
return "desplegado=v" + version;
}
}
}
Qué reconocer: la interpolación $version de Dart es la misma plantilla de JavaScript con otro
delimitador. Lo interesante es que Dart elige su artefacto según el destino: dart compile exe
produce un binario nativo, dart compile js produce JavaScript para el navegador y Flutter produce
un paquete móvil, todo desde el mismo fuente. TypeScript hace algo parecido a menor escala: nunca se
despliega .ts, se despliega el .js transpilado. ActionScript ilustra el final del camino
contrario: su artefacto .swf dependía de un reproductor propietario, y cuando el reproductor
desapareció el código dejó de ser desplegable aunque siguiera siendo correcto.
Representante del núcleo: Java. El artefacto es un .jar portable: el
mismo archivo corre en Linux, macOS y Windows, a cambio de exigir una JVM instalada en destino.
fun main() {
val version = readLine()!!.trim()
println("desplegado=v$version")
}
object Despliegue extends App {
val version = scala.io.StdIn.readLine().trim
println(s"desplegado=v$version")
}
def version = System.in.newReader().readLine().trim()
println "desplegado=v$version"
(require '[clojure.string :as str])
(println (str "desplegado=v" (str/trim (read-line))))
Qué reconocer: los cuatro interpolan sobre el mismo String de Java —Clojure es el único que
renuncia a la plantilla y concatena con str, porque en un lenguaje de listas la función es más
natural que la sintaxis—. Para la entrega, la familia comparte una propiedad muy fuerte: se compila
una vez y el .jar resultante vale para todas las plataformas, así que la matriz de compilación
desaparece. El precio se paga en destino: hay que instalar una JVM compatible, y una versión mayor
equivocada hace fallar el arranque con un error de bytecode. Herramientas como jlink o los
fat jars existen precisamente para empaquetar el runtime dentro del artefacto y quitarse esa
dependencia de encima.
Representante del núcleo: C#.
let version = stdin.ReadLine().Trim()
printfn "desplegado=v%s" version
Imports System
Module Despliegue
Sub Main()
Dim version = Console.ReadLine().Trim()
Console.WriteLine("desplegado=v" & version)
End Sub
End Module
Qué reconocer: los tres compilan a la misma IL dentro de un .dll, que es el equivalente exacto
del .jar: artefacto portable que exige la máquina virtual instalada. Donde .NET se separa de la
JVM es en que ofrece las dos opciones de forma explícita: dotnet publish en modo framework
dependent entrega el .dll pequeño y confía en el runtime de destino, mientras que en modo
self-contained entrega un ejecutable por plataforma con el runtime dentro —y ahí reaparece la
matriz de compilación que la JVM había evitado—. Elegir entre un artefacto de 200 KB con
dependencia externa y otro de 70 MB autosuficiente es la decisión de entrega más concreta de esta
familia.
Representante del núcleo: C. Lo que se entrega es un binario por plataforma, sin runtime que instalar pero con una matriz de compilación que crece sola.
#include <iostream>
#include <string>
int main() {
std::string version;
std::getline(std::cin, version);
std::cout << "desplegado=v" << version << '\n';
}
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
char buf[128];
if (fgets(buf, sizeof buf, stdin)) {
NSString *linea = [NSString stringWithUTF8String:buf];
NSCharacterSet *espacios = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString *version = [linea stringByTrimmingCharactersInSet:espacios];
printf("desplegado=v%s\n", version.UTF8String);
}
}
return 0;
}
Qué reconocer: ambos son superconjuntos de C, y el getline de C++ o el fgets de Objective-C
son el mismo gesto que la versión en C de la clase. Para la entrega, la familia paga el precio
completo del compilado AOT: un binario por sistema operativo y por arquitectura, más la
dependencia de bibliotecas compartidas que tienen que existir en destino con la versión correcta
—el clásico fallo de libstdc++ o de glibc demasiado antigua—. Objective-C lo agrava al depender de
Foundation, lo que en la práctica limita su despliegue al ecosistema de Apple. Aquí no hay runtime
que instalar, pero sí un sistema operativo que tiene que parecerse mucho al del build.
Representantes del núcleo: Go · Rust. Compilados a binario estático: se copia un archivo al servidor y funciona, sin instalar nada alrededor.
const std = @import("std");
pub fn main() !void {
var buf: [128]u8 = undefined;
const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
const version = std.mem.trim(u8, linea, " \r\n");
try std.io.getStdOut().writer().print("desplegado=v{s}\n", .{version});
}
import std/strutils
let version = stdin.readLine().strip()
echo "desplegado=v" & version
import std.stdio, std.string;
void main() {
auto version = readln().strip();
writeln("desplegado=v", version);
}
Qué reconocer: los tres producen un ejecutable autosuficiente, igual que Go y Rust, y por eso el
despliegue se reduce a scp más un servicio que lo arranque: la imagen de contenedor puede partir de
scratch porque no hay intérprete ni runtime que copiar. Zig destaca por su compilación cruzada
trivial: un solo comando —zig build-exe -target aarch64-linux, -target x86_64-windows— genera
el binario de cualquier plataforma desde la máquina que sea, porque el compilador incluye las
bibliotecas de C de todos los objetivos. Eso colapsa la matriz de entrega de la familia AOT a un
único job de build, que es exactamente el problema que C++ no sabe resolver sin toolchains cruzados
montados a mano. Nim y D compilan pasando por C, lo que les da portabilidad de fuente pero les
devuelve la dependencia de un compilador de C en cada plataforma de destino.
Representante del núcleo: SQL. No hay binario que desplegar: lo que se entrega es un script que se aplica sobre un motor que ya está corriendo.
:- initialization(main, main).
main :-
read_line_to_string(user_input, Linea),
normalize_space(string(Version), Linea),
format("desplegado=v~w~n", [Version]).
% Datalog no tiene E/S ni concatenación de cadenas: la versión se declara como hecho
% y la etiqueta se modela como una relación entre versión y despliegue.
version("1.2.3").
desplegado(V, "v") :- version(V).
Qué reconocer: Prolog sí tiene entrada/salida y resuelve el contrato con format, pero fíjate en
que su unidad de despliegue no es un ejecutable sino una base de conocimiento que se carga en el
intérprete, igual que un .sql se aplica sobre una base de datos. Esa es la propiedad que comparte
toda la familia: el artefacto es declarativo y el motor ya vive en destino, así que la pregunta de
entrega deja de ser "¿qué binario copio?" y pasa a ser "¿cómo versiono y aplico este cambio sin
romper lo que ya está cargado?" —que es literalmente el problema de las migraciones de esquema—.
Datalog lo lleva al extremo: sin efectos ni cadenas, solo puede declarar la relación, no imprimirla.
Veinte lenguajes, un solo problema, y el mismo esqueleto en todos: leer la versión y prefijarla. Lo que cambia es qué se entrega: fuente más intérprete en destino, un artefacto portable que exige máquina virtual, o un binario por plataforma. Eso es lo transferible.