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

⬅️ Volver a la clase 159 · 🌐 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 —serializar un par clave/valor— 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
x 5 serializado=x:5
edad 30 serializado=edad:30
n 100 serializado=n:100

Scripting dinámico

Representantes del núcleo: Python · PHP. Tipado dinámico y conversión implícita entre texto y número: justo el punto donde la serialización se vuelve interesante, porque el formato de salida no recuerda de qué tipo venía el dato.

Ruby

require 'json' # la biblioteca estándar ya trae JSON, aquí basta el formato simple

clave, valor = STDIN.gets.split
puts "serializado=#{clave}:#{valor}"

Perl

my ($clave, $valor) = split ' ', <STDIN>;
print "serializado=$clave:$valor\n";

Lua

-- Lua no trae ningún serializador en su biblioteca estándar: para JSON
-- haría falta dkjson o lua-cjson. El formato `clave:valor` sí es nativo.
local clave, valor = io.read("l"):match("(%S+)%s+(%S+)")
print(string.format("serializado=%s:%s", clave, valor))

Tcl

gets stdin linea
lassign [split [string trim $linea]] clave valor
puts "serializado=$clave:$valor"

R

p <- scan("stdin", what = "", nmax = 2, quiet = TRUE)
cat(sprintf("serializado=%s:%s\n", p[1], p[2]))

Qué reconocer: los cinco parten la línea y pegan las piezas, pero se separan en qué saben del dato una vez serializado. Ruby y Perl traen serializador en la biblioteca estándar —json en Ruby, JSON::PP en el núcleo de Perl desde 5.14—, mientras que Lua no trae ninguno: hay que instalar dkjson o lua-cjson. Y hay una diferencia que muerde en producción: Lua anterior a 5.3 y R usan coma flotante doble para todo número, así que un entero de 64 bits que cruce por JSON pierde precisión silenciosamente por encima de 2^53. Tcl esquiva el problema por el lado contrario: para él todo es cadena y nunca convierte a menos que se lo pidas.


JavaScript / web

Representantes del núcleo: JavaScript · TypeScript. JSON nació aquí —es un subconjunto de los literales de objeto de JavaScript— y sus límites son los límites del tipo Number.

Dart

import 'dart:convert'; // jsonEncode viene en la biblioteca estándar
import 'dart:io';

void main() {
  final p = stdin.readLineSync()!.trim().split(RegExp(r'\s+'));
  print('serializado=${p[0]}:${p[1]}');
}

ActionScript 3

// ActionScript corre en el reproductor Flash, sin stdin: se ilustra la serialización.
// Su formato binario propio es AMF (flash.net.registerClassAlias), no JSON.
package {
    public class Serializador {
        public static function serializar(clave:String, valor:String):String {
            return "serializado=" + clave + ":" + valor;
        }
    }
}

Qué reconocer: ambos comparten con JavaScript la idea de que serializar es "convertir a texto lo que ya es un objeto", pero no comparten los tipos. ActionScript usa Number —doble— para todo número no declarado, con lo que hereda la misma pérdida de precisión que JavaScript en enteros grandes. Dart, en cambio, tiene un int de 64 bits real cuando compila a nativo, y aun así al compilar a JavaScript ese int se vuelve doble: el mismo programa cambia de garantías según el destino. Ninguno de los dos tiene fecha nativa en JSON —JSON no tiene tipo fecha en absoluto—, por eso la convención universal es escribirla como cadena ISO 8601.


JVM

Representante del núcleo: Java. Tipos declarados de antemano: el deserializador sabe a qué clase debe mapear cada campo antes de leer el primer byte.

Kotlin

fun main() {
    val (clave, valor) = readLine()!!.trim().split(Regex("\\s+"))
    println("serializado=$clave:$valor")
}

Scala

object Serializador extends App {
  val Array(clave, valor) = scala.io.StdIn.readLine().trim.split("\\s+")
  println(s"serializado=$clave:$valor")
}

Groovy

def (clave, valor) = System.in.newReader().readLine().trim().split(/\s+/)
println "serializado=$clave:$valor"

Clojure

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

(let [[clave valor] (str/split (str/trim (read-line)) #"\s+")]
  (println (str "serializado=" clave ":" valor)))

Qué reconocer: ninguno de los cuatro trae JSON en la biblioteca estándar de la plataforma —se usa Jackson, kotlinx.serialization o data.json—, pero todos heredan de Java un long de 64 bits exacto, así que el problema no es representar el entero sino que el otro extremo del cable sepa leerlo. Kotlin declara el contrato con @Serializable y genera el codificador en compilación, lo más cercano a Protobuf sin salir del lenguaje. Clojure aporta el contraste más instructivo: su formato nativo EDN sí distingue entero de real (1 frente a 1.0), sí tiene enteros de precisión arbitraria (1N) y sí tiene literal de instante (#inst "2026-07-19T00:00:00Z") — exactamente las tres cosas que JSON no puede expresar.


.NET

Representante del núcleo: C#.

F#

let partes = stdin.ReadLine().Split(' ', System.StringSplitOptions.RemoveEmptyEntries)
printfn "serializado=%s:%s" partes.[0] partes.[1]

VB.NET

Imports System

Module Serializador
    Sub Main()
        Dim p = Console.ReadLine().Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries)
        Console.WriteLine($"serializado={p(0)}:{p(1)}")
    End Sub
End Module

Qué reconocer: los tres comparten System.Text.Json en la biblioteca estándar y el mismo modelo de contrato: una clase o record cuyas propiedades definen el esquema. Ese serializador tiene una decisión famosa —escribe long como número JSON, con lo que un identificador de 64 bits llega roto a un consumidor JavaScript, y por eso muchas APIs .NET lo emiten como cadena. F# añade un problema propio: sus tipos unión discriminada y sus opciones no tienen representación natural en JSON y requieren convertidores a medida, el recordatorio de que el formato de intercambio es siempre el mínimo común denominador, no la riqueza del lenguaje.


C / llaves

Representante del núcleo: C. Aquí el tipo lo pones tú, byte a byte.

C++

#include <iostream>
#include <string>

int main() {
    std::string clave, valor;
    std::cin >> clave >> valor;
    std::cout << "serializado=" << clave << ':' << valor << '\n';
}

Objective-C

#import <Foundation/Foundation.h>

int main(void) {
    @autoreleasepool {
        NSFileHandle *in = [NSFileHandle fileHandleWithStandardInput];
        NSString *linea = [[NSString alloc] initWithData:[in availableData]
                                                encoding:NSUTF8StringEncoding];
        NSArray<NSString *> *p = [[linea stringByTrimmingCharactersInSet:
            [NSCharacterSet whitespaceAndNewlineCharacterSet]]
            componentsSeparatedByString:@" "];
        printf("serializado=%s:%s\n", [p[0] UTF8String], [p[1] UTF8String]);
    }
    return 0;
}

Qué reconocer: ni C ni C++ traen JSON en la biblioteca estándar —se recurre a nlohmann/json o RapidJSON—, lo que explica por qué en este mundo se prefieren formatos binarios con esquema compilado: Protobuf y MessagePack generan estructuras C con los tipos exactos, sin pasar por texto. Objective-C es la excepción de la familia: Foundation sí trae NSJSONSerialization, y al deserializar devuelve NSNumber para todo número, un envoltorio que borra la distinción entre entero y real hasta que consultas su objCType. Es el mismo problema de Lua o R, solo que envuelto en un objeto.


Sistemas

Representantes del núcleo: Go · Rust. Compilados y con tipos exactos, pero con biblioteca estándar moderna: el serializador viene incluido.

Zig

const std = @import("std"); // std.json forma parte de la biblioteca estándar

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");
    const clave = it.next().?;
    const valor = it.next().?;
    try std.io.getStdOut().writer().print("serializado={s}:{s}\n", .{ clave, valor });
}

Nim

import std/[strutils, json] # json está en la biblioteca estándar

let p = stdin.readLine().splitWhitespace()
echo "serializado=", p[0], ":", p[1]

D

import std.stdio, std.array, std.string, std.json; // std.json viene incluido

void main() {
    auto p = readln().strip().split();
    writefln("serializado=%s:%s", p[0], p[1]);
}

Qué reconocer: los tres sí traen JSON en la biblioteca estándar (std.json en Zig y en D, std/json en Nim), a diferencia de C, C++ o Lua. Y los tres deserializan hacia un tipo conocido: std.json.parseFromSlice(Config, ...) en Zig exige la estructura destino en tiempo de compilación, de modo que el entero de 64 bits llega como i64 y no como doble. Esa es la diferencia central de la clase: quien declara el tipo destino conserva la precisión; quien deserializa a un tipo genérico la pierde. El precio es que un campo inesperado en el JSON es un error, no un dato que se ignora.


Lógica y declarativa

Representante del núcleo: SQL. Aquí el dato ya vive en una forma estructurada; serializar es solo elegir cómo escribirla.

Prolog

:- initialization(main, main).

main :-
    read_line_to_string(user_input, Linea),
    split_string(Linea, " ", " ", [Clave, Valor]),
    format("serializado=~w:~w~n", [Clave, Valor]).

Datalog

% Datalog puro no tiene E/S ni concatenación de cadenas: el par se declara como
% hecho, y la "serialización" es la propia forma canónica de la tupla.
par("x", "5").

serializado(K, V) :- par(K, V).

Qué reconocer: Prolog es el único de los veinte cuyo formato de intercambio es su propia sintaxis: write_canonical/1 emite un término que read_term/2 vuelve a leer sin pérdida, con la distinción entre entero y real intacta y con enteros de precisión arbitraria. No necesita JSON porque el lenguaje ya es un formato de datos —aunque la biblioteca de SWI-Prolog trae library(http/json) para hablar con el resto del mundo. Datalog lleva la idea al límite: sin E/S y sin efectos, los hechos son el documento serializado. Es la misma renuncia de SQL, que no te dice cómo se codifican las filas en disco porque el formato no es asunto de la consulta.


Y de vuelta a la clase

Veinte lenguajes, un solo problema, y el mismo esqueleto en todos: leer, partir, unir con un separador. Lo que cambia es qué recuerda el formato sobre el dato que transporta —y ahí se decide si un entero de 64 bits, un decimal exacto o una fecha sobreviven al cruce. Eso es lo transferible.

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