⬅️ Volver a la clase 140 · 🧬 Primos del Atlas · 🧟 Índice de lenguajes vivos · 📚 Índice
Comparar dos resultados y decir si son equivalentes. Es literalmente lo que hace el verificador de este curso (clase 040) y lo que este programa reproduce en doce lenguajes. Y no es un ejercicio académico: la migración de sistemas COBOL a Java lleva treinta años haciendo exactamente esto — ejecutar los dos sistemas con los mismos datos durante meses y comparar cada salida, byte a byte, antes de apagar el viejo.
🎯 Estos lenguajes no están aquí por ser antiguos
El criterio es doble y se declara en la ficha de cada uno: que se ejecute hoy —banca, sanidad, aviónica, ERP, diseño de chips, CAD— y que deje a la vista un concepto que los diez del núcleo esconden.
Aquí el concepto es la equivalencia observable: dos implementaciones distintas son intercambiables si producen la misma salida para las mismas entradas. Y estos lenguajes lo enseñan porque es su problema profesional real. COBOL y PL/I viven en migraciones. RPG convive con Java en la misma máquina. Fortran valida modelos nuevos contra modelos viejos. Y todos ellos aportan la parte incómoda: la equivalencia no es igualdad, porque hay redondeo, orden y precisión de por medio.
Y aparece la pregunta que atraviesa la clase: ¿qué tolerancia hace falta para que "igual" siga significando algo?
Y ninguno es una foto fija: casi todos han incorporado en los últimos años JSON, REST, GPU, Unicode o Git. Cada ficha lo detalla en su sección
🔄 Lo que se ha modernizado.
x y (dos resultados a comparar) → stdout: equivalente=<true|false>equivalente si x == y| stdin | esperado |
|---|---|
6 6 |
equivalente=true |
5 7 |
equivalente=false |
0 0 |
equivalente=true |
Qué está verificado en esta página. Los lenguajes de la sección 🟢 se ejecutan en CI contra este mismo
casos.json, igual que las diez implementaciones del núcleo (workflow Labs). Los de la sección 🟡 no pueden cumplir este contrato sin falsear el lenguaje, y se explica por qué. Los de la sección ⚪ sí podrían, pero su cadena de herramientas no está en los runners: son correctos, sin sello de máquina.
Ficha completa · Banca, seguros, gobierno, medios de pago · cobc -x -free prog.cob
IDENTIFICATION DIVISION.
PROGRAM-ID. EQUIVAL.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 LINEA PIC X(80).
01 C-X PIC X(20).
01 C-Y PIC X(20).
01 VX PIC S9(9) COMP.
01 VY PIC S9(9) COMP.
PROCEDURE DIVISION.
ACCEPT LINEA
UNSTRING LINEA DELIMITED BY ALL SPACES
INTO C-X C-Y
END-UNSTRING
COMPUTE VX = FUNCTION NUMVAL(C-X)
COMPUTE VY = FUNCTION NUMVAL(C-Y)
IF VX = VY
DISPLAY "equivalente=true"
ELSE
DISPLAY "equivalente=false"
END-IF
STOP RUN.
Lo que esta clase enseña en COBOL. Esta es, probablemente, la clase de todo el curso donde COBOL tiene más que enseñar, porque la comparación de equivalencia es la técnica central de la modernización de sistemas, y COBOL lleva treinta años en el centro de ese problema.
El procedimiento estándar de una migración se llama parallel run, y es este:
El punto 4 es el que sorprende a quien no ha hecho una migración: aparecen discrepancias que resultan ser errores históricos del sistema original de los que dependen los clientes. Y entonces hay que decidir si el sistema nuevo debe reproducir el error.
La respuesta, casi siempre, es que sí. Es lo que se llama bug-for-bug compatibility.
Y COBOL aporta a esta clase el motivo técnico más frecuente de discrepancia, que es de la clase 045: la aritmética decimal.
COMPUTE RESULTADO ROUNDED = A * B / C
COBOL calcula en decimal con COMP-3 y redondea a la mitad hacia arriba. Un Java que use double
dará otro resultado en los céntimos, y en un sistema bancario eso son millones de discrepancias.
La solución conocida es usar BigDecimal con el modo de redondeo exacto:
resultado.setScale(2, RoundingMode.HALF_UP) // el de COBOL por defecto
Y la letra pequeña que arruina migraciones: ROUNDED de COBOL admite varios modos —ROUNDED MODE
IS NEAREST-EVEN, TRUNCATION, PROHIBITED—, y el que no se declara depende del compilador y de las
opciones de compilación.
La lección para esta clase, y es dura: la equivalencia se define, no se supone. Antes de comparar hay que decidir qué campos, con qué tolerancia y con qué regla de redondeo — y esa decisión es la mitad del trabajo de una migración.
Ficha completa · HPC, clima, física, BLAS/LAPACK · gfortran -O2 prog.f90
program equival
implicit none
integer :: x, y
read(*, *) x, y
if (x == y) then
write(*, '(A)') 'equivalente=true'
else
write(*, '(A)') 'equivalente=false'
end if
end program equival
Lo que esta clase enseña en Fortran. Fortran vive la versión más difícil de esta clase: comparar dos programas numéricos que nunca darán exactamente el mismo resultado.
El motivo es de la clase 073: la suma en coma flotante no es asociativa.
! Estas tres sumas dan resultados DISTINTOS en el último bit:
s = ((a + b) + c) + d
s = (a + b) + (c + d)
s = a + (b + (c + d))
Y eso importa porque cualquier cambio de compilador, de nivel de optimización, de número de procesos MPI o de hilos OpenMP cambia el orden de las sumas — y por tanto el último dígito.
De ahí las técnicas que la comunidad de cálculo científico ha desarrollado y que esta clase debe recoger:
Primera, comparar con tolerancia relativa, no absoluta:
error = abs(nuevo - viejo) / max(abs(viejo), tiny(1.0_dp))
if (error > 1.0e-10_dp) then ...
Segunda, comparar magnitudes agregadas y no valores puntuales: la energía total, la masa conservada, la norma de la diferencia — porque una diferencia local puede ser ruido y una diferencia global es un error.
Tercera, forzar la reproducibilidad cuando hace falta:
gfortran -ffp-contract=off # sin fundir multiplicación y suma (FMA)
ifort -fp-model precise # sin reasociar
export MKL_CBWR=COMPATIBLE # resultados reproducibles entre CPUs
export OMP_NUM_THREADS=1 # sin reparto variable
-ffp-contract=off merece la explicación: la instrucción FMA calcula a*b + c con un solo
redondeo en vez de dos, y por tanto es más precisa y da otro resultado. El compilador la usa por
defecto si el procesador la tiene, y por eso el mismo código da distintos números en distintas
máquinas.
Y MKL_CBWR —conditional bitwise reproducibility— es la respuesta de Intel a este problema:
obliga a la biblioteca a usar el mismo camino de código en cualquier CPU, a costa de rendimiento.
Esa es la lección general y transferible: la reproducibilidad bit a bit es alcanzable y se paga. Y la pregunta de ingeniería no es si conseguirla, sino si el dominio la necesita — en un modelo climático probablemente no, en un cálculo de certificación probablemente sí.
Ficha completa · Aviónica, espacio, ferrocarril, defensa · gnatmake prog.adb
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Equival is
X, Y : Integer;
begin
Get (X);
Get (Y);
if X = Y then
Put_Line ("equivalente=true");
else
Put_Line ("equivalente=false");
end if;
end Equival;
Lo que esta clase enseña en Ada. Ada aporta a esta clase una idea que cambia el planteamiento: si la equivalencia se puede expresar como un contrato, se puede comprobar en cada llamada real, no solo en las pruebas.
function Nuevo_Calculo (X : Entrada) return Salida
with Post => Nuevo_Calculo'Result = Viejo_Calculo (X);
Esa postcondición dice literalmente "el nuevo debe dar lo mismo que el viejo", y con
Assertion_Policy (Check) se comprueba en producción, en cada llamada, con datos reales (clase
118).
Es el parallel run de COBOL de esta página expresado en el lenguaje y ejecutado por el sistema, en lugar de por un proceso externo que compara ficheros.
Y con SPARK, se puede ir más lejos:
-- gnatprove intenta DEMOSTRAR que son equivalentes para TODA entrada
Lo que la comparación de salidas hace por muestreo, la demostración lo hace para el dominio completo. No siempre es posible —depende de la complejidad de las funciones— pero cuando lo es, sustituye meses de ejecución en paralelo.
Y Ada tiene un mecanismo que encaja exactamente con esta clase y que merece conocerse: la redundancia con votación, usada en sistemas críticos.
-- Tres implementaciones independientes del mismo cálculo; se vota
Resultado := Votar (Impl_A (X), Impl_B (X), Impl_C (X));
Programación en N versiones: se escriben tres implementaciones, idealmente por equipos distintos, y el sistema toma el valor en el que coincidan al menos dos.
Se usó en el Boeing 777 y en el Airbus A320 —con procesadores y compiladores distintos—, y su premisa es la de esta clase llevada al extremo: si dos implementaciones independientes coinciden, es muy improbable que ambas estén mal de la misma manera.
Su límite también es conocido y merece decirse: los errores de especificación afectan a las tres versiones por igual. Si el requisito estaba mal, las tres implementaciones lo cumplirán mal y votarán lo mismo.
Es la razón por la que la industria crítica invierte tanto en la especificación y tan poco en el código: el verificador de equivalencia no protege de una idea equivocada.
Ficha completa · Escritorio empresarial, TPV, industria (vía Delphi y Free Pascal) · fpc -Mobjfpc prog.pas
program Equival;
{$MODE OBJFPC}{$H+}
var
X, Y: Integer;
begin
Read(X, Y);
if X = Y then
WriteLn('equivalente=true')
else
WriteLn('equivalente=false');
end.
Lo que esta clase enseña en Pascal. El ecosistema Pascal se encuentra con esta clase por una vía muy concreta y muy frecuente: migrar una aplicación Delphi de 32 bits a 64 bits, o de Delphi a Free Pascal, o de Windows a Linux.
Y ahí aparece un catálogo de diferencias que ilustra bien lo que la equivalencia tiene de traicionera:
| Diferencia | Consecuencia |
|---|---|
Integer sigue siendo 32 bits, pero el puntero pasa a 64 |
Integer(punteroCasteado) trunca |
NativeInt cambia de tamaño |
los cálculos de desplazamiento cambian |
string es UTF-16 en Delphi y puede ser UTF-8 en FPC |
Length da otro número |
Extended es de 80 bits en x86 y de 64 en x64 |
el último dígito cambia |
El fin de línea es #13#10 o #10 |
la comparación de ficheros falla |
La cuarta fila es la más insidiosa y conecta con Fortran de esta página: Delphi para Windows de
64 bits redefinió Extended como Double, así que un cálculo financiero acumulado da otro
resultado tras migrar, sin ningún cambio en el código.
Y string como UTF-16 frente a UTF-8 (clase 093) es la otra fuente clásica: Length(s) cuenta
unidades de código, no caracteres, así que un texto con acentos da longitudes distintas en cada
plataforma.
La técnica que el ecosistema usa es la de esta clase, y es sana:
{ 1. capturar la salida del sistema actual }
GuardarResultado('caso_' + IntToStr(I) + '.esperado', Calcular(Entrada[I]));
{ 2. tras migrar, comparar }
CheckEquals(CargarEsperado(I), Calcular(Entrada[I]), 'caso ' + IntToStr(I));
Y hay una herramienta del mundo Delphi que merece nombrarse porque automatiza justo esto: el comparador de bases de datos, que ejecuta la aplicación vieja y la nueva contra copias de la misma base y compara las tablas al terminar.
Es el parallel run de COBOL, aplicado a aplicaciones de escritorio con base de datos — el mismo patrón, otra escala.
Ficha completa · IA simbólica, CAD, investigación · sbcl --script prog.lisp
(let ((x (read))
(y (read)))
(format t "equivalente=~A~%" (if (equal x y) "true" "false")))
Lo que esta clase enseña en Common Lisp. Lisp obliga a hacerse la pregunta que esta clase esconde: "equivalente" ¿en qué sentido? Y lo hace porque tiene cuatro predicados de igualdad distintos (clase 101):
(eq x y) ; el MISMO objeto: identidad
(eql x y) ; eq, o números del mismo tipo y valor, o caracteres iguales
(equal x y) ; eql, o listas/cadenas iguales elemento a elemento
(equalp x y) ; equal, ignorando mayúsculas y mezclando tipos numéricos
(equal "Hola" "hola") ; NIL
(equalp "Hola" "hola") ; T
(equalp 1 1.0) ; T ¡un entero y un real!
(equal 1 1.0) ; NIL
Esas cuatro respuestas distintas a la misma pregunta son el contenido de esta clase: el verificador de equivalencia tiene que elegir una, y la elección es una decisión de diseño con consecuencias.
¿Es 1 equivalente a 1.0? En un verificador de resultados numéricos, probablemente sí. En uno de
tipos, desde luego que no.
Y Lisp aporta una técnica que esta clase debe recoger y que la comunidad usa mucho: la comparación generativa.
(check-it:check-that
(check-it:generator (integer))
(lambda (n) (= (implementacion-vieja n) (implementacion-nueva n))))
Pruebas basadas en propiedades: en lugar de escribir casos, se declara la propiedad —"las dos implementaciones coinciden"— y la biblioteca genera cientos de entradas aleatorias, incluidos los casos extremos que nadie escribiría.
Y cuando encuentra un fallo, hace lo más valioso: reduce la entrada al caso mínimo que lo reproduce.
Esa técnica nació en Haskell con QuickCheck (1999) y hoy está en todos los ecosistemas —Hypothesis, proptest, jqwik—, y encaja con esta clase mejor que con ninguna otra: cuando lo que se quiere comprobar es que dos implementaciones coinciden, la propiedad se escribe sola.
Es, con diferencia, la forma más eficiente de verificar una reescritura: no hay que inventar casos, solo hay que decir qué debe cumplirse.
Ficha completa · Diseño de chips (EDA), redes, testing · tclsh prog.tcl
gets stdin linea
lassign [string trim $linea] x y
puts "equivalente=[expr {$x eq $y ? {true} : {false}}]"
Lo que esta clase enseña en Tcl. Tcl expone en una línea la trampa de esta clase, y es la de la
clase 101: == y eq no son lo mismo.
expr {"10" == "10.0"} ;# 1 -- compara como NÚMEROS
expr {"10" eq "10.0"} ;# 0 -- compara como CADENAS
expr {"1e3" == "1000"} ;# 1
expr {" 5" == "5"} ;# 1 -- ¡con espacio!
Un verificador de equivalencia escrito con == daría por buena una salida 10.0 donde se esperaba
10. Y a veces eso es lo correcto, y a veces es un fallo grave.
Por eso el ejemplo de esta página usa eq: el contrato del curso es sobre texto exacto (clase
040), y ahí la comparación de cadenas es la definición correcta.
Y Tcl es especialmente adecuado para el papel de esta clase por una razón práctica: es el lenguaje de pegamento por excelencia, y un verificador de equivalencia es exactamente eso.
set esperado [exec ./version_vieja < $entrada]
set obtenido [exec ./version_nueva < $entrada]
if {$esperado ne $obtenido} {
puts "DIFIERE en $entrada"
exec diff [makeTemp $esperado] [makeTemp $obtenido] >@ stdout
}
Cinco líneas y ya hay un verificador de equivalencia entre dos binarios cualesquiera. Es literalmente lo que hace el verificador de este curso, y es la razón por la que Tcl se usó durante décadas en la industria de diseño de circuitos: automatizar la comparación de salidas de herramientas distintas.
Y tcltest tiene un mecanismo pensado para esto:
customMatch equivalenteNumerico {apply {{esperado obtenido} {
expr {abs($esperado - $obtenido) < 1e-9}
}}}
test calculo-1.1 {...} -body { calcular } -match equivalenteNumerico -result 3.14159265
-match con un comparador propio permite declarar qué significa "igual" por prueba, que es
justo la decisión que Fortran y COBOL de esta página obligan a tomar.
Ficha completa · Sysadmin, texto, bioinformática · perl prog.pl
use strict;
use warnings;
my $linea = <STDIN>;
chomp $linea;
my ($x, $y) = split ' ', $linea;
print "equivalente=", ($x eq $y ? 'true' : 'false'), "\n";
Lo que esta clase enseña en Perl. Perl es, históricamente, la herramienta con la que se han hecho la mayoría de los verificadores de equivalencia del mundo real, y por un motivo que esta clase debe explicar: es lo que había instalado en todas partes y sabía manejar texto.
Un verificador completo cabe en muy poco:
use strict; use warnings;
use Test::More;
for my $caso (glob 'casos/*.in') {
my $viejo = `./sistema_viejo < $caso`;
my $nuevo = `./sistema_nuevo < $caso`;
is($nuevo, $viejo, "equivalencia en $caso");
}
done_testing();
Y con TAP (clase 139), la salida ya es consumible por cualquier integración continua.
Y Perl distingue —como Tcl de esta página— entre comparar como número y como cadena:
"10" == "10.0" # verdadero: numérico
"10" eq "10.0" # falso: textual
Y el ecosistema tiene módulos que resuelven las variantes difíciles de esta clase:
| Módulo | Para qué |
|---|---|
| Test::Deep | estructuras con comodines: num(3.14, 0.01), ignore() |
| Text::Diff | la diferencia legible entre dos salidas |
| Test::Differences | eq_or_diff: falla mostrando un diff en columnas |
| Test::Files | comparar ficheros y árboles completos |
Test::Deep merece el detalle, porque resuelve el problema real de comparar respuestas que
contienen partes que deben cambiar:
cmp_deeply($respuesta, {
id => ignore(), # cambia en cada ejecución
creado_en => re(qr/^\d{4}-\d{2}/), # una fecha, no importa cuál
total => num(120.50, 0.01), # con tolerancia
items => bag(@esperados), # el mismo conjunto, otro orden
});
ignore(), re(), num() con tolerancia y bag() para orden indiferente son exactamente las
cuatro excepciones que aparecen en toda comparación de sistemas reales: identificadores, marcas de
tiempo, redondeo y orden.
Declararlas explícitamente es mejor que filtrarlas con expresiones regulares antes de comparar, porque queda escrito qué se está ignorando y por qué — que es la disciplina que el cierre de esta clase pide.
Ficha completa · Videojuegos, navegadores, finanzas, HPC · g++ -std=c++17 prog.cpp
#include <iostream>
#include <string>
int main() {
std::string x, y;
if (!(std::cin >> x >> y)) return 1;
std::cout << "equivalente=" << (x == y ? "true" : "false") << '\n';
return 0;
}
Lo que esta clase enseña en C++. C++ aporta a esta clase el problema más incómodo, y es el de la Parte 8: el comportamiento indefinido hace que "equivalente" pueda dejar de tener sentido (clase 136).
Dos compilaciones del mismo código pueden dar resultados distintos si hay comportamiento indefinido de por medio, y ninguna de las dos es "la correcta". Un verificador que compara dos versiones puede estar comparando dos comportamientos igualmente inválidos.
De ahí que en C++ el verificador de equivalencia se combine siempre con los desinfectantes:
g++ -O0 -fsanitize=address,undefined -o viejo_dbg viejo.cpp
g++ -O2 -o viejo_opt viejo.cpp
# si viejo_dbg y viejo_opt discrepan, el problema NO es la comparación
Comparar el mismo programa consigo mismo con y sin optimización es el primer paso, y detecta el comportamiento indefinido antes de que contamine la comparación real.
Y C++ tiene una herramienta específica para esta clase que merece conocerse:
csmith | tee prog.c # genera programas C aleatorios SIN comportamiento indefinido
gcc prog.c -o a1; clang prog.c -o a2
./a1; ./a2 # si difieren, uno de los DOS COMPILADORES tiene un fallo
Csmith genera programas aleatorios garantizadamente bien definidos y compara compiladores entre sí. Encontró cientos de errores reales en GCC, Clang y otros.
Es el verificador de equivalencia aplicado un nivel más abajo —a las implementaciones del lenguaje, no a los programas— y es la misma idea de esta clase.
Y sobre la comparación numérica, C++20 añadió lo que faltaba:
#include <compare>
auto r = a <=> b; // orden de tres vías
std::is_eq(r); std::is_lt(r);
// y para coma flotante, lo de siempre:
std::abs(a - b) <= tol * std::max(std::abs(a), std::abs(b));
Y la letra pequeña que arruina comparaciones y merece decirse: std::partial_ordering::unordered.
Con NaN de por medio, a <=> b no devuelve ni menor, ni igual, ni mayor: devuelve "no
comparable" — porque NaN != NaN.
Es la formalización, en el sistema de tipos, de algo que Fortran de esta página lleva sufriendo desde 1985: en coma flotante, la igualdad no es una relación de equivalencia.
Estos lenguajes no pueden leer de stdin y escribir en stdout sin dejar de ser ellos
mismos. No es una limitación del material: es su naturaleza. El cálculo es el mismo y la forma
de entrar y salir es la de su anfitrión. No pasan por el verificador, y se dice.
Ficha completa · IBM i: ERP, retail, logística, manufactura · CRTBNDRPG sobre IBM i
En IBM i un programa recibe sus datos por parámetros, por un fichero o por una pantalla, nunca por la entrada estándar.
**free
ctl-opt dftactgrp(*no) actgrp(*caller);
dcl-pi EQUIVAL;
x char(20) const;
y char(20) const;
end-pi;
dcl-s res varchar(5);
if %trim(x) = %trim(y);
res = 'true';
else;
res = 'false';
endif;
dsply ('equivalente=' + res);
*inlr = *on;
return;
Lo que esta clase enseña en RPG. IBM i vive esta clase de una forma particular y muy instructiva: el sistema viejo y el nuevo conviven en la misma máquina, y esa convivencia es el modelo de modernización de la plataforma.
Un mismo trabajo puede llamar a un programa RPG y a una clase Java. Una tabla DB2 la leen los dos. Y la lista de bibliotecas decide cuál se ejecuta (clase 139):
CHGLIBL LIBL(NUEVO VIEJO COMUN) <-- el nuevo tiene prioridad
CHGLIBL LIBL(VIEJO COMUN) <-- vuelta atrás inmediata
Ese es el mecanismo de despliegue y de reversión más simple de esta página (clase 148): cambiar el orden de una lista.
Y para la comparación, la plataforma da una capacidad que casi ningún sistema tiene de serie: el diario de la base de datos.
STRJRNPF FILE(CLIENTES) JRN(MIJRN) IMAGES(*BOTH)
IMAGES(*BOTH) registra la imagen anterior y la posterior de cada cambio de cada fila, con el
trabajo, el usuario, el programa y la marca de tiempo.
Y eso se consulta con SQL:
SELECT * FROM TABLE(QSYS2.DISPLAY_JOURNAL('MIBIB', 'MIJRN'))
WHERE JOURNAL_ENTRY_TYPE IN ('UP','PT','DL')
Con el diario, el verificador de equivalencia no compara salidas: compara los cambios que cada sistema hizo en la base de datos. Se ejecuta el viejo, se anota el diario; se restaura, se ejecuta el nuevo, se anota; y se comparan las secuencias de cambios.
Es una forma de equivalencia más fuerte que comparar salidas, porque captura los efectos secundarios además del resultado — que es justo lo que se escapa en una prueba de caja negra.
Y es una idea transferible a cualquier ecosistema con una base de datos que soporte captura de cambios: si dos sistemas producen la misma secuencia de cambios, son equivalentes en lo que importa.
Sí podrían cumplir el contrato, pero su cadena de herramientas no está en los runners de CI.
Ficha completa · Mainframe z/OS: banca, seguros · IBM Enterprise PL/I for z/OS
equival: procedure options(main);
declare (x, y) fixed binary(31);
get list (x, y);
if x = y then
put skip list ('equivalente=true');
else
put skip list ('equivalente=false');
end equival;
Lo que esta clase enseña en PL/I. PL/I es el lenguaje donde la palabra equivalencia tiene un significado técnico extra que merece explicarse, porque es una trampa histórica de esta clase.
declare 1 registro,
2 codigo char(4),
2 importe fixed decimal(9,2);
declare texto char(10) based(addr(registro)); /* la MISMA memoria, otro tipo */
based y defined permiten que dos declaraciones distintas se refieran a la misma memoria —lo que
en Fortran es EQUIVALENCE y en C es una unión.
Y eso es exactamente lo contrario de lo que esta clase busca: dos vistas del mismo dato, no dos implementaciones del mismo cálculo. Conviene tener clara la diferencia porque el vocabulario colisiona.
Lo que PL/I sí aporta a la clase, y es de peso, es la razón por la que las migraciones desde PL/I son tan delicadas: el sistema de conversiones.
declare a fixed decimal(5,2);
declare b fixed binary(31);
declare c char(10);
a = b; /* binario -> decimal: puede truncar */
c = a; /* decimal -> texto: con un formato IMPLÍCITO */
a = c; /* texto -> decimal: condición CONVERSION si no es numérico */
Cada una de esas asignaciones tiene reglas de conversión definidas en el estándar, con decenas de páginas de detalle — y un reescritor en otro lenguaje tiene que reproducirlas exactamente.
Y la más traicionera es la de la primera línea de esta explicación: fixed decimal(5,2) tiene
precisión y escala declaradas, y cada operación aritmética calcula la precisión del resultado según
reglas del estándar:
fixed decimal(5,2) * fixed decimal(3,1) -> fixed decimal(9,3)
El tipo del resultado se deduce, y si excede la precisión máxima, se trunca por la izquierda con la
condición SIZE.
Reproducir eso en un lenguaje con double es imposible; reproducirlo con BigDecimal requiere
implementar las reglas de precisión de PL/I a mano.
Es la misma lección que COBOL en esta página, subida de nivel: la aritmética decimal con precisión declarada no se migra sola, y es la razón número uno de discrepancias en las migraciones de sistemas financieros.
Ficha completa · Sanidad: historia clínica, VistA, Epic · YottaDB
EQUIVAL ; Verificador de equivalencia -- clase 140
read linea
set x = $piece(linea, " ", 1)
set y = $piece(linea, " ", 2)
write "equivalente=", $select(x = y : "true", 1 : "false"), !
quit
Lo que esta clase enseña en M. M expone la misma trampa que Tcl en esta página, y de forma más
brusca: = en M compara según el contexto, y M convierte agresivamente.
write ("10" = "10.0") ; 0 -- comparación de CADENAS, distintas
write (10 = "10.0") ; 0 -- también cadenas: M compara texto con =
write (+"10" = +"10.0") ; 1 -- el + fuerza interpretación NUMÉRICA
write ("10abc" + 0) ; 10 -- ¡convierte el prefijo numérico y descarta el resto!
La cuarta línea es la característica más peligrosa de M para esta clase: "10abc" + 0 da 10 sin
error ninguno, porque M interpreta el prefijo numérico y descarta lo demás.
Un verificador de equivalencia que compare numéricamente en M daría por iguales "10" y "10abc".
Por eso el operador correcto para comparar salidas textualmente es = sobre cadenas, y el numérico se
fuerza con + explícito. Es la misma distinción eq/== de Tcl y Perl, resuelta con un prefijo en
lugar de con dos operadores.
Y M aporta a esta clase una técnica que su modelo de datos hace natural y que es genuinamente útil: comparar dos globals enteras recorriéndolas en orden.
comparar(g1, g2) ;
new s1, s2, dif
set (s1, s2) = "", dif = 0
for do quit:(s1 = "") & (s2 = "")
. set s1 = $order(@g1@(s1))
. set s2 = $order(@g2@(s2))
. if s1 '= s2 set dif = dif + 1 quit
. if s1 '= "", @g1@(s1) '= @g2@(s2) set dif = dif + 1
quit dif
$order recorre los subíndices en orden colativo (clase 095), así que dos globals se comparan en
un solo recorrido simultáneo, sin cargar nada en memoria y sin ordenar.
Es exactamente el algoritmo de diff sobre ficheros ordenados, aplicado a la base de datos, y en los
sistemas VistA es la forma habitual de verificar una migración de datos: recorrer las dos globals a la
vez y contar diferencias.
Y encaja con la conclusión de RPG en esta página: cuando el estado vive en la base de datos, la equivalencia se comprueba sobre el estado, que es más fuerte que comprobarla sobre la salida.
Ficha completa · Banca, seguros, trading · Pharo
| linea partes x y |
linea := stdin nextLine trimBoth.
partes := linea substrings: ' '.
x := partes at: 1.
y := partes at: 2.
Transcript show: 'equivalente=', (x = y ifTrue: [ 'true' ] ifFalse: [ 'false' ]); cr.
Lo que esta clase enseña en Smalltalk. Smalltalk plantea la pregunta de esta clase con la máxima claridad, porque la igualdad es un mensaje y por tanto es negociable:
a == b "identidad: ¿el MISMO objeto? -- no se puede redefinir"
a = b "igualdad: la define CADA CLASE"
= es un método ordinario, y una clase puede definir lo que quiera:
Medida >> = otra
^ (self valorEnMetros - otra valorEnMetros) abs < 0.001
Medida >> hash
^ (self valorEnMetros roundTo: 0.001) hash
La tolerancia de equivalencia se declara en el objeto, y desde ese momento todo el sistema la
usa: los conjuntos, los diccionarios, includes:, y por supuesto las pruebas.
Es la respuesta más limpia de esta página a la pregunta "¿qué significa igual?", porque la respuesta vive junto al dato al que se refiere en lugar de repetirse en cada comparación.
Y el aviso que va con ella, y que es la regla más citada de la programación orientada a objetos: si
redefines =, tienes que redefinir hash, o los conjuntos y diccionarios se romperán en silencio
(clase 094).
Y Smalltalk aporta a esta clase dos herramientas propias de su modelo:
Primera, storeString como base de la comparación (clase 105):
objetoA storeString = objetoB storeString
Compara la representación serializada completa, incluidos los objetos anidados — que es la
comparación estructural profunda de is_deeply de Perl, obtenida gratis.
Y segunda, la comparación de imágenes: como el estado del sistema entero es un objeto, se puede guardar la imagen antes de una operación, ejecutar las dos implementaciones y comparar el estado resultante.
Es el equivalente al diario de IBM i de esta página, conseguido por la vía del modelo de objetos: si el estado es inspeccionable, la equivalencia se puede comprobar sobre el estado.
Y cierra donde empezó esta clase: el verificador de equivalencia no compara programas, compara lo que se puede observar de ellos. Definir qué es observable es la decisión de ingeniería; lo demás es recorrer y comparar.
Lo transferible: una prueba de integración comprueba una frontera; el verificador de equivalencia comprueba una sustitución. Y el segundo es la herramienta más valiosa que existe para cambiar algo grande sin miedo, porque convierte una migración en una operación con red: si el nuevo produce lo mismo que el viejo para todo lo que ha pasado por el sistema, se puede cambiar. La disciplina que hay que aprender es guardar entradas y salidas reales, porque sin ellas no hay red — y esa decisión hay que tomarla antes de necesitarla.
⏮️ Volver a la clase 140 · 🧬 Los primos del Atlas · 🧟 Índice de lenguajes vivos