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

⬅️ Volver a la clase 051 · 🌐 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 —sumar un número consigo mismo y concatenarlo consigo mismo— 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
5 suma=10 texto=55
3 suma=6 texto=33
12 suma=24 texto=1212

Este es el problema que mejor separa fuerte de débil: el mismo valor tiene que comportarse como número en un sitio y como texto en el otro. Lo interesante no es el resultado, sino cuánto trabajo pide cada lenguaje para conseguir las dos cosas.


Scripting dinámico

Representantes del núcleo: Python · PHP. Aquí viven los dos extremos: Python es dinámico pero fuerte (1 + "1" es un error), y PHP es dinámico y débil (1 + "1" vale 2).

Ruby

n = STDIN.gets.to_i
# Ruby es fuerte: `n + n.to_s` lanzaría TypeError. La conversión se escribe.
puts "suma=#{n + n} texto=#{n.to_s + n.to_s}"

Perl

my $n = <STDIN>;
chomp $n;
# El caso extremo de tipado débil: el escalar no tiene tipo, lo elige el OPERADOR.
# `+` lo lee como número; `.` lo lee como texto. Nunca hay que convertir nada.
printf "suma=%d texto=%s\n", $n + $n, $n . $n;

Lua

local n = io.read("n")
-- Lua coacciona en las dos direcciones: número en `..` y cadena en aritmética.
print(string.format("suma=%d texto=%s", n + n, n .. n))

Tcl

gets stdin n
# En Tcl *todo* es cadena: concatenar es simple yuxtaposición y para sumar
# hay que pedir explícitamente una evaluación aritmética con `expr`.
puts "suma=[expr {$n + $n}] texto=$n$n"

R

n <- as.integer(readLines("stdin", n = 1))
cat(sprintf("suma=%d texto=%s\n", n + n, paste0(n, n)))

Qué reconocer: los cinco son dinámicos, pero se reparten en toda la escala de fuerza. Ruby y R son fuertes: exigen to_s / paste0 para tratar el número como texto. Perl, Lua y Tcl son débiles y lo son de tres maneras distintas —Perl deja que el operador decida, Lua coacciona en ambos sentidos, Tcl parte de que todo es cadena y sube a número solo bajo expr—. Dinámico y débil no son sinónimos, y esta familia lo demuestra ella sola.


JavaScript / web

Representantes del núcleo: JavaScript · TypeScript. La familia del + sobrecargado: suma si los dos lados son números, concatena si alguno es texto.

Dart

import 'dart:io';

void main() {
  final n = int.parse(stdin.readLineSync()!.trim());
  // Dart corrigió el `+` de JavaScript: `n + '$n'` no compila.
  print('suma=${n + n} texto=$n$n');
}

ActionScript 3

package {
    // ActionScript corre en el reproductor Flash, sin stdin: se ilustra el cálculo.
    // Conserva el `+` ambiguo de ECMAScript: con un operando String, concatena.
    public class Fuerza {
        public static function linea(n:int):String {
            return "suma=" + (n + n) + " texto=" + (String(n) + String(n));
        }
    }
}

Qué reconocer: ActionScript 3 hereda el + ambiguo de ECMAScript, con los mismos paréntesis defensivos alrededor de n + n que hacen falta en JavaScript. Dart rompió con esa herencia: aunque la sintaxis se parece, su + es fuerte y solo suma números. TypeScript se queda en medio —marca el error al compilar, pero el JavaScript generado sigue concatenando en tiempo de ejecución—.


JVM

Representante del núcleo: Java. Una familia fuerte, con una única grieta famosa: el + de String.

Kotlin

fun main() {
    val n = readLine()!!.trim().toInt()
    // `n + "$n"` no compila: Int no tiene un `plus(String)`.
    println("suma=${n + n} texto=$n$n")
}

Scala

object Fuerza extends App {
  val n = scala.io.StdIn.readLine().trim.toInt
  println(s"suma=${n + n} texto=$n$n")
}

Groovy

def n = System.in.newReader().readLine().trim().toInteger()
// Groovy relaja la regla: `'a' + 1` concatena y `1 + '1'` falla. La asimetría es suya.
println "suma=${n + n} texto=${n.toString() + n}"

Clojure

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

(let [n (Long/parseLong (str/trim (read-line)))]
  ;; `(+ 1 "1")` lanza ClassCastException: fuerte, pero comprobado en ejecución.
  (println (str "suma=" (+ n n) " texto=" n n)))

Qué reconocer: los cuatro son fuertes, y se nota en que ninguno suma un número con una cadena. Pero cambia cuándo te lo dicen: Kotlin y Scala en el compilador, Clojure al ejecutar. Clojure además evita el problema de raíz separando los operadores —+ solo suma, str solo concatena—, la misma solución que adoptaron los lenguajes de sistemas. Groovy es la oveja negra: reintroduce a propósito parte de la debilidad para parecerse a un lenguaje de scripting.


.NET

Representante del núcleo: C#.

F#

let n = int (stdin.ReadLine().Trim())
// F# es el más fuerte del atlas: ni siquiera `1 + 1.0` compila, y `+` con
// una cadena tampoco. Todo cruce de tipos pasa por una conversión escrita.
printfn "suma=%d texto=%s" (n + n) (string n + string n)

VB.NET

Module Fuerza
    Sub Main()
        ' Con Option Strict Off, VB.NET convierte solo: "5" + 5 da 10.
        ' Por eso el operador de concatenación canónico es `&`, no `+`:
        ' `&` siempre significa texto y no deja lugar a la ambigüedad.
        Dim n As Integer = Integer.Parse(Console.ReadLine().Trim())
        Console.WriteLine("suma=" & (n + n) & " texto=" & n.ToString() & n.ToString())
    End Sub
End Module

Qué reconocer: el CLR es fuerte por debajo, pero cada lenguaje decide cuánta debilidad ofrece encima. F# no ofrece ninguna. VB.NET arrastra la de sus antepasados BASIC y por eso inventó un operador aparte, &, para que concatenar nunca se confunda con sumar. C# se queda en el medio, con + sobrecargado para string igual que Java.


C / llaves

Representante del núcleo: C. Estático y, en teoría, tipado; en la práctica, con un agujero enorme llamado cast.

C++

#include <iostream>
#include <string>

int main() {
    int n;
    std::cin >> n;
    // `+` sobre std::string concatena, pero por SOBRECARGA de operador,
    // no por coerción: los tipos siguen comprobándose uno a uno.
    const std::string s = std::to_string(n);
    std::cout << "suma=" << n + n << " texto=" << s + s << '\n';
}

Objective-C

#import <Foundation/Foundation.h>

int main(void) {
    @autoreleasepool {
        int n;
        scanf("%d", &n);
        NSString *s = [NSString stringWithFormat:@"%d", n];
        NSString *texto = [s stringByAppendingString:s];
        printf("suma=%d texto=%s\n", n + n, [texto UTF8String]);
    }
    return 0;
}

Qué reconocer: en C concatenar dos números sería copiar bytes a mano, y por eso los dos primos resuelven la mitad textual con un tipo aparte —std::string, NSString—. C++ consigue escribir s + s sin debilitar nada: es una sobrecarga resuelta al compilar, no una conversión implícita. Objective-C ni siquiera intenta sobrecargar + y pide el mensaje stringByAppendingString:, mucho más verboso pero imposible de confundir con una suma.


Sistemas

Representantes del núcleo: Go · Rust. La familia que decidió que la mejor forma de evitar la ambigüedad es no reutilizar el mismo símbolo.

Zig

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(i64, std.mem.trim(u8, linea, " \r"), 10);
    // Zig no tiene sobrecarga de operadores: `+` solo suma números.
    // El texto se construye siempre por formateo.
    try std.io.getStdOut().writer().print("suma={d} texto={d}{d}\n", .{ n + n, n, n });
}

Nim

import std/strutils

let n = stdin.readLine().strip().parseInt()
# `+` suma, `&` concatena, `$` convierte a texto. Tres operadores, cero ambigüedad.
echo "suma=", n + n, " texto=", $n & $n

D

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

void main() {
    int n = readln().strip().to!int;
    string s = n.to!string;
    // D reservó `~` para concatenar precisamente para que `+` no tuviera que decidir.
    writefln("suma=%d texto=%s", n + n, s ~ s);
}

Qué reconocer: los tres separan los operadores —& en Nim, ~ en D, solo formateo en Zig— y exigen una conversión escrita ($, to!string) para pasar de número a texto. Es la misma postura que Rust con to_string() y que Go con strconv. Cuando un lenguaje quiere ser fuerte de verdad, lo primero que hace es dejar de reutilizar el signo +.


Lógica y declarativa

Representante del núcleo: SQL. SQL es notoriamente débil —compara y suma entre tipos distintos con reglas de conversión propias de cada motor—, y sus primos van en la dirección contraria.

Prolog

:- initialization(main, main).

main :-
    read_line_to_string(user_input, Linea),
    number_string(N, Linea),
    % `is` evalúa aritmética; nunca concatena. Y el texto se construye
    % con predicados aparte: los dos mundos jamás se tocan.
    Suma is N + N,
    format(string(S), "~w", [N]),
    string_concat(S, S, Texto),
    format("suma=~w texto=~w~n", [Suma, Texto]).

Datalog

% Datalog puro no tiene E/S ni concatenación de cadenas: solo hechos y reglas
% con aritmética sobre los términos. Se muestra la mitad numérica del contrato.
entrada(5).

suma(S) :- entrada(N), S = N + N.

Qué reconocer: en Prolog el término 5 y el término "5" son cosas distintas que nunca se convierten solas: hay un predicado para cada travesía (number_string, string_concat). Eso lo hace fuerte aunque no declare tipos, que es justo la combinación que Python también encarna. Y es el contraste exacto con SQL, donde '5' + 5 puede dar 10, un error o '55' según el motor.


Y de vuelta a la clase

Veinte lenguajes, un solo problema, y una pregunta que los ordena de punta a punta: ¿el lenguaje convierte por ti o te obliga a escribir la conversión? Perl y Tcl están en un extremo, F# y Zig en el otro, y en medio cabe casi todo lo que usarás en tu vida profesional. Eso es lo transferible.

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