⬅️ Volver a la clase 050 · 🧬 Primos del Atlas · 🧟 Índice de lenguajes vivos · 📚 Índice
Sumar un entero y un real. La operación más inocente del programa, y la que obliga a cada lenguaje a declarar su postura: ¿cuándo se decide que esto es una suma de reales? ¿Al compilar, mirando las declaraciones? ¿Al ejecutar, mirando los valores? ¿O no se decide nunca, porque no hay tipos que mirar?
🎯 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 el momento en que se resuelve el tipo, y estos lenguajes cubren el espectro entero con posturas más nítidas que el núcleo. Ada representa el extremo estático absoluto: la suma no compila sin una conversión escrita, aunque sea evidente lo que quieres. COBOL decide al compilar, pero la conversión la esconde en el
MOVE. Fortran, Pascal y C++ promocionan solos en la dirección segura. Y Tcl, M, Perl y Smalltalk no deciden nada hasta que la operación se ejecuta.Lo interesante es que estático y dinámico no es una escala de rigor: Smalltalk es dinámico y fuertemente tipado, Tcl es dinámico y débil, y C++ es estático y permite conversiones que pierden datos. Son dos ejes distintos, y esta clase junto con la 051 los separa.
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.
a b (a entero, b real) → stdout: suma=<a+b con 2 decimales>suma = a + b (a entero promovido a real)| stdin | esperado |
|---|---|
2 3.5 |
suma=5.50 |
10 0.25 |
suma=10.25 |
0 0 |
suma=0.00 |
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. MIXTO.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 LINEA PIC X(80).
01 TXT-A PIC X(20).
01 TXT-B PIC X(20).
01 A PIC S9(9) COMP-3.
01 B PIC S9(9)V99 COMP-3.
01 S PIC S9(9)V99 COMP-3.
01 ED-S PIC -(9)9.99.
PROCEDURE DIVISION.
ACCEPT LINEA
UNSTRING LINEA DELIMITED BY ALL SPACES INTO TXT-A TXT-B
MOVE FUNCTION NUMVAL(TXT-A) TO A
MOVE FUNCTION NUMVAL(TXT-B) TO B
COMPUTE S = A + B
MOVE S TO ED-S
DISPLAY "suma=" FUNCTION TRIM(ED-S)
STOP RUN.
Lo que esta clase enseña en COBOL. COBOL es estático hasta el extremo: el tipo, el número de dígitos, la posición de la coma decimal y la representación física de cada dato están decididos antes de compilar, y no pueden cambiar. Una variable no puede contener a veces un número y a veces un texto; ni siquiera puede contener un número con más dígitos de los declarados.
Pero —y esta es la parte interesante— la conversión entre tipos numéricos es totalmente
implícita. COMPUTE S = A + B con A sin decimales y B con dos funciona sin decir nada: el
compilador alinea las comas decimales, hace la suma y ajusta al destino. COBOL sabe exactamente qué
está pasando porque todo está declarado, así que no necesita preguntar.
Eso lo sitúa en una casilla que casi ningún lenguaje moderno ocupa: estático, fuerte en cuanto a la forma del dato, y permisivo en la aritmética. La comprobación no está en prohibir la mezcla, está en que la mezcla se resuelve con reglas deterministas y documentadas.
El precio es el de la clase 049: cuando el destino no da de sí, el resultado se trunca en silencio. COBOL confía en que declaraste bien.
Ficha completa · HPC, clima, física, BLAS/LAPACK · gfortran -O2 prog.f90
program mixto
implicit none
integer :: a
real(kind=8) :: b, s
character(len=32) :: buf
read(*, *) a, b
s = a + b ! promoción implícita: a se convierte a real
write(buf, '(F20.2)') s
write(*, '(A,A)') 'suma=', trim(adjustl(buf))
end program mixto
Lo que esta clase enseña en Fortran. Fortran es estático y promociona en la dirección segura:
en a + b con a entero y b real, el entero se convierte a real y la suma es real. La regla se
llama conversión aritmética y está en el estándar; no hay ambigüedad.
Lo que hay que vigilar es que la promoción ocurre en la expresión, no en la asignación, y esa distinción es la fuente del error más clásico del lenguaje:
real(kind=8) :: x
x = 1 / 2 ! x vale 0.0 -- la división es ENTERA, y luego se promociona
x = 1.0d0 / 2 ! x vale 0.5 -- ahora la división ya es real
El destino no influye en cómo se evalúa la expresión. Es el mismo comportamiento de C, Java y Go, y el mismo error.
Y implicit none vuelve a ser decisivo en esta clase: sin él, Fortran es estático pero con tipos
adivinados. Una variable sin declarar recibe tipo según su inicial, así que total = a + b podría
estar sumando en entero sin que aparezca ninguna declaración que lo delate. Es tipado estático con la
mitad de las garantías, y es la razón de que esa línea sea obligatoria en cualquier Fortran serio.
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;
with Ada.Long_Float_Text_IO; use Ada.Long_Float_Text_IO;
procedure Mixto is
A : Integer;
B, S : Long_Float;
begin
Get (A);
Get (B);
-- S := A + B; NO COMPILA. La conversión es obligatoria y visible.
S := Long_Float (A) + B;
Put ("suma="); Put (S, Fore => 1, Aft => 2, Exp => 0);
New_Line;
end Mixto;
Lo que esta clase enseña en Ada. La línea comentada es el contenido entero de la clase: A + B
no compila. Ada no promociona nada, nunca, entre tipos numéricos distintos. Ni siquiera en la
dirección segura que Fortran, Pascal, C++ y Java permiten sin decir nada.
Y va más lejos de lo que parece, porque en Ada dos tipos con la misma representación siguen siendo distintos:
type Metros is new Float;
type Pies is new Float;
M : Metros := 100.0;
P : Pies := 50.0;
-- X := M + P; NO COMPILA: son tipos distintos
X : Metros := M + Metros (Float (P) * 0.3048); -- la conversión declara la intención
Eso es tipado nominal fuerte: el nombre del tipo importa, no solo su forma. Es la característica que habría evitado la pérdida de la sonda Mars Climate Orbiter, y la razón de que Ada siga en aviónica.
El coste es real y conviene reconocerlo: el código es más largo y hay conversiones por todas partes. La apuesta de Ada es que escribir la conversión obliga a pensarla, y que en un sistema donde un fallo cuesta vidas, ese coste sale barato. En un guion de cinco líneas, no.
Ficha completa · Escritorio empresarial, TPV, industria (vía Delphi y Free Pascal) · fpc -Mobjfpc prog.pas
program Mixto;
{$MODE OBJFPC}{$H+}
var
A: Integer;
B, S: Double;
begin
Read(A, B);
S := A + B; { Pascal promociona en la dirección segura }
WriteLn('suma=', S:0:2);
end.
Lo que esta clase enseña en Pascal. Pascal ocupa la casilla intermedia con una regla explícita y fácil de recordar: promociona hacia donde no se pierde información, y prohíbe lo demás.
S := A + B; { Integer + Double -> Double. Correcto. }
A := S; { NO COMPILA: hay que escribir Trunc(S) o Round(S) }
Es la misma política que Fortran, con la diferencia de que Pascal la aplica también a los tipos
definidos por el usuario. Un subrango 1..10 se asigna a un Integer sin problema, pero al revés
requiere comprobación de rango en ejecución con {$R+}.
Y esta clase toca un punto donde Pascal fue más estricto que casi todos: los tipos
incompatibles por nombre. type Metros = Integer crea un alias compatible, pero
type Metros = type Integer —con la palabra type repetida— crea un tipo distinto que no se
mezcla, exactamente como el new de Ada. Delphi lo usa mucho para que el compilador distinga
identificadores que son todos enteros pero significan cosas distintas.
Es una capacidad que muy pocos lenguajes tienen y que hoy se pide constantemente con el nombre de
newtype o branded types: TypeScript la simula con trucos, Rust la tiene con struct Metros(f64),
y Pascal la tenía en 1970.
Ficha completa · IA simbólica, CAD, investigación · sbcl --script prog.lisp
(setf *read-default-float-format* 'double-float)
(let* ((a (read))
(b (read))
(s (+ a b)))
(format t "suma=~,2F~%" s))
Lo que esta clase enseña en Common Lisp. Lisp es dinámico y fuertemente tipado, y esta
combinación es la que más cuesta ver desde fuera. Dinámico: el tipo pertenece al valor, y a
puede contener hoy un entero y mañana una lista. Fuerte: (+ 1 "dos") no convierte nada, levanta
un error de tipo en ejecución.
Y la aritmética aplica la regla de contagio de la torre numérica que apareció en la clase 043: al
mezclar un entero exacto con un real inexacto, el resultado es inexacto. (+ 2 3.5d0) da 5.5d0, y
(+ 1/2 1/3) da 5/6 porque los dos son exactos.
Lo que Lisp añade y que casi ningún dinámico tiene es la posibilidad de declarar tipos cuando interesa, sin dejar de ser dinámico:
(defun suma (a b)
(declare (type double-float a b)
(optimize (speed 3) (safety 0)))
(+ a b))
Con esa declaración, SBCL genera código nativo tan rápido como el de C, sin comprobaciones. Sin ella, la misma función acepta cualquier número. Es tipado gradual —lo que hoy hacen TypeScript, Python con anotaciones o Sorbet en Ruby— disponible en el estándar de 1994. Y SBCL va más lejos: infiere y avisa en tiempo de compilación de incompatibilidades que puede demostrar, aunque no hayas declarado nada.
Ficha completa · Diseño de chips (EDA), redes, testing · tclsh prog.tcl
gets stdin linea
lassign [split [string trim $linea]] a b
set s [expr {$a + $b}]
puts "suma=[format %.2f $s]"
Lo que esta clase enseña en Tcl. No hay tipos, así que no hay nada que decidir al compilar. La
pregunta "¿esto es una suma de enteros o de reales?" la responde expr, en el momento de
ejecutar, mirando el aspecto del texto:
expr {2 + 3} ;# 5 -- los dos parecen enteros
expr {2 + 3.5} ;# 5.5 -- uno parece real, promociona
expr {"2" + "3"} ;# 5 -- las comillas no cambian nada
expr {2 + "hola"} ;# ERROR -- Tcl SÍ falla aquí
Esa última línea importa: Tcl es de tipado débil, pero no es JavaScript. No inventa un resultado; lanza un error. La debilidad de Tcl consiste en aceptar que cualquier cadena que parezca un número lo sea, no en convertir cualquier cosa a cualquier cosa.
Y hay un detalle de implementación que explica por qué esto no es lento: Tcl guarda junto a la cadena una representación interna con su tipo real —entero, real, lista, comando compilado—, y solo la regenera si el valor cambia. Un bucle que sume un millón de veces no reanaliza el texto un millón de veces. La semántica es "todo es cadena"; la implementación es otra cosa.
Ese truco —conocido como dual-ported object— es de 1997 y es el antepasado directo de las clases ocultas de los motores de JavaScript modernos.
Ficha completa · Sysadmin, texto, bioinformática · perl prog.pl
use strict;
use warnings;
my $linea = <STDIN>;
chomp $linea;
my ($a_val, $b_val) = split ' ', $linea;
printf "suma=%.2f\n", $a_val + $b_val;
Lo que esta clase enseña en Perl. Perl es dinámico y débil, y aquí ni siquiera hay una
decisión que tomar: $a_val y $b_val contienen texto leído de la entrada, y el operador + los
lee como números porque eso es lo que hace +. No hay promoción de entero a real porque no hay
entero ni real: hay un escalar que sabe comportarse como ambos.
Internamente, un escalar de Perl (una estructura SV) tiene ranuras para el valor entero (IV), el
real (NV) y la cadena (PV), y va rellenando la que haga falta. Por eso $x = "3.5"; $y = $x + 0;
no "convierte": simplemente materializa la ranura numérica y la conserva para la próxima vez.
La consecuencia práctica es que el tipo lo elige el operador, no el dato, y de ahí que Perl necesite dos juegos de operadores —tema de la clase 051—. Y también que la única forma de comprobar algo sea preguntar explícitamente:
use Scalar::Util qw(looks_like_number);
die "no es un número" unless looks_like_number($b_val);
Nota sobre el estilo: las variables se llaman $a_val y $b_val y no $a y $b a propósito.
$a y $b son variables especiales que Perl usa internamente en sort, y aunque use strict
las deja pasar, usarlas para otra cosa es una de esas trampas que solo muerden mucho después.
Ficha completa · Videojuegos, navegadores, finanzas, HPC · g++ -std=c++17 prog.cpp
#include <iomanip>
#include <iostream>
int main() {
int a{};
double b{};
if (!(std::cin >> a >> b)) return 1;
const double s = a + b; // promoción aritmética implícita
std::cout << "suma=" << std::fixed << std::setprecision(2) << s << '\n';
return 0;
}
Lo que esta clase enseña en C++. C++ es estático y con conversiones aritméticas implícitas definidas por un conjunto de reglas —las usual arithmetic conversions— que casi nadie ha leído entero y que producen sorpresas genuinas:
int a = -1;
unsigned int b = 1;
if (a < b) { } // FALSO: a se convierte a unsigned y vale 4294967295
short x = 30000, y = 30000;
int z = x + y; // ambos se promocionan a int: 60000, correcto
short w = x + y; // desbordamiento al volver a short
La regla de fondo es que los operandos se promocionan al tipo "más grande" de la expresión, y que
unsigned gana a signed del mismo rango. Ese último punto es el origen de una familia entera de
errores, y la razón de que las guías modernas recomienden no usar unsigned para cantidades que solo
son "no negativas".
Lo que C++ ofrece a cambio es que todas esas conversiones se pueden hacer visibles: compilar con
-Wconversion -Wsign-conversion convierte cada promoción con pérdida en un aviso. Y desde C++11, la
inicialización con llaves las prohíbe donde importa:
int n1 = 3.7; // compila, n1 vale 3
int n2 {3.7}; // NO COMPILA: narrowing conversion
Es la misma política de Ada, disponible como opción en vez de como norma.
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 MIXTO;
a int(10) const;
b packed(15:2) const;
end-pi;
dcl-s s packed(15:2);
dcl-s salida char(40);
s = a + b; // entero + decimal: RPG alinea y suma
salida = 'suma=' + %char(s);
dsply salida;
*inlr = *on;
return;
Lo que esta clase enseña en RPG. RPG es estático y, como COBOL, resuelve la mezcla numérica sin protestar: alinea las comas decimales y ajusta al destino. El compilador lo puede hacer porque conoce exactamente los dígitos y decimales de cada operando.
Lo que RPG añade y que conviene conocer es que el resultado intermedio también tiene precisión
declarada, y ahí está el riesgo. Al multiplicar dos packed(15:2), el resultado natural tendría 30
dígitos con 4 decimales, que excede el máximo del tipo (63 dígitos en versiones recientes, 31 en las
antiguas). Si el destino no da de sí, se trunca.
Por eso existe la palabra clave eval-corr para estructuras y, sobre todo, la posibilidad de
forzar la precisión intermedia:
s = %dech(a * b : 15 : 2); // decide TÚ los dígitos y el redondeo
Y hay una diferencia con COBOL que salta a la vista en el tipado: RPG sí distingue int de
packed, es decir, binario de decimal, mientras que COBOL lo trata como una cláusula de
almacenamiento (COMP frente a COMP-3) sobre el mismo PIC. Dos formas de decir lo mismo, con
consecuencias distintas al leer el código: en RPG el tipo lo dices, en COBOL lo dice la forma.
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
mixto: procedure options(main);
declare a fixed binary(31);
declare b fixed decimal(15,2);
declare s fixed decimal(15,2);
declare pres picture 'ZZZZZZZZZ9V.99';
get list (a, b);
s = a + b; /* binaria + decimal: PL/I convierte segun sus reglas */
pres = s;
put skip list ('suma=' || trim(pres));
end mixto;
Lo que esta clase enseña en PL/I. PL/I es estático y el campeón absoluto de la conversión
implícita. Aquí se suman un fixed binary y un fixed decimal —bases distintas, no solo tipos
distintos— y el lenguaje lo resuelve sin decir nada: convierte el binario a decimal según una regla
del estándar y opera.
Esa regla existe, está documentada y ocupa varias páginas. Define la precisión del resultado intermedio a partir de la de los operandos, para cada combinación de base y escala. Es determinista y prácticamente imposible de recordar, y ese es exactamente el problema que Dijkstra señalaba: un lenguaje puede ser completamente especificado y aun así inmanejable, si nadie puede predecir su comportamiento sin consultar el manual.
La lección de diseño que deja PL/I para esta clase es la más valiosa del programa: hay una diferencia entre "el lenguaje sabe qué hacer" y "el programador sabe qué va a pasar". COBOL acierta porque sus reglas son pocas; Ada acierta porque no tiene ninguna; PL/I falla porque tiene demasiadas.
Cuando en un lenguaje moderno alguien discute si añadir una conversión implícita más, este es el precedente que hay que citar.
Ficha completa · Sanidad: historia clínica, VistA, Epic · YottaDB
MIXTO ; Estatico vs dinamico -- clase 050
read linea
set a = $piece(linea, " ", 1)
set b = $piece(linea, " ", 2)
set s = a + b
write "suma=", $justify(s, 0, 2), !
quit
Lo que esta clase enseña en M. M es el caso más puro de tipado dinámico y débil: no hay tipos
que resolver ni al compilar ni al ejecutar. a y b son cadenas, + las lee como números, y el
resultado es una cadena que representa un número.
Y aquí conviene mirar algo que no se ve en ningún otro lenguaje de esta página: la ausencia de tipos llega también a la base de datos. Un global de M no tiene esquema, así que el mismo nodo puede contener hoy un número y mañana un texto, y las claves del árbol se ordenan con una regla mixta — primero la cadena vacía, luego los números en orden numérico, luego el resto en orden de colación—.
Esa regla de ordenación numérica antes que alfabética es una decisión de diseño con cincuenta años que hoy resulta familiar: es lo que hacen los índices de una base de datos documental. M era NoSQL sin esquema en 1966, con las mismas ventajas —flexibilidad ante datos irregulares— y los mismos inconvenientes —ninguna garantía, validación siempre por cuenta del programador—.
Por eso VistA construyó FileMan encima: un diccionario que define, para cada campo, su tipo, su rango, su obligatoriedad y sus reglas. Es literalmente el sistema de tipos que el lenguaje no tiene, implementado como datos dentro de la propia base de datos.
Ficha completa · Banca, seguros, trading · Pharo
| partes a b |
partes := stdin nextLine substrings collect: [ :cada | cada asNumber ].
a := partes first.
b := partes second.
Transcript
show: 'suma=', ((a + b) asFloat printShowingDecimalPlaces: 2);
cr.
Lo que esta clase enseña en Smalltalk. Smalltalk es dinámico y fuertemente tipado, y es el
mejor ejemplo de que esos dos ejes son independientes. No hay declaraciones de tipo en ninguna parte,
pero 3 + 'hola' no produce un resultado raro: produce un error, porque SmallInteger no sabe
sumar una cadena.
Y la forma en que ocurre la suma mixta es lo específico de esta clase. a + b con a entero y b
real no es una regla del lenguaje: es doble despacho. SmallInteger>>+ recibe un argumento que no
sabe manejar y hace lo único sensato — le pregunta al argumento:
Number >> + aNumber
^ self generality < aNumber generality
ifTrue: [ (aNumber coerce: self) + aNumber ]
ifFalse: [ self + (self coerce: aNumber) ]
Cada clase numérica declara su generalidad —entero < fracción < real < complejo— y la de menor generalidad se convierte a la de mayor. La torre numérica de Lisp existe aquí también, pero implementada como código de biblioteca que puedes leer y extender, no como una regla del compilador.
La consecuencia es notable: si defines una clase Dinero y le das generality y coerce:, se
integra en la aritmética existente y 3 + unDinero funciona. Añadir un tipo numérico al lenguaje no
requiere tocar el lenguaje.
La pregunta que deja esta página no es "¿estático o dinámico?" sino "¿cuánto sabe el compilador antes de ejecutar, y qué hace con lo que no sabe?". Ada lo sabe todo y se niega a suponer. C++ lo sabe casi todo y supone en silencio. Smalltalk no sabe nada hasta que llega el objeto, pero cuando llega comprueba de verdad. Y Tcl y M no comprueban nunca. Las cuatro posturas tienen sistemas en producción desde hace décadas, así que ninguna es simplemente incorrecta.
⏮️ Volver a la clase 050 · 🧬 Los primos del Atlas · 🧟 Índice de lenguajes vivos