⬅️ Volver a la clase 114 · 🧬 Primos del Atlas · 🧟 Índice de lenguajes vivos · 📚 Índice
Doblar cada elemento sin tocar la lista original. Es map, la operación fundacional del paradigma
funcional, y su nombre viene de aquí: maplist está en el manual de Lisp de 1960. Y hay una
sorpresa en esta página: Fortran resuelve esta clase sin bucle y sin función auxiliar —v * 2— y
lo hace por una razón que no tiene nada que ver con la elegancia funcional.
🎯 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 transformación sin mutación, y estos lenguajes lo enseñan porque muestran las dos motivaciones históricas, que son distintas. Lisp (1958) llegó a las funciones puras por el cálculo lambda y el razonamiento matemático. Fortran 90 llegó a lo mismo por el rendimiento:
v = v * 2ypureexisten para que el compilador pueda vectorizar y paralelizar, y esa es la razón por la que Fortran tiene la única declaración de pureza comprobada de toda la página.Dos caminos opuestos —la demostración y la máquina— hacia la misma propiedad: si nada muta, el orden no importa.
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.
doblados=<cada x·2 unidos por ->doblados = map(x → 2x, lista)| stdin | esperado |
|---|---|
1 2 3 |
doblados=2-4-6 |
5 |
doblados=10 |
2 4 |
doblados=4-8 |
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. FUNC1.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 LINEA PIC X(200).
01 TOKEN PIC X(20).
01 TLEN PIC 9(2) COMP VALUE 0.
01 I PIC 9(4) COMP.
01 L PIC 9(4) COMP.
01 N PIC 9(4) COMP VALUE 0.
01 ORIGEN.
05 ELEM PIC S9(9) COMP-3 OCCURS 100 TIMES.
01 DESTINO.
05 ELEM-D PIC S9(18) COMP-3 OCCURS 100 TIMES.
01 SALIDA PIC X(200) VALUE SPACES.
01 SPOS PIC 9(4) COMP VALUE 1.
01 ED PIC -(17)9.
01 TXT PIC X(20).
PROCEDURE DIVISION.
ACCEPT LINEA
MOVE SPACES TO TOKEN
PERFORM VARYING I FROM 1 BY 1 UNTIL I > 200
IF LINEA(I:1) = SPACE
PERFORM CERRAR-TOKEN
ELSE
ADD 1 TO TLEN
MOVE LINEA(I:1) TO TOKEN(TLEN:1)
END-IF
END-PERFORM
PERFORM CERRAR-TOKEN
*> La tabla ORIGEN no se toca: el resultado va a DESTINO
PERFORM VARYING I FROM 1 BY 1 UNTIL I > N
COMPUTE ELEM-D(I) = ELEM(I) * 2
END-PERFORM
PERFORM VARYING I FROM 1 BY 1 UNTIL I > N
MOVE ELEM-D(I) TO ED
MOVE FUNCTION TRIM(ED) TO TXT
MOVE 0 TO L
INSPECT FUNCTION REVERSE(TXT) TALLYING L FOR LEADING SPACE
COMPUTE L = 20 - L
IF I > 1
MOVE "-" TO SALIDA(SPOS:1)
ADD 1 TO SPOS
END-IF
MOVE TXT(1:L) TO SALIDA(SPOS:L)
ADD L TO SPOS
END-PERFORM
COMPUTE L = SPOS - 1
DISPLAY "doblados=" SALIDA(1:L)
STOP RUN.
CERRAR-TOKEN.
IF TLEN > 0
ADD 1 TO N
COMPUTE ELEM(N) = FUNCTION NUMVAL(TOKEN)
MOVE SPACES TO TOKEN
MOVE 0 TO TLEN
END-IF.
Lo que esta clase enseña en COBOL. COBOL no tiene nada funcional: no hay map, no hay
funciones de primera clase (clase 085) y no hay estructuras inmutables. Lo que hace este programa es
lo único posible: escribir el resultado en una tabla distinta y no tocar la original.
Y eso, que parece pobre, resulta ser la disciplina central del procesamiento por lotes.
FICHERO-ENTRADA → programa → FICHERO-SALIDA
Un lote COBOL clásico no modifica su entrada: la lee y escribe un fichero nuevo. El de ayer sigue ahí. Si algo sale mal, se vuelve a ejecutar con el mismo fichero de entrada y se obtiene exactamente el mismo resultado.
Eso es una función pura a escala de sistema: misma entrada, misma salida, sin efectos sobre lo que había. Y no es teoría — es la razón de que los procesos nocturnos de un banco se puedan repetir cuando falla uno intermedio, y de que exista la práctica de conservar generaciones de ficheros.
En z/OS eso tiene incluso soporte del sistema: los grupos de datos generacionales.
//SALIDA DD DSN=BANCO.MOVTOS(+1),DISP=(NEW,CATLG)
(+1) crea la generación siguiente; (0) es la actual y (-1) la anterior. El sistema conserva
automáticamente las últimas N versiones. Es control de versiones de datos integrado en el sistema
operativo desde los años sesenta, y es inmutabilidad a nivel de fichero.
Quien haya trabajado con almacenamiento inmutable, con event sourcing o con la arquitectura de un data lake reconocerá el patrón inmediatamente: no modifiques, añade una versión nueva. Los mainframes llevan sesenta años haciéndolo, y por la misma razón — poder repetir el proceso.
Y dentro del programa, COBOL sí tiene funciones intrínsecas puras desde 1989: FUNCTION NUMVAL,
FUNCTION MAX, FUNCTION REVERSE, FUNCTION TRIM. Se usan en expresiones y no tienen efectos. Es
poco, y es lo que hay.
Ficha completa · HPC, clima, física, BLAS/LAPACK · gfortran -O2 prog.f90
program func1
implicit none
integer, allocatable :: v(:), doblados(:)
integer :: n, ios, i
character(len=400) :: linea, salida
character(len=20) :: buf
read(*, '(A)') linea
n = 0
do i = 1, 100
if (allocated(v)) deallocate(v)
allocate(v(i))
read(linea, *, iostat=ios) v
if (ios /= 0) exit
n = i
end do
if (allocated(v)) deallocate(v)
allocate(v(n))
read(linea, *) v
doblados = v * 2 ! sin bucle: `v` NO se modifica
salida = ''
do i = 1, n
write(buf, '(I0)') doblados(i)
if (i == 1) then
salida = trim(buf)
else
salida = trim(salida) // '-' // trim(buf)
end if
end do
write(*, '(A)') 'doblados=' // trim(salida)
end program func1
Lo que esta clase enseña en Fortran. doblados = v * 2 es la línea de esta clase, y merece
insistir en lo que ya se apuntó en la clase 107: es programación declarativa, y llegó a Fortran por
el rendimiento, no por la elegancia.
Cuando el compilador ve doblados = v * 2, sabe que cada elemento es independiente del resto. Con
eso puede:
do concurrent.Un bucle escrito a mano con un acumulador no le da esa información, porque el orden de las operaciones queda fijado por el programa.
Y de ahí sale la familia entera de construcciones "funcionales" de Fortran, todas por el mismo motivo:
where (v < 0) v = 0 ! asignación condicional sobre todo el arreglo
w = merge(a, b, mascara) ! elegir elemento a elemento
w = pack(v, v > 0) ! filtrar
s = sum(v) / m = maxval(v) ! reducciones
forall (i = 1:n) w(i) = v(i) * 2 ! F95: sin orden garantizado
do concurrent (i = 1:n) ! F2008: "estas vueltas son independientes"
w(i) = v(i) * 2
end do
do concurrent es la culminación: el programador PROMETE que las iteraciones son
independientes, y el compilador puede paralelizarlas o enviarlas a una GPU. Con nvfortran o
gfortran -fopenmp, ese bucle se ejecuta en una tarjeta gráfica sin escribir CUDA.
Y pure (clase 084) es el sello de todo esto: la única declaración de pureza comprobada por un
compilador en toda esta página. Una función pure no puede modificar sus argumentos, ni variables
globales, ni hacer entrada/salida, y solo puede llamar a otras funciones puras.
pure function doblar(x) result(r)
Es exactamente la definición de función pura del paradigma funcional, y Fortran la exige para poder
llamarla dentro de un forall o un do concurrent.
La ironía es buena: el lenguaje más antiguo de la página tiene la garantía funcional más fuerte, y la tiene por razones de máquina.
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 Func1 is
type Vector is array (Positive range <>) of Integer;
-- Función PURA: recibe un vector y devuelve otro. No modifica nada.
function Doblar (V : Vector) return Vector is
R : Vector (V'Range);
begin
for I in V'Range loop
R (I) := V (I) * 2;
end loop;
return R;
end Doblar;
Datos : Vector (1 .. 100);
N : Natural := 0;
Linea : String (1 .. 400);
Ultimo : Natural;
Pos : Integer := 1;
Valor : Integer;
Fin : Positive;
begin
Get_Line (Linea, Ultimo);
loop
while Pos <= Ultimo and then Linea (Pos) = ' ' loop
Pos := Pos + 1;
end loop;
exit when Pos > Ultimo;
Get (Linea (Pos .. Ultimo), Valor, Fin);
N := N + 1;
Datos (N) := Valor;
Pos := Fin + 1;
end loop;
Put ("doblados=");
declare
R : constant Vector := Doblar (Datos (1 .. N));
begin
for I in R'Range loop
Put (R (I), Width => 1);
if I < R'Last then
Put ("-");
end if;
end loop;
end;
New_Line;
end Func1;
Lo que esta clase enseña en Ada. function Doblar (V : Vector) return Vector devuelve un arreglo
completo, y eso en Ada funciona desde 1983 sin reservar memoria a mano: el tamaño del resultado se
deduce de V'Range, y el compilador se encarga.
Es una función pura en el sentido estricto —no toca nada de fuera— y hasta Ada 2012 el lenguaje lo
imponía por gramática: una función no podía tener parámetros in out (clase 109).
Ada 2012 relajó eso y a cambio dio la herramienta que va más lejos que cualquier otra de esta página:
function Doblar (V : Vector) return Vector
with Global => null, -- NO toca NINGÚN estado global
Post => (for all I in V'Range => Doblar'Result (I) = V (I) * 2);
Global => null declara que la función no lee ni escribe ninguna variable de fuera. Y
Post con un cuantificador universal especifica el resultado completo, no un caso.
Y aquí está lo importante: con SPARK, eso no se comprueba en ejecución — se DEMUESTRA.
gnatprove --level=2 func1.adb
La herramienta genera obligaciones de prueba y las envía a demostradores automáticos (Z3, CVC5, Alt- Ergo). Si pasan, está matemáticamente probado que la función cumple su contrato para toda entrada posible, y que no hay desbordamientos, ni divisiones por cero, ni accesos fuera de rango.
Eso es lo que el paradigma funcional persigue con la pureza —poder razonar sobre el código como sobre matemáticas— llevado hasta el final: no razonar a mano, sino que lo demuestre una máquina.
Y no es un ejercicio académico. SPARK se usa en el sistema de control de tráfico aéreo británico (iFACTS), en el metro sin conductor de París, en aviónica de Airbus y en implementaciones criptográficas.
Ada 2012 añadió además for all y for some como expresiones normales del lenguaje, no solo en
contratos:
if (for all I in V'Range => V (I) > 0) then ...
Cuantificadores lógicos en un if. Es la sintaxis de las matemáticas dentro de un lenguaje
imperativo.
Ficha completa · Escritorio empresarial, TPV, industria (vía Delphi y Free Pascal) · fpc -Mobjfpc prog.pas
program Func1;
{$MODE OBJFPC}{$H+}
uses SysUtils;
type
TVector = array of Integer;
{ Función pura: devuelve un vector NUEVO }
function Doblar(const V: TVector): TVector;
var
I: Integer;
begin
SetLength(Result, Length(V));
for I := 0 to High(V) do
Result[I] := V[I] * 2;
end;
var
V, D: TVector;
Linea, Tok, Salida: string;
I: Integer;
C: Char;
begin
ReadLn(Linea);
SetLength(V, 0);
Tok := '';
for I := 1 to Length(Linea) + 1 do
begin
if I <= Length(Linea) then C := Linea[I] else C := ' ';
if C = ' ' then
begin
if Tok <> '' then
begin
SetLength(V, Length(V) + 1);
V[High(V)] := StrToInt(Tok);
Tok := '';
end;
end
else
Tok := Tok + C;
end;
D := Doblar(V);
Salida := '';
for I := 0 to High(D) do
begin
if Salida <> '' then Salida := Salida + '-';
Salida := Salida + IntToStr(D[I]);
end;
WriteLn('doblados=', Salida);
end.
Lo que esta clase enseña en Pascal. Pascal no es funcional y tiene una pieza de esta clase que
conviene mirar de cerca: el const del parámetro.
function Doblar(const V: TVector): TVector;
const hace dos cosas a la vez (clase 109): prohíbe modificar el parámetro —que es la mitad de la
definición de función pura— y evita la copia. Es la única forma en Pascal de expresar "recibo esto
para leerlo".
Y Object Pascal moderno tiene lo que hace falta para el estilo funcional, llegado tarde:
type
TFuncion = reference to function(X: Integer): Integer; { Delphi 2009: CLAUSURA }
function Mapear(const V: TVector; F: TFuncion): TVector;
begin
SetLength(Result, Length(V));
for var I := 0 to High(V) do { Delphi 10.3: variable EN LÍNEA }
Result[I] := F(V[I]);
end;
D := Mapear(V, function(X: Integer): Integer
begin Result := X * 2 end);
reference to function es la clausura: captura variables del ámbito y lleva conteo de referencias
para gestionarse sola (clase 083). Con ella, map, filter y reduce se escriben sin problema.
Free Pascal y Delphi traen además TArray.Sort<T> y TEnumerable con Select y Where al estilo
LINQ en Spring4D, la biblioteca comunitaria de referencia.
Sobre inmutabilidad, Pascal ofrece poco: no hay estructuras persistentes ni tipos inmutables. Lo
que hay es la disciplina de const y la copia al escribir de las cadenas (clase 102).
Y merece señalarse una asimetría llamativa que ya apareció en la clase 102: las cadenas largas de
Pascal tienen copia al escribir y los arreglos dinámicos no. Una cadena se comporta como un valor
inmutable; un arreglo, no. Esa incoherencia es lo que hace que el estilo funcional en Pascal exija más
cuidado del que parece — Result := V en una función comparte, no copia.
Ficha completa · IA simbólica, CAD, investigación · sbcl --script prog.lisp
(let ((v '()))
(loop for x = (read *standard-input* nil nil)
while x
do (push x v))
(setf v (nreverse v))
;; mapcar: la operación fundacional. `v` no se toca.
(format t "doblados=~{~D~^-~}~%" (mapcar (lambda (x) (* 2 x)) v)))
Lo que esta clase enseña en Common Lisp. maplist aparece en el manual del LISP 1.5 de 1962, y
maplist está en el memorando de McCarthy de 1960. La palabra map que hoy usan JavaScript,
Python, Rust y todos los demás viene de ahí.
Y Lisp tiene una familia entera, con distinciones que la mayoría de los lenguajes no hace:
(mapcar #'f lista) ; aplica a cada ELEMENTO, devuelve lista
(mapcar #'+ l1 l2) ; VARIAS listas a la vez, elemento a elemento
(maplist #'f lista) ; aplica a cada SUBLISTA (la cola completa)
(mapcan #'f lista) ; como mapcar, y CONCATENA los resultados
(mapc #'f lista) ; solo por el EFECTO, devuelve la lista original
(map 'vector #'f secuencia) ; sobre cualquier secuencia, con el TIPO del resultado
(reduce #'+ lista) ; el fold
(remove-if-not #'evenp l) ; el filter
(map 'vector ...) merece atención: el primer argumento dice de qué tipo será el resultado, así
que la misma función sirve para producir una lista, un vector o una cadena. Es una generalidad que en
otros lenguajes exige convertir después.
Y mapc frente a mapcar es la distinción que esta clase quiere marcar: uno es para transformar y
el otro para provocar efectos. Tenerlos separados y con nombres distintos es una forma de decir en el
código cuál es la intención.
Ahora, la honestidad histórica que esta clase exige: Lisp nunca fue funcional puro. Como se dijo
en la clase 108, setq, rplaca y nconc están desde el principio, y el estilo dominante en Common
Lisp es mixto.
Lo que Lisp aportó no fue la pureza, sino las funciones como valores y la recursión como herramienta central, que son los cimientos sobre los que ML, Haskell, Scheme y todos los demás construyeron la pureza.
Y para inmutabilidad de verdad, la respuesta moderna del ecosistema son las estructuras persistentes
con compartición estructural (clase 097): FSet en Common Lisp, y sobre todo Clojure, un Lisp
que hizo de la inmutabilidad su decisión central y demostró que se puede pagar el coste.
Ficha completa · Diseño de chips (EDA), redes, testing · tclsh prog.tcl
gets stdin linea
set v [split [string trim $linea]]
# lmap: el `map` de Tcl (8.6). `v` no se modifica.
set doblados [lmap x $v { expr {$x * 2} }]
puts "doblados=[join $doblados -]"
Lo que esta clase enseña en Tcl. lmap llegó en Tcl 8.6 (2012), veinticuatro años después del
lenguaje, y su historia dice algo sobre cómo evolucionan los lenguajes de guion.
Antes, el idioma era construir la lista a mano:
set doblados {}
foreach x $v { lappend doblados [expr {$x * 2}] }
Y lmap es literalmente eso con otra sintaxis: acepta las mismas formas que foreach, incluidas
las dos que se mencionaron en la clase 108:
lmap x $v { ... } ;# uno a uno
lmap {a b} $v { ... } ;# de DOS en dos
lmap a $l1 b $l2 { ... } ;# dos listas EN PARALELO
Esa última hace en Tcl lo que (mapcar #'+ l1 l2) en Lisp y zip en Python, y es más general que el
map de la mayoría de los lenguajes.
Y aquí está lo importante de esta clase en Tcl: la inmutabilidad no es una opción, es el modelo
(clase 102). Todos los valores son inmutables desde el punto de vista del programa, y
set doblados [lmap ...] produce una lista nueva sin que v pueda verse afectada.
Por debajo hay copia al escribir con conteo de referencias, así que la semántica funcional se obtiene con el coste de la imperativa mientras haya una sola referencia.
Esa última condición es la trampa de rendimiento que ya se explicó y que aquí conviene repetir con su consecuencia funcional:
set b $a ;# ahora hay DOS referencias
lset b 0 99 ;# aquí se duplica la lista entera
El estilo funcional en Tcl es correcto y puede ser caro, y el idioma para lo caro es upvar con
el nombre de la variable, que es imperativo.
Tcl 8.6 añadió además apply para lambdas anónimas (clase 083) y tcl::mathop para pasar operadores
como valores (clase 085), con lo que el estilo funcional es escribible:
lmap x $v {*}[list apply {{x} {expr {$x * 2}}}]
Es viable y nadie lo escribe así: el bloque literal de lmap es más claro. Es un buen recordatorio de
que tener las piezas no hace idiomático el estilo.
Ficha completa · Sysadmin, texto, bioinformática · perl prog.pl
use strict;
use warnings;
my $linea = <STDIN>;
chomp $linea;
my @v = split ' ', $linea;
my @doblados = map { $_ * 2 } @v; # @v no se toca
print "doblados=", join('-', @doblados), "\n";
Lo que esta clase enseña en Perl. map, grep y sort con bloque están en Perl desde 1987,
cuando ningún otro lenguaje mayoritario de uso general los tenía en la sintaxis.
map { $_ * 2 } @v # transformar
grep { $_ > 0 } @v # filtrar
sort { $a <=> $b } @v # ordenar con comparador
reverse @v
List::Util: sum, max, min, first, reduce, any, all, none, pairs
Y map en Perl es más general que en casi todos: el bloque puede devolver cualquier número de
elementos, no solo uno.
map { ($_, $_ * 2) } @v # DUPLICA la longitud: 1,2 2,4 3,6
map { $_ % 2 ? $_ : () } @v # devolver () DESCARTA: map hace de filter
Ese comportamiento —map es en realidad un flatMap— es lo que en Haskell es concatMap y en
JavaScript flatMap, y en Perl es el comportamiento por defecto desde el principio.
Ahora la advertencia importante de esta clase, y es específica de Perl: $_ dentro de map y
grep es un ALIAS, no una copia.
my @v = (1, 2, 3);
my @m = map { $_ *= 2; $_ } @v; # ¡MODIFICA @v!
print "@v"; # 2 4 6
Modificar $_ dentro del bloque cambia la lista original, exactamente como @_ en las subrutinas
(clase 079). Es la trampa clásica del estilo funcional en Perl, y la razón de que la forma correcta
sea map { $_ * 2 } —sin asignación— y no map { $_ *= 2 }.
Es un caso limpio de lo que dice el cierre de esta clase: el estilo funcional necesita que la inmutabilidad esté garantizada por algo, y en Perl no lo está.
Para inmutabilidad real, CPAN ofrece Readonly, const (Perl 5.28 tiene use constant y atributos
:const) e Hash::Util::lock_hash, que congela una estructura y hace que modificarla sea un error.
Ficha completa · Videojuegos, navegadores, finanzas, HPC · g++ -std=c++17 prog.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
int main() {
const std::vector<int> v{std::istream_iterator<int>(std::cin),
std::istream_iterator<int>()};
std::vector<int> doblados;
doblados.reserve(v.size());
std::transform(v.begin(), v.end(), std::back_inserter(doblados),
[](int x) { return x * 2; });
std::cout << "doblados=";
for (std::size_t i = 0; i < doblados.size(); ++i) {
if (i != 0) std::cout << '-';
std::cout << doblados[i];
}
std::cout << '\n';
return 0;
}
Lo que esta clase enseña en C++. std::transform es el map de C++, y su nombre distinto no
es casual: la STL nombró sus algoritmos con vocabulario matemático y de la tradición de C, no de Lisp.
La declaración const std::vector<int> v es la parte importante del programa: const es la única
garantía de inmutabilidad que ofrece C++, y es una garantía fuerte —modificar v no compila— con
una grieta conocida:
const int x = 5;
const_cast<int&>(x) = 6; // compila; comportamiento INDEFINIDO si x era const de verdad
mutable int cache; // un campo modificable dentro de un método const
const_cast existe para interoperar con APIs de C mal declaradas, y usarlo para modificar algo
realmente constante es comportamiento indefinido.
Y C++ tiene una segunda forma de "puro" que ya apareció en la clase 107 y que es más fuerte que la de
casi todos: constexpr.
constexpr int doblar(int x) { return x * 2; }
static_assert(doblar(21) == 42); // se evalúa AL COMPILAR
constexpr std::array<int, 3> v{1, 2, 3};
Una función constexpr puede evaluarse en tiempo de compilación, lo que exige que sea pura en el
sentido práctico: sin entrada/salida, sin estado global mutable, sin memoria dinámica salvo la que se
libere dentro (C++20). Y consteval (C++20) obliga a que se evalúe al compilar.
No es una declaración de pureza como el pure de Fortran —una constexpr sí puede modificar sus
locales— y para el propósito de esta clase da la misma propiedad: el resultado depende solo de los
argumentos.
C++20 añadió además las ranges, que dan el estilo funcional componible que faltaba:
auto doblados = v | std::views::transform([](int x) { return x * 2; });
Y es perezoso: doblados no calcula nada hasta que se recorre, y no reserva memoria. Es
evaluación diferida en C++, que es lo que Haskell hace por defecto — con la diferencia de que aquí se
pide explícitamente y su coste es visible.
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 FUNC1;
entrada char(200) const;
end-pi;
dcl-s texto varchar(200);
dcl-s tok varchar(20) inz('');
dcl-s c char(1);
dcl-s i int(10);
dcl-s origen int(10) dim(100);
dcl-s doblados int(20) dim(100);
dcl-s n int(10) inz(0);
dcl-s salida varchar(200) inz('');
texto = %trimr(entrada);
for i = 1 to %len(texto) + 1;
if i <= %len(texto);
c = %subst(texto : i : 1);
else;
c = ' ';
endif;
if c = ' ';
if tok <> '';
n += 1;
origen(n) = %int(tok);
tok = '';
endif;
else;
tok += c;
endif;
endfor;
// origen no se modifica: el resultado va a otra tabla
for i = 1 to n;
doblados(i) = origen(i) * 2;
endfor;
for i = 1 to n;
if salida <> '';
salida += '-';
endif;
salida += %char(doblados(i));
endfor;
dsply ('doblados=' + salida);
*inlr = *on;
return;
Lo que esta clase enseña en RPG. RPG no tiene map, ni funciones de primera clase idiomáticas, ni
inmutabilidad, y su respuesta es la misma que la de COBOL: una tabla de origen y otra de destino.
Lo que sí tiene, y es más de lo que parece, son operaciones sobre tablas completas (clase 089):
suma = %sum(%subarr(v : 1 : n)); // reducción
sorta v; // ordenar en el sitio
pos = %lookup(clave : v); // buscar
%subarr(destino : 1 : n) = %subarr(origen : 1 : n); // copiar un tramo
%sum es un reduce integrado, y %subarr permite trabajar con rebanadas — las secciones de arreglo
de Fortran, con otra sintaxis.
Y donde RPG sí es funcional sin llamarlo así es en el sitio donde la plataforma pone la potencia: SQL.
exec sql
select sum(importe), avg(importe), max(importe)
into :total, :media, :maximo
from movimientos
where fecha between :desde and :hasta
group by cliente;
Eso es map, filter, reduce y group by en una sentencia declarativa, sin bucles y sin mutación,
ejecutada por un motor que decide el plan. Es el paradigma funcional-declarativo en el sitio donde
están los datos, que es la clase 117.
Y sobre inmutabilidad, la plataforma aporta algo que el lenguaje no: el registro de diario (journaling).
STRJRNPF FILE(BIBLIO/CLIENTES) JRN(BIBLIO/QSQJRN) IMAGES(*BOTH)
Con IMAGES(*BOTH), el sistema guarda la imagen anterior y la posterior de cada cambio, en un
diario inmutable y ordenado. Con eso se puede reconstruir el estado en cualquier instante, deshacer
cambios y replicar a otro sistema.
Es registro de eventos inmutable, integrado en el sistema operativo desde 1988, y es exactamente la arquitectura que hoy se llama event sourcing — con treinta años de ventaja y sin nombre de moda.
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
func1: procedure options(main);
declare linea char(200) varying;
declare tok char(20) varying initial('');
declare c char(1);
declare origen(100) fixed binary(31);
declare doblados(100) fixed binary(31);
declare (i, n) fixed binary(31);
declare salida char(200) varying initial('');
get edit (linea) (a(200));
linea = trim(linea);
n = 0;
do i = 1 to length(linea) + 1;
if i <= length(linea) then c = substr(linea, i, 1); else c = ' ';
if c = ' ' then do;
if tok ^= '' then do;
n = n + 1;
origen(n) = tok;
tok = '';
end;
end;
else tok = tok || c;
end;
/* aritmetica de arreglos: sin bucle, y `origen` no se toca */
doblados = origen * 2;
do i = 1 to n;
if salida ^= '' then salida = salida || '-';
salida = salida || trim(char(doblados(i)));
end;
put skip list ('doblados=' || salida);
end func1;
Lo que esta clase enseña en PL/I. doblados = origen * 2; es aritmética de arreglos, y PL/I la
tenía en 1964 — veintiséis años antes que Fortran 90, como se dijo en la clase 089.
v = 0; /* a todos los elementos */
w = v * 2 + u; /* elemento a elemento */
s = sum(v); /* reducción */
put list (v); /* imprimir el arreglo entero */
cliente.saldo = 0; /* un CAMPO de cien registros a la vez */
Esa última línea, ya mencionada en la clase 095, es lo que hoy se llama procesamiento columnar, y no tiene equivalente en ningún lenguaje del núcleo.
Y aquí conviene hacer una precisión importante para no exagerar: PL/I tenía la notación, no la
garantía. La aritmética de arreglos de PL/I es azúcar sobre bucles generados por el compilador, y el
lenguaje no tiene ninguna declaración de pureza — nada equivalente al pure de Fortran 95.
Sin esa garantía, el compilador no puede saber si una función que aparece en una expresión de arreglo tiene efectos, así que no puede reordenar ni paralelizar con seguridad. La notación es cómoda; la optimización que Fortran obtiene de la suya, PL/I no la obtiene.
Es la diferencia entre parecer declarativo y serlo, y esta clase es buen sitio para señalarla, porque explica por qué Fortran sigue dominando el cálculo numérico y PL/I nunca compitió ahí pese a tener aritmética de arreglos antes.
Sobre funciones de primera clase, PL/I sí tiene las variables entry (clase 085), así que un
map genérico es escribible:
mapear: procedure (v, n, f);
declare f entry (fixed binary(31)) returns (fixed binary(31)) variable;
do i = 1 to n;
v(i) = f(v(i));
end;
end mapear;
Funciona, y no es idiomático: en 1964 nadie escribía así, y el código PL/I que hay en producción es imperativo de principio a fin.
Ficha completa · Sanidad: historia clínica, VistA, Epic · YottaDB
FUNC1 ; Funcional I -- clase 114
read linea
kill doblados
set n = $length(linea, " ")
; el array `doblados` es nuevo: `linea` no se modifica
for i=1:1:n set doblados(i) = $piece(linea, " ", i) * 2
set salida = ""
for i=1:1:n do
. if salida '= "" set salida = salida _ "-"
. set salida = salida _ doblados(i)
write "doblados=", salida, !
quit
Lo que esta clase enseña en M. M no tiene nada funcional: no hay map, no hay funciones de
primera clase, no hay clausuras y no hay inmutabilidad. El programa recorre y construye otro array.
Lo que sí tiene, y es lo que esta clase puede aprovechar, es una propiedad que se apuntó en la clase 101 y que aquí encaja del todo: en M no existen las estructuras compartidas.
set a = b ; copia el valor
merge a = b ; copia el árbol ENTERO (clase 102)
No hay ninguna forma de que dos nombres designen la misma estructura. Nunca hay que preguntarse si una modificación se propagará a otro sitio, que es exactamente la pregunta que el cierre de esta clase dice que la inmutabilidad elimina.
M la elimina por el otro camino: no compartiendo nunca. Es más caro —cada copia es una copia real— y da la misma tranquilidad.
Y en la capa de datos, M tiene la pieza que hace posible razonar sobre estado compartido: la transacción.
tstart
set ^SALDO(a) = ^SALDO(a) - importe
set ^SALDO(b) = ^SALDO(b) + importe
tcommit
Dentro de tstart/tcommit, los cambios son invisibles para los demás procesos hasta confirmar, y
si algo falla se deshacen. Es aislamiento, y resuelve el mismo problema que la inmutabilidad —que
nadie vea un estado a medias— con la herramienta de las bases de datos en lugar de la de los
lenguajes.
Merece cerrar con esa observación, porque recorre toda esta parte del curso: los lenguajes funcionales y las bases de datos transaccionales atacan el mismo problema desde lados opuestos. Uno dice "no cambies nada"; el otro, "cambia lo que quieras, pero que nadie lo vea hasta que esté".
Clojure, que es un Lisp obsesionado con la inmutabilidad, tiene memoria transaccional por software
con dosync — y su autor cita explícitamente las bases de datos como inspiración. Los dos mundos
llevan sesenta años convergiendo.
Ficha completa · Banca, seguros, trading · Pharo
| v doblados |
v := stdin nextLine substrings collect: [ :cada | cada asNumber ].
"collect: es el map. `v` no se modifica: devuelve una colección NUEVA"
doblados := v collect: [ :cada | cada * 2 ].
Transcript
show: 'doblados=', ((doblados collect: [ :cada | cada printString ])
inject: '' into: [ :acc :cada |
acc isEmpty ifTrue: [ cada ] ifFalse: [ acc, '-', cada ] ]);
cr.
Lo que esta clase enseña en Smalltalk. collect: es el map de Smalltalk, y su nombre es
anterior a que "map" se generalizara fuera de Lisp. El vocabulario completo de la jerarquía de
colecciones (clase 089) es este:
coleccion collect: [ :x | ... ] "map"
coleccion select: [ :x | ... ] "filter"
coleccion reject: [ :x | ... ] "filter negado"
coleccion detect: [ :x | ... ] ifNone: [ ] "find"
coleccion inject: 0 into: [ :a :b | ] "reduce / fold"
coleccion do: [ :x | ... ] separatedBy: [ ]
coleccion anySatisfy: / allSatisfy:
Y la regla species de la clase 090 hace que collect: sobre un Set devuelva un Set y sobre una
String devuelva una String. Es más cuidadoso que el map de la mayoría, que siempre devuelve una
lista.
Ahora bien, hay que decir la verdad sobre la inmutabilidad: Smalltalk es profundamente mutable.
Los objetos tienen estado, at:put: modifica en el sitio, las cadenas son mutables (clase 093) y no
hay tipos inmutables en el núcleo.
Lo que sí hay, y es de las cosas más peculiares del sistema, es la inmutabilidad como propiedad de un objeto individual, activable en marcha:
objeto beReadOnlyObject.
objeto isReadOnly.
objeto beWritableObject.
Cualquier objeto se puede congelar, y a partir de ahí un intento de modificarlo dispara
ModificationForbidden, que es una excepción normal y por tanto manejable.
Con eso se construyen cosas que en otros lenguajes exigen soporte del compilador: detectar quién modifica un objeto —congelándolo y capturando la excepción para ver la pila— o implementar transacciones a nivel de objeto.
Y esa propiedad viene de lo mismo que todo en este lenguaje: la inmutabilidad es una bandera en la cabecera del objeto, comprobada por la máquina virtual, y como todo en Smalltalk, se puede consultar y cambiar desde el propio sistema.
Lo transferible: la inmutabilidad no se pide por pureza moral, se pide porque elimina una pregunta. Con datos mutables, ante cualquier valor hay que saber quién más lo tiene y cuándo lo tocó; sin mutación, esa pregunta desaparece y con ella los errores de concurrencia, los alias sorprendentes (clase 102) y media clase de fallos difíciles de reproducir. El coste es copiar, y ahí está la salida que descubrió Lisp y que hoy usan Clojure y Rust: compartir lo que no cambia (clase 097).
⏮️ Volver a la clase 114 · 🧬 Los primos del Atlas · 🧟 Índice de lenguajes vivos