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

âŹ…ïž Volver a la clase 161 · 🌐 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 —consumir los mensajes de una cola y sumarlos— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del nĂșcleo.

Si entendiste la versiĂłn de Python, la de Ruby te resultarĂĄ familiar aunque no la hayas visto nunca. Ese reconocimiento es exactamente lo que este curso quiere producir.

⚠ 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
1 2 3 recibido=6
5 recibido=5
10 20 30 40 recibido=100

Scripting dinĂĄmico

Representantes del nĂșcleo: Python · PHP. Los lenguajes del pegamento entre procesos: nacidos para leer de una tuberĂ­a y escribir en otra.

Ruby

cola = STDIN.read.split.map(&:to_i)
puts "recibido=#{cola.sum}"

Perl

use List::Util qw(sum0);

my @cola = split ' ', <STDIN>;
printf "recibido=%d\n", sum0(@cola);

Lua

-- Lua no trae sockets en su biblioteca estĂĄndar (harĂ­a falta LuaSocket),
-- pero io.read sobre stdin sĂ­ es parte del nĂșcleo.
local recibido = 0
for m in io.read("l"):gmatch("%-?%d+") do
  recibido = recibido + tonumber(m)
end
print("recibido=" .. recibido)

Tcl

# Tcl trae `socket` en el nĂșcleo del lenguaje; aquĂ­ basta el canal stdin,
# que es un canal mĂĄs, indistinguible de un socket para el resto del cĂłdigo.
gets stdin linea
set recibido 0
foreach m [split [string trim $linea]] {
    incr recibido $m
}
puts "recibido=$recibido"

R

cola <- scan("stdin", what = integer(), quiet = TRUE)
cat(sprintf("recibido=%d\n", sum(cola)))

QuĂ© reconocer: los cinco leen la cola igual —una lĂ­nea de stdin, partir, sumar— y ahĂ­ acaba el parecido. En sockets la familia se rompe: Ruby trae TCPServer y Perl IO::Socket en su distribuciĂłn, Lua no trae ninguno y necesita LuaSocket, y R los abre con socketConnection pero de forma bloqueante y sin bucle de eventos. Tcl es el mĂĄs elegante de la tanda: socket estĂĄ en el nĂșcleo del lenguaje, devuelve un canal que se lee con el mismo gets que stdin, y fileevent mĂĄs vwait dan un servidor concurrente sin hilos ni bibliotecas. Un programa Tcl puede cambiar de tuberĂ­a a red sustituyendo una lĂ­nea.


JavaScript / web

Representantes del nĂșcleo: JavaScript · TypeScript. Familia del bucle de eventos: la cola de mensajes no es una biblioteca, es el modelo de ejecuciĂłn.

Dart

import 'dart:io';

void main() {
  final cola = stdin.readLineSync()!.trim().split(RegExp(r'\s+')).map(int.parse);
  final recibido = cola.fold<int>(0, (a, b) => a + b);
  print('recibido=$recibido');
}

ActionScript 3

// ActionScript corre en el reproductor Flash: no tiene stdin ni procesos hijos,
// solo flash.net.Socket. Se ilustra el consumo de la cola ya recibida.
package {
    public class Cola {
        public static function recibir(mensajes:Array):String {
            var recibido:int = 0;
            for each (var m:int in mensajes) {
                recibido += m;
            }
            return "recibido=" + recibido;
        }
    }
}

QuĂ© reconocer: ActionScript marca el lĂ­mite duro de esta clase: no tiene stdin ni puede lanzar procesos, porque el reproductor Flash era una caja aislada; lo Ășnico que le queda es flash.net.Socket, y aun asĂ­ solo hacia servidores que publiquen una polĂ­tica de seguridad. Dart es lo contrario: dart:io trae stdin, Process.start y ServerSocket, y añade los isolates, que se comunican por paso de mensajes con SendPort —una cola real dentro del proceso, sin memoria compartida, igual que si fueran procesos separados. Es el mismo modelo de los Web Workers de JavaScript, con la misma consecuencia: lo que cruza la frontera se copia.


JVM

Representante del nĂșcleo: Java. Hilos con memoria compartida, y sobre ellos todas las abstracciones de cola que existen.

Kotlin

fun main() {
    val recibido = readLine()!!.trim().split(Regex("\\s+")).sumOf { it.toInt() }
    println("recibido=$recibido")
}

Scala

object Cola extends App {
  val recibido = scala.io.StdIn.readLine().trim.split("\\s+").map(_.toInt).sum
  println(s"recibido=$recibido")
}

Groovy

def recibido = System.in.newReader().readLine().trim().split(/\s+/)*.toInteger().sum()
println "recibido=$recibido"

Clojure

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

(let [cola (map parse-long (str/split (str/trim (read-line)) #"\s+"))]
  (println (str "recibido=" (reduce + cola))))

QuĂ© reconocer: los cuatro heredan de Java el mismo System.in, el mismo ProcessBuilder para lanzar procesos hijos y la misma BlockingQueue para las colas dentro del proceso. Lo que cambia es la abstracciĂłn que cada comunidad pone encima: Kotlin usa corrutinas y Channel, Scala el modelo de actores de Akka —donde cada actor tiene su propio buzĂłn y nada se comparte—, y Clojure core.async, que copia los canales de Go dentro de la JVM. Cuatro vocabularios distintos para la misma idea de esta clase: un productor deposita, un consumidor retira, y la cola desacopla su velocidad. Groovy es el que menos ceremonia pone para lo bĂĄsico, y por eso vive en los scripts de despliegue.


.NET

Representante del nĂșcleo: C#.

F#

let recibido =
    stdin.ReadLine().Split(' ', System.StringSplitOptions.RemoveEmptyEntries)
    |> Array.sumBy int
printfn "recibido=%d" recibido

VB.NET

Imports System

Module Cola
    Sub Main()
        Dim cola = Console.ReadLine().Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries)
        Dim recibido = 0
        For Each m In cola
            recibido += Integer.Parse(m)
        Next
        Console.WriteLine($"recibido={recibido}")
    End Sub
End Module

Qué reconocer: los tres comparten Console.In, Process con RedirectStandardOutput = True para hablar con un hijo por tuberías, y System.Threading.Channels para la cola en memoria. F# añade lo mås parecido a Erlang que hay en el CLR: el MailboxProcessor, un agente con buzón propio que se lee con Receive() en un bucle asíncrono, sin bloqueo alguno ni cerrojos. VB.NET no tiene nada equivalente en su sintaxis y usa las mismas clases con For Each, lo que deja claro que en esta plataforma la concurrencia vive en la biblioteca, no en el lenguaje.


C / llaves

Representante del nĂșcleo: C. La capa donde stdin, socket y tuberĂ­a son lo mismo: un descriptor de fichero.

C++

#include <iostream>

int main() {
    long long recibido = 0, m;
    while (std::cin >> m) {
        recibido += m;
    }
    std::cout << "recibido=" << recibido << '\n';
}

Objective-C

#import <Foundation/Foundation.h>

int main(void) {
    @autoreleasepool {
        long long recibido = 0, m;
        while (scanf("%lld", &m) == 1) {
            recibido += m;
        }
        printf("recibido=%lld\n", recibido);
    }
    return 0;
}

QuĂ© reconocer: en esta familia se ve el mecanismo desnudo: fork duplica el proceso, pipe crea el par de descriptores y exec reemplaza el programa, que es literalmente lo que hacen por dentro subprocess de Python y ProcessBuilder de Java. C++ envuelve el descriptor en un flujo —std::cin es el mismo stdin de C con otra cara— y desde C++11 añade std::thread y colas propias, pero sin socket en la biblioteca estĂĄndar. Objective-C sĂ­ lo trae todo en Foundation: NSTask para el proceso hijo, NSPipe para el canal y NSRunLoop para atenderlos sin bloquear, un bucle de eventos muy parecido al de Tcl. Es la misma idea, empaquetada por Apple.


Sistemas

Representantes del nĂșcleo: Go · Rust. Nativos, sin runtime pesado, con acceso directo a las llamadas al sistema.

Zig

const std = @import("std");

pub fn main() !void {
    var buf: [256]u8 = undefined;
    const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
    var it = std.mem.tokenizeAny(u8, linea, " \r\t");
    var recibido: i64 = 0;
    while (it.next()) |m| {
        recibido += try std.fmt.parseInt(i64, m, 10);
    }
    try std.io.getStdOut().writer().print("recibido={d}\n", .{recibido});
}

Nim

import std/[strutils, sequtils, math]

let cola = stdin.readLine().splitWhitespace().map(parseInt)
echo "recibido=", cola.sum()

D

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

void main() {
    auto cola = readln().strip().split().map!(to!long);
    writefln("recibido=%d", cola.sum());
}

QuĂ© reconocer: los tres traen en la biblioteca estĂĄndar lo que C deja al sistema operativo: std.process.Child en Zig, std/osproc en Nim y std.process en D lanzan un hijo y capturan su salida en tres lĂ­neas, sin tocar fork. Y los tres traen serializador JSON incluido, asĂ­ que pueden poner un mensaje estructurado en la cola sin dependencias. En sockets se separan de Go y Rust: Zig expone std.net a bajo nivel y sin abstracciĂłn de concurrencia, mientras D ofrece paso de mensajes entre hilos con std.concurrency —send y receive sobre buzones, con los datos aislados por el sistema de tipos, la misma renuncia a la memoria compartida que hace Rust.


LĂłgica y declarativa

Representante del nĂșcleo: SQL. Describir el resultado, no el recorrido: sumar la cola es una agregaciĂłn, no un bucle.

Prolog

:- initialization(main, main).

main :-
    read_line_to_string(user_input, Linea),
    split_string(Linea, " ", " ", Partes),
    maplist([S, N]>>number_string(N, S), Partes, Cola),
    sum_list(Cola, Recibido),
    format("recibido=~w~n", [Recibido]).

Datalog

% Datalog puro no tiene E/S, procesos ni agregaciĂłn: los mensajes se declaran
% como hechos. Sumarlos exige un dialecto con agregados, como Soufflé:
%   recibido(t) :- t = sum v : { mensaje(_, v) }.
mensaje(1, 1).
mensaje(2, 2).
mensaje(3, 3).

QuĂ© reconocer: Prolog sĂ­ puede hablar con el mundo —read_line_to_string/2 sobre user_input, y SWI-Prolog trae process_create/3 y sockets—, pero la suma se expresa como sum_list/2 sobre la lista completa, no como un acumulador que se reasigna en un bucle. Datalog no puede ni eso: sin E/S, sin efectos y sin agregaciĂłn en el nĂșcleo, los mensajes solo existen como hechos declarados, y la suma requiere salirse a un dialecto como SoufflĂ©. Es la misma renuncia de SQL, que agrega con sum() y no te deja decidir en quĂ© orden se recorren las filas.


Y de vuelta a la clase

Veinte lenguajes, un solo problema, y una conclusiĂłn que vale mĂĄs que el cĂłdigo: los mecanismos de comunicaciĂłn difieren enormemente —Tcl tiene sockets en el nĂșcleo, Lua no tiene ninguno, ActionScript ni siquiera puede lanzar un proceso—, pero stdin/stdout es el Ășnico mecanismo que todos comparten. Por eso el verificador de equivalencia de este curso estĂĄ construido sobre Ă©l: es el mĂ­nimo comĂșn denominador de la interoperabilidad, y el Ășnico contrato que ningĂșn lenguaje se niega a cumplir. Eso es lo transferible.

âźïž Volver a la clase 161 · 🌐 Ver las familias en el Atlas