🧬 El mismo programa en las familias de lenguajes — Clase 083

⬅️ Volver a la clase 083 · 🌐 Atlas de familias · 📚 Índice

Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la familia entera. El mismo problema de la clase —una función que devuelve otra función que recuerda un valor base— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del núcleo.

El cierre es donde más se nota la personalidad de cada lenguaje: no cambia si se captura, cambia qué se captura —el valor o la variable— y cuánto hay que escribir para decirlo.

⚠️ Qué está verificado y qué no. Ruby, Perl y Lua se ejecutan en CI contra el mismo casos.json que el núcleo, igual que las diez implementaciones de la clase (workflow Labs). Los otros 17 primos son material de lectura: su toolchain no está en el workflow, así que están escritos para ser correctos pero sin el sello de la máquina. Verificar tres de veinte no es verificarlos todos.

El contrato, igual para todos

stdin esperado
10 r1=11 r2=12
0 r1=1 r2=2
100 r1=101 r2=102

Scripting dinámico

Representantes del núcleo: Python · PHP. Funciones anónimas baratas y captura automática del entorno léxico… casi siempre.

Ruby

base = STDIN.gets.to_i
sumar = ->(x) { base + x }   # lambda: aridad estricta y 'return' propio
puts "r1=#{sumar.call(1)} r2=#{sumar.(2)}"

Perl

my $base = <STDIN>;
chomp $base;
my $sumar = sub { return $base + $_[0] };  # cierre sobre la léxica 'my $base'
printf "r1=%d r2=%d\n", $sumar->(1), $sumar->(2);

Lua

local function hacer_sumador(b)
  return function(x) return b + x end   -- 'b' pasa a ser un upvalue del cierre
end

local base = io.read("n")
local sumar = hacer_sumador(base)
print(string.format("r1=%d r2=%d", sumar(1), sumar(2)))

Tcl

# Tcl no captura el entorno léxico: 'apply' recibe el valor ligado como argumento.
gets stdin base
set sumar [list apply {{b x} {expr {$b + $x}}} $base]
puts "r1=[{*}$sumar 1] r2=[{*}$sumar 2]"

R

hacer_sumador <- function(b) {
  force(b)                    # vence la evaluación perezosa: fija el valor ahora
  function(x) b + x
}

base <- as.integer(readLines("stdin", n = 1))
sumar <- hacer_sumador(base)
cat(sprintf("r1=%d r2=%d\n", sumar(1L), sumar(2L)))

Qué reconocer: Ruby es el que más vocabulario tiene: un block es un argumento implícito que no es un objeto, un proc es un block cosificado con aridad laxa y return que sale del método que lo creó, y una lambda es estricta con la aridad y devuelve solo de sí misma —tres cosas parecidas con semánticas distintas—. Perl captura las léxicas declaradas con my y solo esas: una our global no se congela en el cierre. Lua nombra explícitamente lo capturado, los upvalues, y los comparte entre cierres creados en el mismo alcance. R esconde una trampa propia: por su evaluación perezosa, sin force(b) el valor se resolvería al llamar y no al crear. Tcl es el único que no tiene cierres: hay que empaquetar el valor a mano con apply.


JavaScript / web

Representantes del núcleo: JavaScript · TypeScript.

Dart

import 'dart:io';

int Function(int) hacerSumador(int b) => (x) => b + x; // captura léxica

void main() {
  final base = int.parse(stdin.readLineSync()!.trim());
  final sumar = hacerSumador(base);
  print('r1=${sumar(1)} r2=${sumar(2)}');
}

ActionScript 3

// Sin stdin en el reproductor Flash; el cierre sí es exactamente el de JavaScript.
package {
    public class Cierres {
        public static function hacerSumador(base:int):Function {
            return function(x:int):int { return base + x; }; // captura 'base'
        }
    }
}

Qué reconocer: la familia entera hereda el cierre de Scheme vía JavaScript, y por eso el gesto es idéntico en los cuatro: una función devuelta que sigue viendo la variable de quien la creó. La captura es de la variable, no del valor, y de ahí nace el error clásico de capturar el contador de un bucle var y verlo terminado en todas las iteraciones. Dart evita ese caso porque cada vuelta de su for crea un enlace nuevo, igual que let en JavaScript moderno. ActionScript 3 conserva la trampa completa: es ECMAScript 4, la misma máquina.


JVM

Representante del núcleo: Java. Aquí la diferencia entre primos es real y visible, porque Java impone una restricción que los demás no.

Kotlin

fun hacerSumador(base: Int): (Int) -> Int = { x -> base + x }

fun main() {
    val base = readLine()!!.trim().toInt()
    val sumar = hacerSumador(base)
    println("r1=${sumar(1)} r2=${sumar(2)}")
}

Scala

object Cierres extends App {
  val base = scala.io.StdIn.readLine().trim.toInt
  val sumar: Int => Int = x => base + x
  println(s"r1=${sumar(1)} r2=${sumar(2)}")
}

Groovy

def base = System.in.newReader().readLine().trim() as int
def sumar = { x -> base + x }   // el cierre guarda 'owner' y 'delegate', no solo el valor
println "r1=${sumar(1)} r2=${sumar(2)}"

Clojure

(require '[clojure.string :as str])

(defn hacer-sumador [base]
  (fn [x] (+ base x)))     ; captura un valor inmutable, no una variable

(let [base (Integer/parseInt (str/trim (read-line)))
      sumar (hacer-sumador base)]
  (println (str "r1=" (sumar 1) " r2=" (sumar 2))))

Qué reconocer: Java exige que la variable capturada sea final o efectivamente final: la lambda copia el valor en un campo sintético y por eso el compilador prohíbe modificarla después —una restricción que se ve en la implementación de la clase—. Kotlin no la tiene: puede capturar un var y modificarlo desde dentro, porque lo envuelve en un objeto Ref en el montón. Groovy va aún más lejos: su Closure es un objeto con owner, thisObject y delegate, y ese delegate reconfigurable es lo que hace posibles los DSL de Gradle. Scala captura como Java pero su val por defecto hace que la restricción casi nunca se note. Clojure cambia el problema: captura valores inmutables, nunca variables, así que la pregunta "¿y si cambia después?" no puede plantearse; para estado mutable hace falta un atom explícito.


.NET

Representante del núcleo: C#.

F#

let hacerSumador b = fun x -> b + x   // aplicación parcial: 'hacerSumador b' ya es la función

let b = int (stdin.ReadLine().Trim())
let sumar = hacerSumador b
printfn "r1=%d r2=%d" (sumar 1) (sumar 2)

VB.NET

Module Cierres
    Function HacerSumador(valorBase As Integer) As Func(Of Integer, Integer)
        Return Function(x) valorBase + x   ' captura la variable, no una copia
    End Function

    Sub Main()
        Dim b As Integer = Integer.Parse(Console.ReadLine().Trim())
        Dim sumar = HacerSumador(b)
        Console.WriteLine("r1=" & sumar(1) & " r2=" & sumar(2))
    End Sub
End Module

Qué reconocer: el CLR captura por referencia a la variable, no por valor: el compilador crea una clase oculta con la variable como campo y la comparte entre el cierre y el código que lo creó, justo lo contrario de la copia que hace Java. Eso permite que un cierre de VB.NET o C# modifique la variable capturada y que el cambio se vea fuera. F# lo hace innecesario: sus enlaces son inmutables por defecto y la currificación es nativa —hacerSumador b devuelve una función sin escribir fun ni return—, que es la misma idea que la aplicación parcial de Prolog más abajo.


C / llaves

Representante del núcleo: C. C no tiene cierres: sus primos tuvieron que inventárselos, y cada uno lo hizo distinto.

C++

#include <iostream>
#include <functional>

std::function<int(int)> hacer_sumador(int base) {
    return [base](int x) { return base + x; }; // [base] = captura por COPIA
    // Con [&base] capturaría por referencia y devolvería un puntero colgante.
}

int main() {
    int base;
    std::cin >> base;
    auto sumar = hacer_sumador(base);
    std::cout << "r1=" << sumar(1) << " r2=" << sumar(2) << '\n';
}

Objective-C

#import <Foundation/Foundation.h>

typedef int (^Sumador)(int);

int main(void) {
    @autoreleasepool {
        int base;
        scanf("%d", &base);
        // Un bloque captura por copia; con __block la variable sería compartida.
        Sumador sumar = ^(int x) { return base + x; };
        printf("r1=%d r2=%d\n", sumar(1), sumar(2));
    }
    return 0;
}

Qué reconocer: los dos hacen explícita la decisión que los demás toman por ti. En C++ la lista de captura lo dice todo: [base] copia, [&base] toma referencia —y devolver ese cierre deja un puntero colgante, un error que ningún recolector te va a perdonar—. Objective-C usa ^ para sus bloques, que copian por defecto y necesitan __block para compartir la variable, además de Block_copy para sobrevivir al alcance donde nacieron. Es la misma pregunta que Rust responde con move y Fn/FnMut/FnOnce: quién es dueño de lo capturado y cuánto vive.


Sistemas

Representantes del núcleo: Go · Rust. Donde no hay recolector, capturar significa decidir dónde vive el estado capturado.

Zig

const std = @import("std");

// Zig NO tiene cierres: el estado capturado se declara a mano en una estructura.
const Sumador = struct {
    base: i64,
    fn llamar(self: Sumador, x: i64) i64 {
        return self.base + x;
    }
};

pub fn main() !void {
    var buf: [64]u8 = undefined;
    const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
    const base = try std.fmt.parseInt(i64, std.mem.trim(u8, linea, " \r\n"), 10);
    const sumar = Sumador{ .base = base };
    try std.io.getStdOut().writer().print(
        "r1={d} r2={d}\n",
        .{ sumar.llamar(1), sumar.llamar(2) },
    );
}

Nim

import std/strutils

proc hacerSumador(base: int): proc (x: int): int =
  result = proc (x: int): int = base + x   # el entorno capturado va al montón

let base = stdin.readLine().strip().parseInt()
let sumar = hacerSumador(base)
echo "r1=", sumar(1), " r2=", sumar(2)

D

import std.stdio, std.string, std.conv;

int delegate(int) hacerSumador(int base) {
    return (int x) => base + x;   // 'delegate' = puntero a función + puntero al contexto
}

void main() {
    const base = readln().strip().to!int;
    auto sumar = hacerSumador(base);
    writefln("r1=%d r2=%d", sumar(1), sumar(2));
}

Qué reconocer: Zig no tiene cierres en absoluto y es coherente con su lema de "ninguna asignación de memoria oculta": si el estado capturado tiene que sobrevivir a la función, tú declaras la estructura y tú decides dónde guardarla. Nim y D sí los tienen y ambos delatan el coste en el tipo: D distingue function (solo puntero, sin contexto) de delegate (puntero + contexto), y Nim mueve el entorno capturado al montón salvo que el cierre no escape. Rust hace la misma contabilidad con move y los tres rasgos Fn, y Go la esconde tras el análisis de escape del compilador. Ninguno regala el cierre: solo cambia quién paga y si te lo cuenta.


Lógica y declarativa

Representante del núcleo: SQL. Sin funciones de primera clase, la captura se convierte en otra cosa.

Prolog

:- initialization(main, main).

sumar(Base, X, R) :- R is Base + X.

main :-
    read_line_to_string(user_input, Linea),
    number_string(Base, Linea),
    Sumar = sumar(Base),        % aplicación parcial: el término lleva Base dentro
    call(Sumar, 1, R1),         % call/3 completa los argumentos que faltaban
    call(Sumar, 2, R2),
    format("r1=~d r2=~d~n", [R1, R2]).

Datalog

% Datalog no tiene funciones de primera clase ni cierres: la "captura" consiste
% en ligar la base dentro de la propia regla, y la regla no es un valor.
base(10).

r1(R) :- base(B), R = B + 1.
r2(R) :- base(B), R = B + 2.

Qué reconocer: Prolog se acerca sorprendentemente al cierre sin tenerlo: sumar(Base) es un término parcialmente aplicado que call/N completa después, y ese término lleva el valor dentro igual que un cierre lleva su entorno. La diferencia es que Base está ligada de una vez y para siempre: no existe la duda de si el cierre ve el valor o la variable, porque la variable no puede cambiar. Datalog ni siquiera llega ahí —una regla no es un valor que se pueda pasar—, y como SQL, sustituye la función por una relación con un parámetro más.


Y de vuelta a la clase

Veinte lenguajes, un solo problema, y una pregunta que los separa a todos: ¿el cierre se lleva el valor o la variable? Java copia y por eso exige final; .NET comparte la variable; C++ y Objective-C te obligan a elegir por escrito; Clojure elimina la pregunta haciendo inmutable todo; y Zig, Tcl y Datalog contestan que no hay cierre que valga.

⏮️ Volver a la clase 083 · 🌐 Ver las familias en el Atlas