⬅️ Volver a la clase 080 · 🧬 Primos del Atlas · 🧟 Índice de lenguajes vivos · 📚 Índice
Ahora al revés: una función que modifica la variable del llamante. Es lo contrario de la clase anterior y, en la mitad de estos lenguajes, es lo que pasa si no haces nada. La pregunta interesante no es cómo se consigue, sino dónde se declara: ¿en la firma, donde lo ve quien implementa, o en la llamada, donde lo ve quien usa?
🎯 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 mutación del argumento, y estos lenguajes se reparten en dos escuelas. En la firma: Fortran con
intent(inout), Ada conin out, Pascal convar, C++ con&. En la llamada: COBOL conBY REFERENCEy M con el punto delante del argumento.La segunda escuela es hoy minoritaria y tiene una consecuencia grave: leyendo el subprograma no se sabe si sus parámetros son seguros, porque depende de cómo lo invoquen. Y leyendo la llamada tampoco se sabe si el subprograma va a modificar algo. La información está partida en dos sitios.
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.
n → stdout: antes=<n> despues=<2n>la función duplica la variable original vía referencia| stdin | esperado |
|---|---|
5 |
antes=5 despues=10 |
3 |
antes=3 despues=6 |
7 |
antes=7 despues=14 |
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. PORREF.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 LINEA PIC X(80).
01 N PIC S9(9) COMP-3.
01 ANTES PIC S9(9) COMP-3.
01 ED-A PIC -(8)9.
01 ED-D PIC -(8)9.
PROCEDURE DIVISION.
ACCEPT LINEA
MOVE FUNCTION NUMVAL(LINEA) TO N
MOVE N TO ANTES
PERFORM DOBLAR *> modifica N: los párrafos comparten TODO
MOVE ANTES TO ED-A
MOVE N TO ED-D
DISPLAY "antes=" FUNCTION TRIM(ED-A)
" despues=" FUNCTION TRIM(ED-D)
STOP RUN.
DOBLAR.
COMPUTE N = N * 2.
Lo que esta clase enseña en COBOL. Un párrafo siempre trabaja sobre las variables globales, así que en cierto sentido todo COBOL es paso por referencia llevado al extremo: no hay parámetros que pasar porque no hay nada privado.
Para los subprogramas, COBOL declara el mecanismo en la llamada:
CALL "SUB" USING BY REFERENCE A *> el DEFECTO: SUB puede modificar A
Y el subprograma recibe las direcciones en su LINKAGE SECTION, que es una sección de datos
especial: declara la forma de algo que vive en otro programa.
DATA DIVISION.
LINKAGE SECTION.
01 PARAM-A PIC S9(9) COMP-3.
PROCEDURE DIVISION USING PARAM-A.
COMPUTE PARAM-A = PARAM-A * 2. *> escribe en la memoria del LLAMANTE
La LINKAGE SECTION no reserva memoria: describe la forma de un dato ajeno. Si el llamante pasa
un campo con otra PIC, el subprograma lo interpreta con la suya y el resultado es basura, sin
ningún error. No hay comprobación de tipos entre programas compilados por separado.
Ese es uno de los fallos más difíciles de diagnosticar del mundo COBOL, y la razón de que los copybooks de la clase 052 sean tan importantes: compartir la definición es la única garantía de que los dos lados coincidan.
Ficha completa · HPC, clima, física, BLAS/LAPACK · gfortran -O2 prog.f90
program porref
implicit none
integer :: n, antes
read(*, *) n
antes = n
call doblar(n) ! por referencia: modifica n
write(*, '(A,I0,A,I0)') 'antes=', antes, ' despues=', n
contains
pure subroutine doblar(x)
integer, intent(inout) :: x ! ENTRADA Y SALIDA, declarado
x = x * 2
end subroutine doblar
end program porref
Lo que esta clase enseña en Fortran. El paso por referencia es el defecto, así que intent
no cambia el mecanismo: cambia lo que está permitido hacer.
intent |
Se puede leer | Se puede escribir | El compilador… |
|---|---|---|---|
in |
Sí | No | Rechaza la escritura |
out |
No (el valor entrante no cuenta) | Sí | Avisa si se lee antes de asignar |
inout |
Sí | Sí |
Es documentación comprobada, y es lo único que hace legible una interfaz en un lenguaje donde todo se pasa por dirección.
Y hay una regla de Fortran que no tiene ningún lenguaje del núcleo y que es la razón de su rendimiento: está prohibido que dos argumentos de una subrutina se solapen en memoria.
call procesar(v, v) ! ILEGAL si procesar modifica alguno
call procesar(v(1:50), v(40:90)) ! ILEGAL: se solapan
El estándar lo prohíbe y el compilador puede asumir que no ocurre, lo que le permite reordenar y
vectorizar libremente. En C, dos punteros pueden apuntar a lo mismo y el compilador debe suponer lo
peor — de ahí que C99 tuviera que inventar restrict para recuperar a mano lo que Fortran tiene de
serie.
Es exactamente lo que la ficha de la clase 043 anunciaba, aquí en su forma concreta: la ausencia de solapamiento es una obligación del programador y una licencia para el optimizador.
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 Porref is
procedure Doblar (X : in out Integer) is
begin
X := X * 2;
end Doblar;
N, Antes : Integer;
begin
Get (N);
Antes := N;
Doblar (N);
Put ("antes="); Put (Antes, Width => 1);
Put (" despues="); Put (N, Width => 1);
New_Line;
end Porref;
Lo que esta clase enseña en Ada. in out declara la intención, y el compilador sigue eligiendo el
mecanismo — copia de entrada y salida, o dirección — como se vio en la clase 079.
Eso tiene una consecuencia que conviene conocer y que Ada documenta explícitamente: si el mismo objeto se pasa dos veces, el resultado no está definido.
Intercambiar (A, A); -- el estándar NO dice qué pasa
Con paso por dirección, las dos referencias son la misma. Con copia de entrada y salida, la segunda copia de vuelta pisa a la primera. Como el estándar no fija el mecanismo, no puede fijar el resultado. Es el mismo problema del solapamiento de Fortran, expresado a nivel de parámetros.
Y out en Ada no es lo mismo que en Pascal o C#: en Ada, un parámetro out de un tipo escalar
entra sin valor definido, y leerlo antes de asignarlo es un error que el compilador detecta. Para
tipos compuestos con partes discriminantes, sí conserva parte de la información — un matiz que está en
el manual de referencia y que casi nadie conoce.
Ada 2012 permite además in out en funciones, cosa prohibida hasta entonces. Fue una concesión
discutida: la restricción original empujaba hacia funciones puras, y levantarla facilitó la
interoperabilidad a cambio de perder esa garantía.
Ficha completa · Escritorio empresarial, TPV, industria (vía Delphi y Free Pascal) · fpc -Mobjfpc prog.pas
program PorRef;
{$MODE OBJFPC}{$H+}
uses SysUtils;
procedure Doblar(var X: Integer); { VAR: por referencia }
begin
X := X * 2;
end;
var
N, Antes: Integer;
begin
Read(N);
Antes := N;
Doblar(N);
WriteLn('antes=', IntToStr(Antes), ' despues=', IntToStr(N));
end.
Lo que esta clase enseña en Pascal. var es la palabra que inventó el paso por referencia
declarado en la firma, y de ahí la copiaron C# (ref), Object Pascal, Modula-2 y Ada (con otro
nombre).
Antes de Pascal, la elección estaba en la llamada —COBOL— o no había elección —FORTRAN—. ALGOL 60 tenía el paso por nombre, que era aún más raro: el argumento se reevaluaba en cada uso, lo que producía el célebre dispositivo de Jensen y una cantidad notable de confusión.
Wirth simplificó a dos modos, y ese par —por valor o var— es el que ha sobrevivido.
Object Pascal añadió después const y out, con lo que quedan cuatro:
procedure P(A: Integer); { valor }
procedure P(var A: Integer); { referencia, lectura y escritura }
procedure P(const A: TGrande); { referencia, solo lectura }
procedure P(out A: Integer); { referencia, solo escritura }
Y hay una restricción de var que conviene conocer: el argumento debe ser una variable, no una
expresión, y su tipo debe coincidir exactamente. Doblar(N + 0) no compila, y Doblar(B) con
B: Byte tampoco, aunque Byte quepa en Integer.
Esa rigidez es deliberada: si el compilador aceptara una conversión, tendría que crear un temporal, y entonces la modificación se perdería — el dummy argument de PL/I y Fortran. Pascal lo prohíbe en lugar de permitirlo en silencio.
Ficha completa · IA simbólica, CAD, investigación · sbcl --script prog.lisp
(defun doblar (celda)
(setf (car celda) (* 2 (car celda)))) ; muta el OBJETO, no la variable
(let* ((n (read))
(caja (list n))) ; una "caja": lista de un elemento
(doblar caja)
(format t "antes=~D despues=~D~%" n (car caja)))
Lo que esta clase enseña en Common Lisp. Lisp no tiene paso por referencia, y este programa muestra el sustituto universal: envolver el valor en un objeto mutable.
(list n) crea una caja de un elemento. La función recibe la caja —por valor, como siempre— y muta su
contenido con (setf (car celda) ...). El llamante ve el cambio porque ambos apuntan al mismo
objeto.
Es exactamente lo que se hace en Java con un array de un elemento, en Python con una lista, en Go con un puntero y en JavaScript con un objeto. Cuando un lenguaje no tiene referencias, la caja es el patrón.
Y setf merece un apunte, porque es una de las mejores ideas de Common Lisp: funciona sobre
cualquier "lugar", no solo sobre variables.
(setf x 1) ; una variable
(setf (car lista) 1) ; el primer elemento
(setf (aref v 3) 1) ; un elemento de array
(setf (gethash k tabla) 1) ; una entrada de tabla hash
(setf (slot-value obj 'campo) 1) ; un campo de objeto
(setf (symbol-function 'f) g) ; ¡la definición de una función!
Todos son "lugares" que se pueden leer y escribir, y setf es la macro que sabe cómo escribir en cada
uno. Se pueden definir lugares nuevos con defsetf y define-setf-expander, así que una estructura
propia puede participar en setf como si fuera nativa.
Es generalización llevada al límite: en vez de un operador de asignación, un protocolo de asignación extensible.
Ficha completa · Diseño de chips (EDA), redes, testing · tclsh prog.tcl
proc doblar {nombreVar} {
upvar 1 $nombreVar x ;# liga x a la variable del LLAMANTE
set x [expr {$x * 2}]
}
gets stdin linea
set n [string trim $linea]
set antes $n
doblar n ;# se pasa el NOMBRE, no el valor
puts "antes=$antes despues=$n"
Lo que esta clase enseña en Tcl. Fíjate en la llamada: doblar n, sin el $. No se pasa el
valor: se pasa el nombre de la variable, como una cadena.
upvar 1 $nombreVar x liga la variable local x a la variable llamada así en el nivel de pila
indicado —1 es el llamante, 2 el llamante del llamante, #0 el nivel global—. A partir de ahí,
x es la variable del otro.
Es paso por referencia obtenido sin punteros y sin referencias: en Tcl las variables se resuelven por nombre en tiempo de ejecución, así que basta con pasar el nombre.
Y ese mecanismo es la base de toda la extensibilidad del lenguaje. Los comandos del propio Tcl que
dejan resultados en variables —scan, regexp, binary scan, lassign, catch— están
implementados así, y por eso un procedimiento de usuario puede imitarlos exactamente.
Su hermano es uplevel, que ya apareció en la clase 041: evalúa código en el ámbito del
llamante. Con upvar y uplevel juntos se pueden escribir estructuras de control propias que se
comportan como las nativas:
proc repetir {n cuerpo} {
for {set i 1} {$i <= $n} {incr i} { uplevel 1 $cuerpo }
}
La contrapartida es la de siempre: el nombre viaja como una cadena, así que nadie comprueba nada. Un error de escritura en el nombre crea una variable nueva en el ámbito del llamante.
Ficha completa · Sysadmin, texto, bioinformática · perl prog.pl
use strict;
use warnings;
sub doblar {
$_[0] *= 2; # @_ contiene ALIAS: esto modifica al llamante
}
my $n = <STDIN>;
chomp $n;
my $antes = $n;
doblar($n);
print "antes=$antes despues=$n\n";
Lo que esta clase enseña en Perl. Esta es la cara oculta de la clase 079: @_ contiene alias de
los argumentos originales, así que modificar $_[0] modifica la variable del llamante.
Perl es, por tanto, por referencia por defecto, y la copia es opt-in con my ($x) = @_;.
Ese comportamiento tiene usos legítimos y muy usados en el propio núcleo:
chomp($linea); # modifica $linea en el sitio
chop($s);
s/a/b/ for @lista; # modifica CADA elemento de @lista
Y for y map tienen el mismo aliasing, como se vio en las clases 064 y 067.
La forma explícita de pasar por referencia, y la que se usa cuando la intención debe verse, son las referencias:
doblar(\$n); # se pasa una REFERENCIA
sub doblar { my ($ref) = @_; $$ref *= 2 }
\$n crea la referencia y $$ref la desreferencia. Es más verboso y se ve en el sitio de la
llamada, que es exactamente la ventaja: quien lee doblar(\$n) sabe que $n puede cambiar, y quien
lee doblar($n) supone que no — aunque técnicamente pueda.
Con firmas (5.36) el aliasing desaparece: los parámetros de una firma son copias, así que la única forma de modificar al llamante pasa a ser la referencia explícita. Es una mejora de legibilidad importante.
Ficha completa · Videojuegos, navegadores, finanzas, HPC · g++ -std=c++17 prog.cpp
#include <iostream>
void doblar(int& x) { // REFERENCIA: modifica la variable del llamante
x *= 2;
}
int main() {
int n{};
if (!(std::cin >> n)) return 1;
const int antes = n;
doblar(n);
std::cout << "antes=" << antes << " despues=" << n << '\n';
return 0;
}
Lo que esta clase enseña en C++. int& es una referencia, y su diferencia con el puntero de C
es la que da la lección:
Puntero int* |
Referencia int& |
|
|---|---|---|
| Puede ser nulo | Sí | No |
| Se puede reasignar | Sí | No: se liga una vez |
| Sintaxis en el uso | *p = 1 |
x = 1 |
| Sintaxis en la llamada | f(&n) |
f(n) |
La última fila es la discutida: en el sitio de la llamada, doblar(n) no revela que n puede
cambiar. En C había que escribir doblar(&n), y esa marca era información. C++ la perdió a cambio
de sintaxis limpia.
Por eso las Core Guidelines recomiendan: si una función modifica un argumento, pásalo por puntero o devuélvelo, precisamente para que se vea en la llamada. Google llegó a prohibir las referencias no constantes en su guía de estilo por este motivo, y años después relajó la norma.
Es el mismo debate de COBOL y M —¿la marca va en la firma o en la llamada?— con la respuesta de que lo ideal sería en las dos.
Y C++11 añadió un tercer tipo de referencia, int&&, que es la clase siguiente: no significa
"modificable", significa "consumible".
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 PORREF;
n int(10); // SIN const ni value: por referencia (el defecto)
end-pi;
dcl-s antes int(10);
dcl-s salida char(50);
antes = n;
doblar(n); // modifica n
salida = 'antes=' + %char(antes) + ' despues=' + %char(n);
dsply salida;
*inlr = *on;
return;
dcl-proc doblar;
dcl-pi *n;
x int(10); // por referencia
end-pi;
x = x * 2;
end-proc;
Lo que esta clase enseña en RPG. El paso por referencia es el defecto, y basta con no
escribir nada: un parámetro sin const ni value es modificable y el cambio se ve fuera.
Ese defecto es el origen de un problema real de mantenimiento en RPG: un subprocedimiento que
modifica un parámetro por error corrompe datos del llamante sin ninguna señal, igual que en COBOL y
Fortran antes de intent.
Por eso la guía de estilo de la plataforma es tajante: const en todo lo que sea de entrada,
siempre, aunque sea más largo de escribir.
RPG tiene además dos cosas relacionadas que conviene conocer. La primera, options(*varsize), que
permite pasar un campo más corto que el declarado:
dcl-pi *n;
texto char(1000) options(*varsize);
largo int(10) const; // y el llamante dice cuánto mide de verdad
end-pi;
Sin *varsize, RPG exige que el argumento mida exactamente lo declarado. Con él, se pasa la dirección
y el tamaño real viaja aparte — el mismo char* + size_t de C, con los mismos riesgos.
La segunda: %addr(), que da la dirección de una variable, y las variables based(puntero) de la
clase 078. Con ellas se puede hacer aritmética de punteros en RPG, y se usa para las APIs del sistema.
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
porref: procedure options(main);
declare (n, antes) fixed binary(31);
get list (n);
antes = n;
call doblar(n); /* por REFERENCIA: es el defecto */
put skip list ('antes=' || trim(char(antes)) ||
' despues=' || trim(char(n)));
doblar: procedure (x);
declare x fixed binary(31);
x = x * 2;
end doblar;
end porref;
Lo que esta clase enseña en PL/I. El paso por referencia es el defecto y no hay forma de declararlo en la firma: cualquier parámetro es modificable, y lo único que fuerza la copia son los paréntesis extra en el sitio de la llamada, como se vio en la clase 079.
Esa ausencia —no poder decir "este parámetro es de entrada"— es una de las carencias más claras de PL/I frente a Fortran 90 y Ada, y se nota en el mantenimiento: para saber qué modifica un procedimiento hay que leer su cuerpo entero.
Lo que PL/I sí tiene, y es donde el paso por referencia se vuelve realmente potente, es la
combinación con las variables based de la clase 053:
declare p pointer;
declare v(1000) fixed binary(31) based(p);
p = addr(datos); /* apunta a otra cosa */
v(5) = 99; /* escribe en datos, con OTRA forma */
based permite reinterpretar cualquier zona de memoria con la forma que quieras, y addr da la
dirección de cualquier variable. Con eso se escribió Multics, y con eso se puede corromper cualquier
cosa.
Es coherente con el resto del lenguaje: PL/I da las herramientas de un lenguaje de sistemas y las comodidades de uno de aplicación, sin separar las dos cosas. Fue una decisión valiente para 1964 y es la razón de que hoy se cite como ejemplo de lo que no hay que hacer.
Ficha completa · Sanidad: historia clínica, VistA, Epic · YottaDB
PORREF ; Paso por referencia -- clase 080
read n
set antes = n
do doblar(.n) ; el PUNTO: por referencia
write "antes=", antes, " despues=", n, !
quit
;
doblar(x) ; dobla x en el sitio
set x = x * 2
quit
Lo que esta clase enseña en M. El punto delante del argumento —.n— es todo el mecanismo. Sin
él, por valor; con él, por referencia. La decisión está en el sitio de la llamada, como en COBOL.
Y lo que hace único a M es qué se puede pasar así: no una variable, sino un árbol local completo.
kill pacientes
do CARGAR^DATOS(.pacientes)
;
CARGAR(res) ;
set res(1,"nombre") = "Ada"
set res(1,"edad") = 36
set res(2,"nombre") = "Grace"
quit
La rutina llamada crea la estructura entera en el array del llamante: subíndices, niveles y todo.
Al volver, pacientes(1,"nombre") existe.
Eso convierte el paso por referencia en el mecanismo de retorno de estructuras del lenguaje, y explica la forma de todas las APIs de VistA: no devuelven objetos, rellenan arrays.
Hay un detalle que conviene conocer y que causa errores: el punto solo se puede poner sobre un
nombre de variable simple, no sobre un subíndice ni una expresión. do P(.a(1)) no es legal. Para
pasar un subárbol hay que copiarlo primero con merge:
merge temporal = pacientes(1) ; MERGE copia un subárbol completo
do P(.temporal)
merge es otro comando propio de M sin equivalente directo: copia un nodo y todos sus
descendientes de una estructura a otra, local o global, en una sola operación.
Ficha completa · Banca, seguros, trading · Pharo
| n caja |
n := stdin nextLine trimBoth asNumber.
caja := OrderedCollection with: n. "una 'caja' mutable"
caja at: 1 put: (caja first * 2).
Transcript
show: 'antes=', n printString;
show: ' despues=', caja first printString;
cr.
Lo que esta clase enseña en Smalltalk. Smalltalk no tiene paso por referencia, y no puede tenerlo: los parámetros son constantes (clase 079) y los números son inmutables.
El sustituto es el mismo que en Lisp: una caja, un objeto mutable que contiene el valor. Aquí una
OrderedCollection; en código real sería una instancia de ValueHolder, una clase de la biblioteca
pensada exactamente para esto.
| holder |
holder := ValueHolder new.
holder value: 5.
modificar: holder. "el método puede hacer holder value: 10"
holder value. "10"
ValueHolder es más que un truco: es la pieza central del patrón Observador en Smalltalk. Un
ValueHolder puede notificar a quien esté escuchando cuando su contenido cambia, y sobre eso se
construyó toda la capa de enlace de datos de VisualWorks — el antepasado del data binding que hoy
tienen Angular, Vue y SwiftUI.
Es un buen cierre para esta clase: la carencia de paso por referencia empujó a Smalltalk a convertir "un valor que cambia" en un objeto de primera clase, y de ahí salió una idea que la industria adoptó cuarenta años después con otro nombre.
Y en la práctica, la pregunta rara vez se plantea: como los objetos son referencias, un método que recibe una colección o un modelo ya puede modificarlo. La caja solo hace falta para los inmutables.
Lo transferible: si el lenguaje permite mutar argumentos, la firma debe decirlo. Es la misma
regla que la clase 073 sobre los modos, y la razón de que Fortran y Ada insistan tanto en intent y
en in out. Y en los lenguajes sin paso por referencia —Lisp, Smalltalk, Java, Python— el sustituto
es siempre el mismo: envolver el valor en un objeto mutable, que es exactamente lo que hace este
programa en Lisp con una lista de un elemento.
⏮️ Volver a la clase 080 · 🧬 Los primos del Atlas · 🧟 Índice de lenguajes vivos