Parte 11 — Proyecto integrador políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Escribir la parte del proyecto que no se ejecuta y que, sin embargo, decide si el sistema sobrevive: la defensa razonada de las decisiones de lenguaje. Un sistema políglota con cinco lenguajes es o bien una obra de ingeniería —cada elección justificada por lo que ese componente necesita— o bien una colección de caprichos acumulados por rotación de personal. Desde fuera, el código se ve exactamente igual en los dos casos. Lo único que los distingue es la existencia de un documento que explique por qué.
Esa es la asimetría que hace tan valiosa esta clase. El código responde perfectamente a la pregunta qué hace el sistema: es su descripción más precisa y siempre está actualizada, porque es la que se ejecuta. Pero el código no puede responder por qué es así y no de otra forma, ni qué alternativas se descartaron, ni bajo qué supuestos la decisión sigue siendo válida. Esa información existe solo en la cabeza de quien decidió, y se evapora con el primer cambio de equipo. Hunt y Thomas, en The Pragmatic Programmer, argumentan que la documentación debe vivir pegada al código y tratarse con la misma disciplina que él; Newman añade en Building Microservices que en un sistema descompuesto lo que hay que documentar por encima de todo son las fronteras y sus razones, porque nadie las ve leyendo un solo repositorio.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Documentación | Explicar el porqué |
| 2 | Defensa de decisiones | Justificar cada lenguaje |
| 3 | Secciones | Cobertura del documento |
La documentación de un sistema es su explicación escrita, y su valor está concentrado en la parte que el código no puede dar: el contexto, las alternativas descartadas y las consecuencias aceptadas. La defensa de decisiones es el género concreto que nos ocupa hoy: un argumento revisable —y por tanto refutable— sobre por qué este componente está escrito en este lenguaje. Un ADR (Architecture Decision Record) es su formato canónico: un documento breve, numerado e inmutable, con cuatro apartados —contexto, decisión, alternativas consideradas y consecuencias— que se escribe cuando la decisión se toma y no se edita después: si la decisión cambia, se escribe un ADR nuevo que anula al anterior. La cobertura mide cuántas de las decisiones estructurales del sistema tienen su registro.
Que el registro sea inmutable es el detalle que más gente pasa por alto y el que más rendimiento da. Un documento que se reescribe cada vez que cambia la realidad acaba describiendo solo el presente, y el presente ya lo describe el código. Lo que no se puede reconstruir de ninguna otra forma es la secuencia: qué sabíamos cuando elegimos Go para el servicio de ingesta, qué descartamos, qué esperábamos ganar. Con esa secuencia, un ingeniero que llega dos años después puede hacer la única pregunta que importa —"¿siguen siendo ciertos los supuestos?"— y decidir con fundamento si mantener o cambiar. Sin ella solo le quedan dos malas opciones: respetar la decisión por superstición, o rehacerla desde cero por desconocimiento. La segunda es la que produce reescrituras que repiten, una a una, los errores que la decisión original ya había evitado.
Conviene además nombrar el criterio con el que se defiende un lenguaje, porque no es el gusto. Una justificación sólida se apoya en lo que el componente exige: latencia y control de memoria (y entonces Rust, C o Go tienen argumentos), riqueza de ecosistema para un dominio (Python en datos, TypeScript en el navegador), garantías del compilador en una base grande y de larga vida (Java, C#), o expresividad declarativa sobre conjuntos (SQL). Y se apoya, sobre todo, en un factor que no aparece en ninguna tabla comparativa: qué lenguajes puede mantener el equipo que tienes. Un lenguaje técnicamente superior que solo una persona domina es una decisión peor que uno mediocre que todos leen.
Llega un ingeniero nuevo al proyecto y encuentra el servicio de ingesta en Go, la API en TypeScript, el motor de cálculo en Rust y las consultas en SQL. Su primera reacción es razonable: "esto es un desastre, unifiquémoslo todo en un lenguaje". Si no hay documentación, no tiene forma de saber que Rust está ahí porque el cálculo tardaba nueve minutos y ahora tarda veinte segundos, ni que la API es TypeScript porque comparte los tipos del contrato con el frontend y eso eliminó una clase entera de bugs. Con cuatro ADR de media página cada uno, esa misma conversación cambia de naturaleza: deja de ser una opinión contra otra y pasa a ser una revisión de supuestos. Documentar no es burocracia; es lo que convierte una discusión de gustos en una decisión de ingeniería.
n (número de secciones documentadas)documentado=<n> seccionesEspecificación y verificación en casos.json:
| stdin | esperado |
|---|---|
5 |
documentado=5 secciones |
1 |
documentado=1 secciones |
8 |
documentado=8 secciones |
LEER n ; ESCRIBIR documentado=n secciones
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyimport sys
n = int(sys.stdin.readline())
print(f"documentado={n} secciones")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const n = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`documentado=${n} secciones`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`documentado=${n} secciones`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
System.out.println("documentado=" + n + " secciones");
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
int n = int.Parse(Console.In.ReadToEnd().Trim());
Console.WriteLine($"documentado={n} secciones");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(line))
fmt.Printf("documentado=%d secciones\n", n)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let n: i64 = s.trim().parse().unwrap();
println!("documentado={n} secciones");
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long n;
if (scanf("%ld", &n) != 1) return 1;
printf("documentado=%ld secciones\n", n);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL se documenta con comentarios; aqui, el conteo.
WITH t(n) AS (VALUES (5))
SELECT printf('documentado=%d secciones', n) AS resultado FROM t;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$n = (int) trim(fgets(STDIN));
echo "documentado=$n secciones\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
El contrato (casos.json) lee un entero y responde documentado=<n> secciones. Es la
implementación más escueta de la parte, y la elección no es casual: sirve para señalar que la
documentación se cuenta por decisiones registradas, no por páginas escritas. Cinco ADR de media página
cubren más sistema que cincuenta folios de descripción que repiten lo que el código ya dice.
La operación técnica es leer un número y formatearlo, y aun ahí hay algo que mirar: cada lenguaje decide
qué hacer cuando la entrada no es un número. Python lanza ValueError con int(); Java lanza
NumberFormatException; C# lanza FormatException con int.Parse; Rust devuelve un Result que
aquí se abre con .unwrap() y aborta si vino mal. Los cuatro fallan ruidosamente. En cambio Go
descarta el error con n, _ := strconv.Atoi(...) y sigue con n = 0, PHP convierte con (int) sin
protestar, y JavaScript devuelve NaN desde parseInt — un valor que se propaga silenciosamente por
todo el cálculo posterior. Ese contraste entre fallar pronto y seguir con un valor dudoso es una diferencia
semántica pura, y es exactamente el tipo de decisión que un ADR debería recoger cuando eliges el lenguaje
de un componente que valida entradas ajenas.
El detalle idiomático que cierra el recorrido está en cómo cada lenguaje mezcla texto y número.
Python interpola con f"documentado={n} secciones", Rust con println!("documentado={n} secciones")
usando la captura directa de la variable en la plantilla, Go y C con especificadores %d/%ld, y
SQL con printf sobre una tabla de un solo caso. La misma frase, cinco maneras de componerla.
Formatear un número dentro de una frase parece idéntico en todas partes; las diferencias aparecen en los bordes, que es donde siempre viven.
| Clase de diferencia | Observación entre lenguajes |
|---|---|
| Sintáctica | Interpolación (Python, JS/TS, C#, Rust, PHP), concatenación con + (Java), Printf con %d (Go, C), printf de SQLite (SQL). |
| Semántica | Ante una entrada no numérica, Python, Java, C# y Rust fallan de inmediato; Go ignora el error y usa el cero, PHP convierte a cero en silencio y JS produce NaN. El mismo programa, tres políticas de error distintas. |
| Paradigmática | Los imperativos leen un valor y lo formatean; SQL proyecta una columna calculada sobre una tabla — y su documentación natural no son comentarios sino el esquema: nombres de tablas, columnas y restricciones que describen el modelo. |
Hay un paralelismo que vale la pena hacer explícito. Los lenguajes con tipos estáticos y explícitos —Java,
C#, Rust, TypeScript— documentan una parte del porqué dentro del propio código: una firma que dice
Result<Pedido, ErrorValidacion> está declarando qué puede salir mal sin necesidad de un párrafo. Los
dinámicos —Python, JavaScript, PHP— trasladan esa carga a la documentación externa y a las convenciones.
No es que unos necesiten documentar y otros no: es que el límite entre lo que el código puede afirmar por
sí solo y lo que hay que escribir aparte se mueve según el lenguaje. Saber dónde está ese límite en
cada uno es parte de la competencia políglota.
Cada ecosistema tiene su forma de documentación pegada al código: docstrings y Sphinx en Python, Javadoc
en la JVM, comentarios XML y DocFX en .NET, godoc —que en Go es tan central que la comunidad escribe los
comentarios pensando en cómo se leerán renderizados—, rustdoc con ejemplos que además se ejecutan
como pruebas, JSDoc y TypeDoc en JavaScript y TypeScript, phpDocumentor en PHP, y comentarios de esquema en
SQL. La documentación ejecutable de Rust es el caso límite interesante: un ejemplo en la documentación que
deja de compilar rompe la construcción, lo que ataca de raíz el problema de la documentación obsoleta. Por
encima de todos ellos está la capa que no pertenece a ningún lenguaje —README, ADR, diagramas— y que es la
única que puede hablar del sistema completo. En un proyecto políglota esa capa neutral no es opcional:
es el único lugar donde el sistema existe entero.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 175
Detalle en reto.md.
// incrementa i), que es la única parte que el lector ya podía obtener solo → solución: escribir lo que el código no puede decir: la razón, la alternativa descartada y el supuesto que sostiene la decisión.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 174 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 176 ⏭️