Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la familia entera. El mismo problema de la clase —tratar una lista como un flujo: filtrar los pares y duplicarlos— resuelto por los primos de cada familia del Atlas, no solo por los diez lenguajes del núcleo.
Si entendiste la versión de JavaScript, 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.jsonque 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.
stream=<pares duplicados, unidos por ->filtrar pares → map x → 2x| stdin | esperado |
|---|---|
1 2 3 4 |
stream=4-8 |
2 4 |
stream=4-8 |
6 7 8 |
stream=12-16 |
Representantes del núcleo: Python · PHP.
La tubería filtrar → transformar existe en todos, pero solo algunos la hacen perezosa: es
decir, capaz de correr sobre un flujo infinito sin materializar la lista entera.
nums = STDIN.read.split.map(&:to_i)
stream = nums.lazy.select(&:even?).map { |x| x * 2 }
puts "stream=#{stream.to_a.join('-')}"
my @nums = split ' ', do { local $/; <STDIN> };
my @stream = map { $_ * 2 } grep { $_ % 2 == 0 } @nums;
print "stream=", join('-', @stream), "\n";
-- Lua no trae flujos ni map/filter: la corrutina hace de productor perezoso.
local function flujo(t)
return coroutine.wrap(function()
for _, x in ipairs(t) do
if x % 2 == 0 then coroutine.yield(x * 2) end
end
end)
end
local nums = {}
for s in io.read("l"):gmatch("%S+") do
nums[#nums + 1] = tonumber(s)
end
local salida = {}
for v in flujo(nums) do
salida[#salida + 1] = v
end
print("stream=" .. table.concat(salida, "-"))
gets stdin linea
set stream {}
foreach x [split $linea] {
if {$x % 2 == 0} {
lappend stream [expr {$x * 2}]
}
}
puts "stream=[join $stream -]"
v <- as.integer(strsplit(readLines("stdin", n = 1), " +")[[1]])
stream <- v[v %% 2 == 0] * 2
cat(paste0("stream=", paste(stream, collapse = "-"), "\n"))
Qué reconocer: Perl escribe la tubería al revés —map a la izquierda, grep a la derecha—
porque se lee de dentro hacia fuera, no de izquierda a derecha como el .filter().map() que ya
conoces. Ruby es el único de los cinco con pereza declarada: sin .lazy, select construye una
lista intermedia; con .lazy, cada elemento atraviesa toda la cadena antes de que empiece el
siguiente. Tcl no tiene ninguna de las dos cosas y vuelve al bucle explícito. R no necesita el flujo
porque la operación ya es vectorial: v %% 2 == 0 produce un vector de verdaderos y falsos que
indexa el original, y multiplicar por 2 se aplica a todos los elementos a la vez.
Representantes del núcleo: JavaScript · TypeScript. Aquí es donde el reactivo dejó de ser una biblioteca y se volvió parte del lenguaje.
import 'dart:io';
void main() async {
final nums = stdin.readLineSync()!.trim().split(RegExp(r'\s+')).map(int.parse);
final flujo = Stream.fromIterable(nums)
.where((x) => x % 2 == 0)
.map((x) => x * 2);
final salida = await flujo.toList();
print('stream=${salida.join('-')}');
}
// ActionScript no tiene flujos ni stdin: lo más cercano son filter/map sobre Array,
// que son ansiosos y trabajan sobre datos ya completos en memoria.
package {
public class Flujo {
public static function procesar(nums:Array):String {
var stream:Array = nums
.filter(function (x:int, i:int, a:Array):Boolean { return x % 2 == 0; })
.map(function (x:int, i:int, a:Array):int { return x * 2; });
return "stream=" + stream.join("-");
}
}
}
Qué reconocer: Dart es el caso más limpio de toda esta página: Stream es un tipo del núcleo
del lenguaje, con where y map propios y con await integrado, de modo que una secuencia de
valores en el tiempo se trata igual que una lista. ActionScript enseña el punto de partida: sus
filter y map son los mismos de Array en JavaScript —ansiosos, sobre datos ya presentes— y para
lo asíncrono había que bajar a eventos. La distancia entre esos dos bloques es exactamente la que
recorrió la programación reactiva.
Representante del núcleo: Java. El Stream de Java 8 puso la tubería perezosa
en la biblioteca estándar; sus primos fueron más lejos, hasta el flujo asíncrono.
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val nums = readLine()!!.trim().split(Regex("\\s+")).map { it.toInt() }
val salida = nums.asFlow()
.filter { it % 2 == 0 }
.map { it * 2 }
.toList()
println("stream=" + salida.joinToString("-"))
}
object Flujo extends App {
val nums = scala.io.StdIn.readLine().trim.split("\\s+").map(_.toInt)
val stream = LazyList.from(nums).filter(_ % 2 == 0).map(_ * 2)
println(s"stream=${stream.mkString("-")}")
}
def nums = System.in.text.trim().split(/\s+/)*.toInteger()
def stream = nums.findAll { it % 2 == 0 }.collect { it * 2 }
println "stream=${stream.join('-')}"
(require '[clojure.string :as str])
(let [nums (map parse-long (str/split (str/trim (read-line)) #"\s+"))
stream (sequence (comp (filter even?) (map #(* 2 %))) nums)]
(println (str "stream=" (str/join "-" stream))))
Qué reconocer: los cuatro dicen lo mismo con cuatro vocabularios distintos, y ahí está el truco
de la familia. Groovy usa los nombres de Smalltalk (findAll, collect) y es ansioso: cada
paso construye una lista nueva. Scala usa LazyList, una secuencia perezosa que solo calcula lo que
alguien pide. Kotlin es el único con un flujo asíncrono —Flow vive en la biblioteca
kotlinx.coroutines, no en el núcleo, y por eso hace falta runBlocking—: sus operadores pueden
suspenderse esperando datos. Clojure separa la operación de la colección: (comp (filter even?)
(map ...)) es un transductor, una tubería que no menciona sobre qué va a correr y sirve igual
para una secuencia, un canal o un flujo.
Representante del núcleo: C#. LINQ es la tubería perezosa del CLR, y todos los lenguajes de la plataforma la comparten.
open System
let nums =
Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries)
|> Seq.map int
let stream =
nums
|> Seq.filter (fun x -> x % 2 = 0)
|> Seq.map (fun x -> x * 2)
printfn "stream=%s" (String.Join("-", stream))
Imports System.Linq
Module Flujo
Sub Main()
Dim nums = Console.ReadLine().Trim() _
.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) _
.Select(Function(s) Integer.Parse(s))
Dim stream = nums.Where(Function(x) x Mod 2 = 0).Select(Function(x) x * 2)
Console.WriteLine("stream=" & String.Join("-", stream))
End Sub
End Module
Qué reconocer: Where/Select de VB.NET y Seq.filter/Seq.map de F# son la misma
IEnumerable perezosa vista desde dos culturas: la de los métodos de extensión encadenados y la
de las funciones con |>. Ninguna de las dos calcula nada hasta que String.Join recorre el
resultado. .NET es también donde nació Rx (IObservable), el modelo reactivo que después copiaron
RxJava, RxJS y compañía: la idea de que un IEnumerable que empuja valores en vez de esperar a que
se los pidan es su dual exacto.
Representante del núcleo: C. Sin tuberías en el lenguaje, el flujo es un bucle
con un if dentro… hasta C++20.
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> nums;
for (int x; std::cin >> x;) nums.push_back(x);
auto stream = nums
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 2; });
std::cout << "stream=";
bool primero = true;
for (int v : stream) {
if (!primero) std::cout << '-';
std::cout << v;
primero = false;
}
std::cout << '\n';
}
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSData *entrada = [[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile];
NSString *linea = [[NSString alloc] initWithData:entrada encoding:NSUTF8StringEncoding];
NSMutableArray<NSNumber *> *stream = [NSMutableArray array];
NSCharacterSet *espacios = [NSCharacterSet whitespaceAndNewlineCharacterSet];
for (NSString *s in [linea componentsSeparatedByCharactersInSet:espacios]) {
if (s.length == 0) continue;
NSInteger x = [s integerValue];
if (x % 2 == 0) [stream addObject:@(x * 2)];
}
printf("stream=%s\n", [[stream componentsJoinedByString:@"-"] UTF8String]);
}
return 0;
}
Qué reconocer: el | de C++20 no es el or a nivel de bits: los ranges redefinieron ese
operador para encadenar vistas, que no copian nada y solo calculan cuando el for avanza. Es
LINQ compilado a coste cero. Objective-C se quedó en la era anterior —NSArray no tiene filter
ni map, solo filteredArrayUsingPredicate: con NSPredicate— y por eso lo idiomático sigue
siendo el bucle con acumulador, que es literalmente la versión de C con objetos encima.
Representantes del núcleo: Go · Rust. Los iteradores de Rust demostraron que la tubería perezosa puede compilar al mismo bucle que escribirías a mano.
const std = @import("std");
pub fn main() !void {
var buf: [256]u8 = undefined;
const leido = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
var it = std.mem.tokenizeScalar(u8, std.mem.trim(u8, leido, " \r"), ' ');
// Zig no tiene filter/map en la biblioteca estándar: la tubería es el propio bucle.
const out = std.io.getStdOut().writer();
try out.writeAll("stream=");
var primero = true;
while (it.next()) |tok| {
const x = try std.fmt.parseInt(i64, tok, 10);
if (@rem(x, 2) != 0) continue;
if (!primero) try out.writeAll("-");
try out.print("{d}", .{x * 2});
primero = false;
}
try out.writeAll("\n");
}
import std/[strutils, sequtils]
let nums = stdin.readLine().splitWhitespace().map(parseInt)
let stream = nums.filterIt(it mod 2 == 0).mapIt($(it * 2))
echo "stream=" & stream.join("-")
import std.algorithm, std.array, std.conv, std.stdio, std.string;
void main() {
auto stream = readln().strip().split()
.map!(to!int)
.filter!(x => x % 2 == 0)
.map!(x => to!string(x * 2));
writefln("stream=%s", stream.join("-"));
}
Qué reconocer: D es el más cercano a Rust: sus map! y filter! son ranges perezosos, nada
se calcula hasta que join los recorre, y el compilador funde toda la cadena en un solo bucle. Nim
llega al mismo aspecto por otra vía —filterIt/mapIt son macros que reescriben la expresión
en tiempo de compilación, con it como variable implícita— pero son ansiosos: construyen un seq
intermedio. Zig es la excepción deliberada: su biblioteca estándar no ofrece filter ni map
porque el lenguaje evita las abstracciones que esconden asignaciones de memoria, así que el flujo se
escribe como lo que realmente es en la máquina, un bucle con continue.
Representante del núcleo: SQL. WHERE seguido de SELECT ya es una tubería
declarativa: se dice qué se quiere, no cómo recorrerlo.
:- initialization(main, main).
main :-
read_line_to_string(user_input, Linea),
split_string(Linea, " ", " ", Partes),
maplist([S, N]>>number_string(N, S), Partes, Nums),
include([X]>>(0 is X mod 2), Nums, Pares),
maplist([X, Y]>>(Y is X * 2), Pares, Dobles),
maplist(number_string, Dobles, Textos),
atomic_list_concat(Textos, '-', Salida),
format("stream=~w~n", [Salida]).
% Datalog no tiene E/S ni flujos: no hay "primero filtra, luego transforma", porque
% no hay orden. Solo se declara qué relación deriva de cuál; el motor decide el resto.
num(1).
num(2).
num(3).
num(4).
par(X) :- num(X), X mod 2 = 0.
doble(Y) :- par(X), Y = X * 2.
Qué reconocer: include/3 y maplist/3 son el filter y el map de Prolog, con una diferencia
de fondo: en vez de una función que devuelve un valor, reciben un objetivo que debe tener éxito
—include conserva los elementos para los que el objetivo es demostrable—. Datalog quita hasta eso:
par y doble son dos relaciones y la tubería es solo el hecho de que la segunda menciona a la
primera. No hay tiempo, no hay pereza, no hay orden de llegada. Es la misma renuncia de SQL, y por
eso la palabra stream no significa nada aquí: un flujo necesita un antes y un después.
Veinte lenguajes, un solo problema, y una misma pregunta detrás: cuándo se calcula cada elemento. Ansioso (Groovy, Nim), perezoso (Scala, D, ranges de C++), asíncrono (Dart, Flow de Kotlin) o sin tiempo en absoluto (Datalog). El código se parece; las garantías no. Eso es lo transferible.