Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la
familia entera. El mismo problema de la clase —comprobar que n*2 y n+n siguen dando lo
mismo— 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.
nequivalente=<true|false> resultado=<2n>viejo = n * 2, nuevo = n + n; equivalente si coinciden| stdin | esperado |
|---|---|
5 |
equivalente=true resultado=10 |
0 |
equivalente=true resultado=0 |
7 |
equivalente=true resultado=14 |
Representantes del núcleo: Python · PHP. Sin tipos declarados, ninguna herramienta puede demostrar que un cambio es seguro: la red que sostiene la refactorización son las pruebas.
n = STDIN.gets.to_i
viejo = n * 2
nuevo = n + n
puts "equivalente=#{viejo == nuevo} resultado=#{nuevo}"
use strict;
use warnings;
my $n = <STDIN> + 0;
my $viejo = $n * 2;
my $nuevo = $n + $n;
printf "equivalente=%s resultado=%d\n", ($viejo == $nuevo ? 'true' : 'false'), $nuevo;
local n = math.tointeger(io.read("n"))
local viejo, nuevo = n * 2, n + n
print(string.format("equivalente=%s resultado=%d", tostring(viejo == nuevo), nuevo))
gets stdin linea
set n [expr {int([string trim $linea])}]
set viejo [expr {$n * 2}]
set nuevo [expr {$n + $n}]
puts "equivalente=[expr {$viejo == $nuevo ? {true} : {false}}] resultado=$nuevo"
n <- as.integer(readLines("stdin", n = 1))
viejo <- n * 2
nuevo <- n + n
cat(sprintf("equivalente=%s resultado=%d\n", tolower(viejo == nuevo), nuevo))
Qué reconocer: fíjate primero en el detalle pequeño: solo Ruby imprime true tal cual; Perl no
tiene tipo booleano (cierto es 1, falso es la cadena vacía), Tcl usa 0/1, R escribe TRUE en
mayúsculas y hay que bajarlo. Ese desajuste ya es un aviso de lo que viene. Lo grande es esto: en los
cinco, renombrar nuevo es buscar y reemplazar, con todo el riesgo que eso implica —el editor no
sabe si nuevo en otro archivo es la misma variable, un método de otra clase o una cadena—. No hay
renombrado seguro porque no hay información de tipos que permita resolver a qué se refiere cada
nombre. Por eso estas comunidades son las que más insisten en la cobertura de pruebas: en un lenguaje
dinámico la suite de pruebas ocupa el lugar que en otros ocupa el compilador, y refactorizar sin
ella no es refactorizar, es reescribir a ciegas.
Representantes del núcleo: JavaScript · TypeScript.
import 'dart:io';
void main() {
final n = int.parse(stdin.readLineSync()!.trim());
final viejo = n * 2;
final nuevo = n + n;
print('equivalente=${viejo == nuevo} resultado=$nuevo');
}
// ActionScript no tiene stdin: se ilustra la comparación de las dos versiones.
package refactor {
public class Doble {
public static function comprobar(n:int):String {
var viejo:int = n * 2;
var nuevo:int = n + n;
return "equivalente=" + (viejo == nuevo) + " resultado=" + nuevo;
}
}
}
Qué reconocer: esta familia contiene el experimento natural más claro del asunto. JavaScript y
TypeScript son el mismo lenguaje con y sin tipos, y la diferencia en refactorización es enorme:
sobre .ts el editor renombra un símbolo en todo el proyecto con garantías, y sobre .js el mismo
editor solo puede ofrecer una heurística que falla en cuanto hay acceso dinámico por cadena
(obj["nuevo"]). Dart y ActionScript anotan tipos —int, Number— y por eso el renombrado vuelve a
ser una transformación verificable. Es el argumento de ingeniería que empuja a los proyectos grandes
hacia el tipado estático: no es la elegancia, es poder cambiar el código sin miedo.
Representante del núcleo: Java. Es la familia donde nació la refactorización automática: el IDE conoce el árbol de tipos completo y puede reescribirlo.
fun main() {
val n = readLine()!!.trim().toInt()
val viejo = n * 2
val nuevo = n + n
println("equivalente=${viejo == nuevo} resultado=$nuevo")
}
object Refactor extends App {
val n = scala.io.StdIn.readLine().trim.toInt
val viejo = n * 2
val nuevo = n + n
println(s"equivalente=${viejo == nuevo} resultado=$nuevo")
}
def n = System.in.newReader().readLine().trim() as int
def viejo = n * 2
def nuevo = n + n
println "equivalente=${viejo == nuevo} resultado=$nuevo"
(require '[clojure.string :as str])
(let [n (parse-long (str/trim (read-line)))
viejo (* n 2)
nuevo (+ n n)]
(println (str "equivalente=" (= viejo nuevo) " resultado=" nuevo)))
Qué reconocer: los cuatro imprimen true sin adaptación porque comparten el Boolean.toString
de Java. La diferencia real está en las garantías de cambio: Kotlin y Scala tienen renombrado
seguro —el compilador resuelve cada referencia, así que el IDE puede extraer un método o cambiar
una firma y avisar de cada uso roto—, mientras que Groovy, aun corriendo sobre la misma JVM, resuelve
métodos en tiempo de ejecución y cae en la misma trampa que Ruby: el editor adivina. Clojure ocupa un
lugar propio: es dinámico, pero al ser inmutable y sin efectos ocultos, la refactorización más común
—extraer una función— es trivialmente segura, porque una expresión pura se puede mover sin cambiar el
significado del programa. Dos ejes distintos, entonces: tipos para renombrar, pureza para
mover.
Representante del núcleo: C#.
let n = int (stdin.ReadLine().Trim())
let viejo = n * 2
let nuevo = n + n
printfn "equivalente=%b resultado=%d" (viejo = nuevo) nuevo
Imports System
Module Refactor
Sub Main()
Dim n = Integer.Parse(Console.ReadLine().Trim())
Dim viejo = n * 2
Dim nuevo = n + n
Console.WriteLine("equivalente=" & (viejo = nuevo).ToString().ToLowerInvariant() & " resultado=" & nuevo)
End Sub
End Module
Qué reconocer: VB.NET obliga a bajar True a minúsculas, un recordatorio de que el formato de un
booleano es una convención de cada lenguaje y no una verdad universal. En cuanto a refactorización,
los tres son estáticos y el compilador de Roslyn expone el árbol semántico a las herramientas: de ahí
que C# y VB.NET tengan renombrado seguro, extracción de método y cambio de firma aplicables a
toda la solución, y que los analizadores puedan proponer y aplicar arreglos automáticos. F# añade una
garantía extra que conviene ver: con tipos algebraicos y coincidencia de patrones, añadir un caso
nuevo hace que el compilador señale cada punto del código que dejó de ser exhaustivo. Eso convierte
un cambio de modelo en una lista de tareas verificada por la máquina, que es lo más cerca que llega
un lenguaje a garantizar una refactorización completa.
Representante del núcleo: C. Estáticos, pero con un preprocesador que las herramientas no pueden atravesar del todo.
#include <iostream>
int main() {
long long n;
std::cin >> n;
const long long viejo = n * 2;
const long long nuevo = n + n;
std::cout << "equivalente=" << std::boolalpha << (viejo == nuevo)
<< " resultado=" << nuevo << '\n';
}
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
long n = 0;
scanf("%ld", &n);
long viejo = n * 2;
long nuevo = n + n;
printf("equivalente=%s resultado=%ld\n", viejo == nuevo ? "true" : "false", nuevo);
}
return 0;
}
Qué reconocer: std::boolalpha de C++ es exactamente el interruptor que convierte 1 en true;
Objective-C conserva el printf de C y con él la necesidad de escribir el ternario a mano. En
refactorización, la familia es un caso intermedio incómodo: hay tipos, así que en principio el
renombrado es analizable, pero el preprocesador rompe la garantía —una macro puede generar el
identificador que estás renombrando, y la compilación condicional esconde ramas enteras del código
que la herramienta nunca ve porque no están activas en tu configuración—. Por eso las herramientas
serias de esta familia (clang-tidy, clangd) trabajan sobre la base de datos de compilación real:
necesitan saber con qué banderas se compiló cada archivo antes de atreverse a tocar un nombre.
Representantes del núcleo: Go · Rust. Estáticos y sin preprocesador textual, que es justo lo que devuelve la refactorización automática a la familia C.
const std = @import("std");
pub fn main() !void {
var buf: [64]u8 = undefined;
const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
const n = try std.fmt.parseInt(i64, std.mem.trim(u8, linea, " \r\n"), 10);
const viejo = n * 2;
const nuevo = n + n;
try std.io.getStdOut().writer().print("equivalente={} resultado={d}\n", .{ viejo == nuevo, nuevo });
}
import std/strutils
let n = stdin.readLine().strip().parseInt()
let viejo = n * 2
let nuevo = n + n
echo "equivalente=" & $(viejo == nuevo) & " resultado=" & $nuevo
import std.stdio, std.conv, std.string;
void main() {
const n = readln().strip().to!long;
const viejo = n * 2;
const nuevo = n + n;
writefln("equivalente=%s resultado=%s", viejo == nuevo, nuevo);
}
Qué reconocer: los tres marcan las ligaduras como inmutables por defecto o casi —const, let,
const—, igual que let en Rust y a diferencia de C. Eso importa más de lo que parece al
refactorizar: si una variable no se reasigna, moverla, renombrarla o extraer la expresión que la
calcula no puede cambiar el comportamiento, porque no hay estado que dependa del orden. Es la misma
propiedad que hace segura la extracción de funciones en Clojure, aquí conseguida sin renunciar al
control de bajo nivel. Zig añade un detalle que encaja con esta clase: al no tener macros ni
sobrecarga de operadores, el código dice lo que hace, y una herramienta que analiza el fuente ve
lo mismo que el compilador — que es la condición previa para que cualquier refactorización automática
sea de fiar.
Representante del núcleo: SQL. La equivalencia entre dos formas de escribir lo mismo no se prueba ejecutando: se razona sobre la declaración.
:- initialization(main, main).
main :-
read_line_to_string(user_input, Linea),
number_string(N, Linea),
Viejo is N * 2,
Nuevo is N + N,
( Viejo =:= Nuevo
-> Eq = true
; Eq = false
),
format("equivalente=~w resultado=~w~n", [Eq, Nuevo]).
% Datalog no tiene E/S: las dos versiones se declaran como reglas y la equivalencia
% se comprueba por ausencia de contraejemplo (la relación queda vacía).
num(5).
viejo(N, D) :- num(N), D = N * 2.
nuevo(N, D) :- num(N), D = N + N.
contraejemplo(N) :- viejo(N, A), nuevo(N, B), A != B.
Qué reconocer: en Prolog Viejo is N * 2 no es una asignación sino una ligadura única, así que la
refactorización clásica de "cambiar el valor de una variable" ni siquiera existe: solo puedes
reescribir la regla. Y ahí está la idea que esta familia aporta al tema de la clase — el bloque de
Datalog es la definición formal de refactorización segura: dos implementaciones son equivalentes
si no existe ninguna entrada para la que difieran, y contraejemplo es exactamente la consulta que
lo pregunta. Un motor Datalog puede evaluarla sobre todos los hechos declarados; una suite de pruebas
solo la comprueba en los casos que alguien escribió. Esa es la distancia real entre probar y
demostrar, y es la razón de que el optimizador de una base de datos pueda reescribir tu consulta
sin pedirte permiso.
Veinte lenguajes, un solo problema, y el mismo esqueleto en todos: calcular de dos formas y comparar. Lo que cambia es quién te garantiza que el cambio fue seguro: el compilador y el IDE en los estáticos, la suite de pruebas en los dinámicos, y un razonamiento formal en los declarativos. Eso es lo transferible.