Esta página lleva la tesis del programa hasta el final: aprende el representante, reconoce la
familia entera. El mismo problema de la clase —construir la secuencia 1-2-...-n— 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. Y aquí hay un premio extra: construir una cadena por partes es el ejercicio que delata la postura de cada lenguaje frente a la mutabilidad. Unos acumulan en un buffer que se modifica; otros no permiten modificar nada y producen un valor nuevo en cada paso.
⚠️ 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.
n (con n >= 1)sec=1-2-...-n1 a n con el separador -| stdin | esperado |
|---|---|
3 |
sec=1-2-3 |
1 |
sec=1 |
5 |
sec=1-2-3-4-5 |
Representantes del núcleo: Python · PHP. Tipado dinámico y estructuras mutables por defecto: la lista se llena a empujones y solo al final se convierte en texto. La familia entera comparte ese gesto.
n = STDIN.gets.to_i
puts "sec=#{(1..n).to_a.join('-')}"
my $n = <STDIN>;
chomp $n;
print "sec=", join('-', 1 .. $n), "\n";
local n = tonumber(io.read("l"))
local partes = {}
for i = 1, n do
partes[#partes + 1] = tostring(i)
end
print("sec=" .. table.concat(partes, "-"))
gets stdin n
set partes {}
for {set i 1} {$i <= $n} {incr i} {
lappend partes $i
}
puts "sec=[join $partes -]"
n <- as.integer(readLines("stdin", n = 1))
cat(sprintf("sec=%s\n", paste(seq_len(n), collapse = "-")))
Qué reconocer: los cinco ofrecen un join / concat que evita concatenar la cadena una y otra
vez, porque en todos ellos las cadenas son inmutables y cada + crearía una copia nueva. Lo
mutable es el contenedor: la tabla de Lua y la lista de Tcl crecen en su sitio con lappend y
#partes + 1. Tcl es el caso más peculiar de la familia: al ser todo cadena, su "lista" es en
realidad texto con una representación interna de lista, y join funciona sobre esa doble naturaleza.
R vuelve a delatar su origen estadístico: no hay bucle ni acumulador, seq_len(n) ya es el vector
completo y paste lo colapsa de una vez.
Representantes del núcleo: JavaScript · TypeScript.
import 'dart:io';
void main() {
final n = int.parse(stdin.readLineSync()!.trim());
final partes = List<int>.generate(n, (i) => i + 1);
print('sec=${partes.join('-')}');
}
// ActionScript corre en el reproductor Flash, sin stdin: se ilustra la construcción.
package {
public class Secuencia {
public static function construir(n:int):String {
var partes:Array = [];
for (var i:int = 1; i <= n; i++) {
partes.push(i);
}
return "sec=" + partes.join("-");
}
}
}
Qué reconocer: push y join sobre un array son literalmente los mismos métodos de JavaScript;
Array en ActionScript es la clase de la que desciende el array de JS moderno. Dart marca la
diferencia con final: la referencia partes no se puede reasignar, pero la lista que hay detrás
sigue siendo mutable. Esa distinción —el nombre está fijo, el contenido no— es la misma que separa
const de un objeto congelado en JavaScript.
Representante del núcleo: Java. Todos compilan al mismo bytecode y comparten biblioteca estándar; lo que cambia es cuánta ceremonia exigen para decir lo mismo.
fun main() {
val n = readln().trim().toInt()
println("sec=" + (1..n).joinToString("-"))
}
object Secuencia extends App {
val n = scala.io.StdIn.readLine().trim.toInt
println("sec=" + (1 to n).mkString("-"))
}
def n = System.in.newReader().readLine().trim().toInteger()
println "sec=" + (1..n).join('-')
(require '[clojure.string :as str])
(let [n (Integer/parseInt (str/trim (read-line)))]
(println (str "sec=" (str/join "-" (range 1 (inc n))))))
Qué reconocer: los cuatro heredan de Java la regla de que String es inmutable, y por eso los
cuatro tienen un rango (1..n, 1 to n, range) que se colapsa de golpe en vez de un
StringBuilder. Kotlin y Scala hacen explícita la postura con val: un nombre que se liga una vez,
frente a var, que sí se reasigna. Clojure va mucho más lejos y no es solo una cuestión de estilo:
no tiene variables reasignables en absoluto. let liga un nombre a un valor dentro de su ámbito y
ahí se acaba; sus colecciones son persistentes, así que "añadir" siempre devuelve una estructura nueva
que comparte memoria con la anterior. Aquí no se nota el esfuerzo porque el problema no necesita
mutar nada, y ese es precisamente el argumento de Clojure.
Representante del núcleo: C#.
let n = stdin.ReadLine().Trim() |> int
let sec = [ 1 .. n ] |> List.map string |> String.concat "-"
printfn "sec=%s" sec
Imports System
Imports System.Text
Module Secuencia
Sub Main()
Dim n = Integer.Parse(Console.ReadLine().Trim())
Dim sb As New StringBuilder()
For i = 1 To n
If i > 1 Then sb.Append("-"c)
sb.Append(i)
Next
Console.WriteLine("sec=" & sb.ToString())
End Sub
End Module
Qué reconocer: los tres corren sobre el CLR, donde System.String es inmutable y StringBuilder
existe justamente para ofrecer el buffer mutable que falta. VB.NET usa esa herramienta de forma
canónica. F# elige el extremo opuesto: sus let son inmutables por defecto —para tener una variable
reasignable hay que pedirlo con let mutable— y su lista [ 1 .. n ] es una lista enlazada
inmutable, no un array que se rellena. Dos lenguajes, la misma biblioteca base, posturas contrarias
sobre quién debería poder cambiar qué.
Representante del núcleo: C. Memoria explícita, tipos declarados y printf.
#include <iostream>
#include <string>
int main() {
int n = 0;
std::cin >> n;
std::string sec;
for (int i = 1; i <= n; ++i) {
if (i > 1) sec += '-';
sec += std::to_string(i);
}
std::cout << "sec=" << sec << '\n';
}
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
int n = 0;
scanf("%d", &n);
NSMutableArray<NSString *> *partes = [NSMutableArray array];
for (int i = 1; i <= n; i++) {
[partes addObject:[NSString stringWithFormat:@"%d", i]];
}
NSString *sec = [partes componentsJoinedByString:@"-"];
printf("sec=%s\n", [sec UTF8String]);
}
return 0;
}
Qué reconocer: ambos son superconjuntos de C, pero aquí cada uno resuelve la mutabilidad con
la herramienta de su propia biblioteca. std::string de C++ es un buffer mutable que crece solo:
por eso sec += ... es idiomático y no el desastre de rendimiento que sería en Java o Python.
Objective-C hace explícita la distinción en el propio nombre del tipo: NSArray es inmutable y
NSMutableArray es su hermano modificable; la línea que declaras dice, sin comentarios, si el objeto
va a cambiar. Fíjate además en que el resultado final vuelve a ser un NSString inmutable.
Representantes del núcleo: Go · Rust. Compilados, sin máquina virtual, con control sobre el coste de cada operación.
const std = @import("std");
pub fn main() !void {
var buf: [64]u8 = undefined;
const linea = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')).?;
const n = try std.fmt.parseInt(u32, std.mem.trim(u8, linea, " \r"), 10);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var sec = std.ArrayList(u8).init(gpa.allocator());
defer sec.deinit();
var i: u32 = 1;
while (i <= n) : (i += 1) {
if (i > 1) try sec.append('-');
try sec.writer().print("{d}", .{i});
}
try std.io.getStdOut().writer().print("sec={s}\n", .{sec.items});
}
import std/[strutils, sequtils]
let n = stdin.readLine().strip().parseInt()
echo "sec=" & toSeq(1 .. n).mapIt($it).join("-")
import std.stdio, std.array, std.conv, std.range, std.algorithm, std.string;
void main() {
const n = readln().strip().to!int;
const sec = iota(1, n + 1).map!(to!string).array.join("-");
writeln("sec=", sec);
}
Qué reconocer: Zig es el más explícito de todos y aquí se ve por qué. La mutabilidad se declara en
la palabra clave —const frente a var, y el compilador rechaza un var que nunca cambia— y como no
hay recolector de basura, el buffer que crece exige un asignador que tú entregas y liberas con
defer. Nim y D esconden esa maquinaria: let en Nim es inmutable (para reasignar hace falta var)
y const en D marca la referencia y todo lo alcanzable a través de ella, una garantía transitiva
más fuerte que la de casi cualquier otro lenguaje de esta página.
Representante del núcleo: SQL. Se describe qué se quiere, no cómo calcularlo paso a paso.
:- initialization(main, main).
main :-
read_line_to_string(user_input, Linea),
number_string(N, Linea),
numlist(1, N, Numeros),
maplist(number_string, Numeros, Textos),
atomic_list_concat(Textos, '-', Sec),
format("sec=~w~n", [Sec]).
% Datalog puro no tiene E/S, ni aritmética, ni construcción de cadenas: solo hechos y
% reglas. Lo más cercano es enumerar los números como hechos y declarar la relación de
% sucesión; unir el resultado en una cadena queda fuera del lenguaje.
numero(1).
numero(2).
numero(3).
sigue(I, J) :- numero(I), numero(J), J = I + 1.
Qué reconocer: en Prolog no existe la mutabilidad en absoluto, y no como decisión de estilo
sino como consecuencia del modelo. Un nombre como N o Sec no es una variable que guarde un valor:
es una incógnita que se unifica una sola vez. No hay forma de escribir N = N + 1, porque eso
pediría que N sea igual a algo distinto de sí mismo, y fallaría. Por eso el programa avanza creando
nombres nuevos (Numeros, Textos, Sec) en vez de reescribir uno. Datalog lleva la renuncia al
extremo: sin efectos, sin E/S y sin funciones, solo hechos que son ciertos y reglas que derivan más
hechos ciertos. Es la misma renuncia que hace SQL al no decirte cómo recorrer las filas.
Veinte lenguajes, un solo problema, y el mismo esqueleto en todos: leer n, generar los números,
unirlos con guiones. Lo que cambia es quién tiene permiso para cambiar qué: un buffer mutable en
C++ y Zig, una lista mutable con nombre fijo en Dart, una estructura persistente en Clojure, y en
Prolog ni siquiera eso. Eso es lo transferible.