Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Hunt y Thomas, en The Pragmatic Programmer, cuentan que su primer consejo a cualquier equipo es poner todo bajo control de versiones —incluso las notas y la configuración— porque el sistema de versiones es la red de seguridad que permite experimentar sin miedo: si algo sale mal, siempre hay un estado bueno al que volver. Esta clase toma esa idea y la lleva al terreno políglota, donde un mismo repositorio alberga Python, Go, Java, Rust y más, cada uno con sus artefactos de compilación y su ruido particular. El reto no es usar Git —eso se aprende rápido—, sino usarlo bien en un monorepo con diez ecosistemas conviviendo.
El historial de Git es una secuencia de commits: instantáneas del proyecto, cada una con su mensaje, encadenadas de forma que puedes recorrer el pasado, ver quién cambió qué y volver atrás con precisión quirúrgica. El laboratorio destila esta estructura a su operación más elemental —contar los commits de un historial— porque contar es lo que hace por dentro cualquier herramienta que resume la actividad de un repositorio (git rev-list --count, las gráficas de contribuciones, los informes de release). Sobre ese esqueleto mínimo montaremos las decisiones que de verdad importan: qué versionar, qué ignorar y cómo dividir el trabajo en commits legibles.
Al finalizar, podrás:
.gitignore que excluya los artefactos de build de varios lenguajes.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Commit atómico | Un cambio coherente por commit: historial legible |
| 2 | Historial como red de seguridad | Volver atrás sin miedo a experimentar |
| 3 | .gitignore por lenguaje |
No versionar __pycache__, target/, node_modules/… |
| 4 | Monorepo vs. submódulos | Cómo organizar varios lenguajes en un repositorio |
Git. Un sistema de control de versiones distribuido: cada copia clonada contiene el historial completo, no un fragmento. Esto es lo que lo hace robusto —no hay un servidor central del que dependa todo— y lo que permite trabajar sin conexión y ramificar con coste casi nulo. Para Hunt y Thomas esta ubicuidad del historial es la esencia de la red de seguridad.
Commit atómico. Un commit registra una instantánea del proyecto con un mensaje. La virtud que se persigue es la atomicidad: cada commit debe capturar un único cambio coherente y funcional, de modo que su mensaje lo describa con una frase y pueda revertirse aislado. Un commit que mezcla un arreglo de un bug con un reformateo masivo es imposible de revisar y de deshacer limpiamente.
.gitignore. El archivo que le dice a Git qué no seguir. En un proyecto políglota es crítico: cada lenguaje genera artefactos que jamás deben versionarse (__pycache__/ y *.pyc en Python, target/ en Rust y Java, node_modules/ en JavaScript, binarios compilados en Go y C). Versionarlos ensucia el repositorio, provoca conflictos absurdos e infla el clon.
Trabajas en un monorepo que combina un backend en Go, scripts en Python y una interfaz en TypeScript. Un compañero, sin .gitignore adecuado, comitea por error su carpeta node_modules/ y los binarios de go build: el repositorio pasa de unos megabytes a cientos, cada git status se llena de ruido y los merges empiezan a chocar en archivos generados que a nadie le importan. La disciplina que evita este desastre tiene tres patas: un .gitignore con secciones por lenguaje, commits atómicos con mensajes claros, y la regla de oro de no versionar nunca lo que la build puede regenerar. Antes de todo eso, conviene entender la operación básica sobre el historial —contarlo— que implementa el laboratorio.
commits=<cantidad>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
fix add refactor |
commits=3 |
init |
commits=1 |
a b c d |
commits=4 |
LEER mensajes ; ESCRIBIR cantidad
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/. El contrato: leer mensajes de commit separados por espacio y escribir commits=<cantidad>. Con fix add refactor sale commits=3; con init, commits=1.
python/main.py · python main.pyimport sys
msgs = sys.stdin.read().split()
print(f"commits={len(msgs)}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
La solución de Python es casi telegráfica y por eso ilustra bien la operación. sys.stdin.read().split() lee toda la entrada y la trocea por espacios en una lista de tokens; len(...) cuenta cuántos hay. Cada token representa un commit, así que su longitud es el número de commits del historial —exactamente lo que devuelve git rev-list --count HEAD sobre un repositorio real. Que .split() sin argumentos colapse múltiples espacios es una comodidad deliberada: hace que un historial con separación irregular se cuente igual de bien, sin tokens vacíos que inflen el resultado.
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const msgs = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`commits=${msgs.length}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const msgs: string[] = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`commits=${msgs.length}`);
🧬 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));
String[] msgs = br.readLine().trim().split("\\s+");
System.out.println("commits=" + msgs.length);
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
string[] msgs = Console.In.ReadToEnd()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine($"commits={msgs.Length}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
msgs := strings.Fields(line)
fmt.Printf("commits=%d\n", len(msgs))
}
🧬 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 = s.split_whitespace().count();
println!("commits={n}");
}
🧬 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) {
char tok[256];
int c = 0;
while (scanf("%255s", tok) == 1) c++;
printf("commits=%d\n", c);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
El contraste entre C y SQL revela dos formas de «contar». C no construye ninguna colección: lee token a token con scanf("%255s", ...) —el límite 255 es una defensa consciente contra el desbordamiento del buffer, tan característica del rigor de Kernighan y Ritchie— e incrementa un contador. SQL, en cambio, modela los commits como filas de una tabla y aplica count(*): la operación de contar es primitiva en el modelo relacional que describe Date en SQL and Relational Theory, porque una relación es un conjunto de tuplas y su cardinalidad es un dato de primera clase. Ambos llegan al mismo número por caminos conceptualmente opuestos: uno imperativo y byte a byte, otro declarativo y sobre conjuntos.
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: cuenta las filas (commits).
WITH commits(msg) AS (VALUES ('fix'), ('add'), ('refactor'))
SELECT printf('commits=%d', count(*)) AS resultado FROM commits;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$msgs = preg_split('/\s+/', trim(fgets(STDIN)));
echo "commits=" . count($msgs) . "\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 conteo es idéntico; lo que cambia entre lenguajes es qué artefactos de build hay que excluir del control de versiones. Un .gitignore de monorepo políglota necesita una sección por ecosistema.
| Lenguaje | Qué NO versionar (ejemplos) |
|---|---|
| Python | __pycache__/, *.pyc, .venv/, dist/, *.egg-info/ |
| JavaScript / TypeScript | node_modules/, dist/, *.tsbuildinfo |
| Java | target/, build/, *.class |
| C# | bin/, obj/ |
| Go | binarios compilados, *.exe |
| Rust | target/ |
| C | *.o, *.a, binarios |
| SQL | dumps y .db locales |
| PHP | vendor/ |
Nota transversal: lo que sí se versiona en todos es el lockfile (clase 143), porque forma parte del código fuente reproducible; lo que nunca se versiona es la carpeta de dependencias descargadas (node_modules/, vendor/, .venv/), porque se regenera desde el lock. Distinguir ambas cosas es la decisión más frecuente y más mal resuelta en repositorios políglotas.
Git domina, pero el modelo de instantáneas versionadas encadenadas por un hash lo comparten Mercurial y, en otro nivel, sistemas como Fossil o Pijul. La gran decisión de arquitectura en proyectos con varios lenguajes es monorepo vs. submódulos: el monorepo mantiene todo en un solo historial —commits atómicos que cruzan lenguajes, un único punto de verdad— a costa de un repositorio grande; los submódulos (o git subtree) enlazan repositorios independientes, útil cuando cada componente tiene su propio ciclo de vida pero más frágil a la hora de coordinar cambios. Este curso mismo es un monorepo políglota, y su .gitignore es exactamente del tipo que describe la tabla anterior.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 145
Detalle en reto.md.
node_modules/, target/ o binarios infla el repositorio y genera conflictos absurdos. Añádelos al .gitignore desde el primer día..gitignore demasiado agresivo que excluya package-lock.json rompe la reproducibilidad.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 144 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 146 ⏭️