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.
Entre el código fuente que un desarrollador escribe y el artefacto que se ejecuta en producción media un proceso —la build— que compila, enlaza y empaqueta. La ingeniería seria exige que ese proceso sea determinista: la misma entrada debe producir, siempre, el mismo artefacto, byte a byte. Cuando esto se cumple, decimos que la build es reproducible, y de repente cosas antes imposibles se vuelven fáciles: comparar dos compilaciones para saber si el binario publicado corresponde de verdad al código auditado, cachear resultados en integración continua, y confiar en la cadena de suministro de software. McConnell dedica en Code Complete un capítulo entero a la integración y la construcción precisamente porque el build es el punto donde el trabajo individual se convierte en un producto verificable; una build frágil o no repetible envenena todo lo que viene después.
El laboratorio reduce esa idea a su núcleo comprobable: calcular un checksum —aquí, la suma de una lista de enteros— como huella de una entrada. Un checksum real usa un hash criptográfico (SHA-256), pero la mecánica conceptual es la misma que verás en el código: entra una lista, sale un número que la representa y que cambia si la lista cambia. Ese número es lo que un gestor de paquetes compara al descargar un artefacto para confirmar que nadie lo manipuló en tránsito.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Determinismo del build | Misma entrada, mismo artefacto: auditable |
| 2 | Checksum / hash | Huella que delata cualquier alteración |
| 3 | Empaquetado por lenguaje | Wheel, jar, crate, binario estático, NuGet… |
| 4 | Fijado de dependencias y flags | Fuentes ocultas de no-determinismo |
Compilación reproducible. Una build es reproducible cuando, partiendo del mismo código, las mismas dependencias fijadas y los mismos flags de compilación, produce un artefacto idéntico byte a byte en cualquier máquina y momento. El enemigo es el no-determinismo: marcas de tiempo incrustadas, rutas absolutas, orden de iteración no estable o números aleatorios. Eliminarlos es lo que permite la auditoría de la que habla McConnell.
Checksum. Un valor derivado de los datos mediante una función que produce salidas muy distintas ante cambios mínimos. Sirve como detector de alteraciones: si el checksum recalculado no coincide con el publicado, los datos cambiaron. En el laboratorio la función es una suma; en la vida real es un hash criptográfico resistente a colisiones.
Artefacto y empaquetado. El artefacto es la salida de la build lista para distribuir. Cada ecosistema tiene su formato: wheel en Python, jar/war en Java, crate en Rust, un binario estático en Go, un paquete NuGet en C#. Empaquetar bien significa incluir exactamente lo necesario y publicar el checksum junto al artefacto.
Publicas una nueva versión de una biblioteca y un usuario reporta que el binario que descargó «no funciona igual» que el de otro compañero. Al investigar descubres que tu build incrustaba la fecha de compilación en el artefacto: cada compilación producía bytes distintos y, por tanto, un checksum distinto, aunque el código fuera idéntico. Nadie podía verificar nada. La solución es hacer la build determinista —fijar dependencias, retirar la marca de tiempo— para que el checksum publicado sea estable y cualquiera pueda comprobar que su descarga coincide con lo que tú compilaste. El primer ladrillo de esa verificación es saber calcular el checksum, que es lo que implementa el laboratorio.
checksum=<suma de los valores>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
1 2 3 |
checksum=6 |
5 |
checksum=5 |
10 20 30 |
checksum=60 |
LEER lista ; checksum <- suma ; ESCRIBIR checksum
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 enteros separados por espacio y escribir checksum=<suma>. Con 1 2 3 la salida es checksum=6; con 10 20 30, checksum=60.
python/main.py · python main.pyimport sys
nums = [int(x) for x in sys.stdin.read().split()]
print(f"checksum={sum(nums)}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
La versión de Python es la más compacta y expresa con claridad el patrón «leer todo, tokenizar, reducir». sys.stdin.read() consume la entrada completa; .split() sin argumentos parte por cualquier bloque de espacios (incluidos saltos de línea), lo que hace al programa robusto ante entradas con formato irregular. La comprensión de lista [int(x) for x in ...] convierte cada token a entero de una vez —Ramalho, en Fluent Python, defiende estas comprensiones como la forma legible de expresar transformaciones sobre secuencias— y sum(...) hace la reducción. La suma es la operación reproducible por excelencia: no depende del orden en que se recorran los sumandos, así que dos ejecuciones con la misma entrada dan el mismo resultado, que es justo la propiedad que se busca en un build.
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const nums = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`checksum=${nums.reduce((a, b) => a + b, 0)}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const nums: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`checksum=${nums.reduce((a, b) => a + b, 0)}`);
🧬 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[] p = br.readLine().trim().split("\\s+");
long c = 0;
for (String s : p) c += Integer.parseInt(s);
System.out.println("checksum=" + c);
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
using System.Linq;
long c = Console.In.ReadToEnd()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Sum(x => (long) int.Parse(x));
Console.WriteLine($"checksum={c}");
🧬 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')
c := 0
for _, s := range strings.Fields(line) {
n, _ := strconv.Atoi(s)
c += n
}
fmt.Printf("checksum=%d\n", c)
}
🧬 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 c: i64 = s.split_whitespace().map(|x| x.parse::<i64>().unwrap()).sum();
println!("checksum={c}");
}
🧬 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 c = 0, x;
while (scanf("%ld", &x) == 1) c += x;
printf("checksum=%ld\n", c);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
Merece contrastar dos filosofías. C acumula en un bucle while (scanf(...) == 1): no construye ninguna lista intermedia, va sumando token a token hasta que scanf deja de leer un entero. Es la aproximación de mínimo consumo de memoria que caracteriza el estilo de Kernighan y Ritchie, y usa long para el acumulador anticipando que la suma puede desbordar el rango de un entero pequeño. Go y C# eligen el camino opuesto pero igual de deliberado: Go recorre strings.Fields(line) sumando explícitamente, mientras que C# encadena .Split(...).Sum(x => (long) int.Parse(x)) en estilo LINQ declarativo. Fíjate en que ambos, como el C, promocionan a long antes de sumar: es una decisión de ingeniería para evitar el desbordamiento silencioso, un no-determinismo tan real como una marca de tiempo, porque un checksum que desborda deja de representar fielmente su entrada.
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: SUM como checksum simple.
WITH nums(x) AS (VALUES (1), (2), (3))
SELECT printf('checksum=%d', sum(x)) AS resultado FROM nums;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$nums = array_map('intval', preg_split('/\s+/', trim(fgets(STDIN))));
echo "checksum=" . array_sum($nums) . "\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 algoritmo es una suma; lo que diferencia de verdad a los diez lenguajes es cómo empaquetan y qué producen al compilar. Esta es la tabla que conviene memorizar.
| Lenguaje | Artefacto de distribución | Herramienta de empaquetado |
|---|---|---|
| Python | wheel (.whl), sdist |
build, hatch, poetry build |
| JavaScript / TypeScript | tarball npm | npm pack, npm publish |
| Java | .jar, .war |
Maven, Gradle |
| C# | paquete NuGet (.nupkg) |
dotnet pack |
| Go | binario estático único | go build (CGO_ENABLED=0 para estático) |
| Rust | crate / binario en target/ |
cargo build --release, cargo package |
| C | binario u objeto/librería | compilador + ar, Make/CMake |
| SQL | script / migración | según el motor |
| PHP | paquete Composer | composer + archive |
Dos ideas transversales. Primera: la reproducibilidad se consigue igual en todos —dependencias fijadas (lockfile de la clase anterior), flags de compilación explícitos y eliminación de marcas de tiempo—; herramientas como SOURCE_DATE_EPOCH existen precisamente para neutralizar la fecha en el artefacto. Segunda: Go destaca por producir un binario estático autocontenido que no arrastra dependencias de runtime, lo que simplifica enormemente la reproducibilidad frente a un wheel de Python, que asume un intérprete y paquetes instalados en destino.
La verificación por checksum es universal en la distribución de software: los índices de paquetes publican hashes SHA-256 y los gestores los comprueban antes de instalar (go.sum, --require-hashes en pip, el integrity de npm). El movimiento Reproducible Builds, nacido en Debian y adoptado por muchos ecosistemas, persigue exactamente lo que ilustra esta clase: que compilar el mismo código dé siempre el mismo artefacto, para que el checksum publicado sea una prueba y no una casualidad.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 144
Detalle en reto.md.
SOURCE_DATE_EPOCH, rutas relativas).Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 143 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 145 ⏭️