Clase 078 — Genéricos y polimorfismo paramétrico

Parte 5 — Funciones y modularidad · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

Escribir una función una sola vez y que sirva para muchos tipos sin perder la comprobación del compilador. La función mayor(a, b) que devuelve el mayor de dos valores no debería reescribirse para enteros, luego para reales, luego para texto: la lógica —«compara y devuelve el más grande»— es idéntica; lo único que cambia es el tipo de lo que entra. El polimorfismo paramétrico es el mecanismo que captura exactamente esa idea: parametrizar el código por un tipo como se parametriza por un valor. Donde una función normal deja un hueco para un número (a), una función genérica deja además un hueco para un tipo (T), y ese hueco se rellena en el momento de la llamada.

Robert Sebesta, en Concepts of Programming Languages, distingue el polimorfismo ad hoc (la sobrecarga, donde cada tipo tiene su propia implementación) del polimorfismo paramétrico, donde un solo cuerpo de código opera sobre un rango de tipos gracias a un parámetro de tipo. La diferencia es profunda: en la sobrecarga escribes N funciones que casualmente comparten nombre; en el paramétrico escribes una función que el compilador especializa. Benjamin Pierce lo lleva al terreno teórico en Types and Programming Languages: una función genuinamente paramétrica no puede inspeccionar el tipo que recibe —solo puede hacer con él lo que su firma promete—, y de esa restricción nace la parametricidad, la propiedad de que el comportamiento es uniforme para todos los tipos.

El objetivo hondo de la clase es ver que «una función para todos los tipos» no significa «renunciar a los tipos». Al contrario: en los lenguajes estáticos, mayor<T> sigue exigiendo que T sea comparable, y esa restricción se verifica antes de ejecutar. Genéricos bien hechos son reutilización con seguridad, no a costa de ella.

🧩 Situación

Imagina una biblioteca de utilidades donde alguien necesitó el máximo de dos enteros y escribió maxInt. Meses después, otro necesitó el de dos double y copió la función cambiando el tipo: nació maxDouble. Luego llegó maxString, maxLong, maxFloat... Un día se descubre que todas tenían el mismo error sutil en el caso de empate, y hay que corregirlo en seis sitios idénticos. Esa duplicación es la que Robert Martin llama deuda en Clean Code: el mismo cambio repetido en muchos lugares es un imán de bugs. El polimorfismo paramétrico borra el problema de raíz: mayor<T: Comparable>(a, b) se escribe una vez, sirve para todo tipo que sepa compararse, y el compilador rechaza en el sitio de la llamada cualquier tipo que no cumpla el contrato. Una corrección, un cuerpo, cero copias.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
3 7 max=7
9 2 max=9
5 5 max=5

📖 Definiciones y características

📐 Algoritmo (pseudocódigo neutral)

FUNCION max<T comparable>(a,b): DEVOLVER a SI a>b SINO b

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys


def mayor(a, b):
    return a if a > b else b


a, b = map(int, sys.stdin.readline().split())
print(f"max={mayor(a, b)}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

// JS es dinámico: la función ya sirve para cualquier tipo comparable.
function mayor(a, b) {
  return a > b ? a : b;
}

const [a, b] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`max=${mayor(a, b)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

function mayor<T>(a: T, b: T): T {
  return a > b ? a : b;
}

const [a, b]: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`max=${mayor(a, b)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static <T extends Comparable<T>> T mayor(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] p = br.readLine().trim().split("\\s+");
        int a = Integer.parseInt(p[0]);
        int b = Integer.parseInt(p[1]);
        System.out.println("max=" + mayor(a, b));
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

T Mayor<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b;

string[] p = Console.In.ReadToEnd()
    .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
int a = int.Parse(p[0]);
int b = int.Parse(p[1]);
Console.WriteLine($"max={Mayor(a, b)}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "cmp"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func mayor[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    f := strings.Fields(line)
    a, _ := strconv.Atoi(f[0])
    b, _ := strconv.Atoi(f[1])
    fmt.Printf("max=%d\n", mayor(a, b))
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn mayor<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let v: Vec<i64> = s.split_whitespace().map(|x| x.parse().unwrap()).collect();
    println!("max={}", mayor(v[0], v[1]));
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

/* C no tiene genéricos: se escribe una función por tipo (o macros). */
long mayor(long a, long b) {
    return a > b ? a : b;
}

int main(void) {
    long a, b;
    if (scanf("%ld %ld", &a, &b) != 2) return 1;
    printf("max=%ld\n", mayor(a, b));
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL: max() es polimórfico incorporado.
WITH pares(a, b) AS (VALUES (3, 7), (9, 2), (5, 5))
SELECT printf('max=%d', max(a, b)) AS resultado FROM pares;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
// PHP es dinámico: la función sirve para cualquier tipo comparable.
function mayor($a, $b) {
    return $a > $b ? $a : $b;
}

[$a, $b] = preg_split('/\s+/', trim(fgets(STDIN)));
echo "max=" . mayor((int) $a, (int) $b) . "\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔬 Ejemplo trabajado — del stdin a la salida

Sigamos el primer caso de casos.json (stdin = "3 7", esperado = "max=7") por tres lenguajes que resuelven la genericidad con filosofías opuestas.

Python. No hay parámetro de tipo por ningún lado: def mayor(a, b) acepta lo que sea. La línea a, b = map(int, sys.stdin.readline().split()) produce a=3, b=7, y mayor(3, 7) evalúa 3 if 3 > 7 else 7. Como 3 > 7 es falso, devuelve 7, y print(f"max={7}") emite max=7. Python es genérico «gratis» por duck typing: la función corre con cualquier cosa que soporte >. Pero esa libertad no es polimorfismo paramétrico verificado: si le pasaras un entero y un texto, mayor(3, "7") no fallaría al definir la función, sino que reventaría en tiempo de ejecución con un TypeError. El contrato «T debe ser comparable» existe solo como esperanza, no como comprobación.

Java. Aquí T es explícito: la firma <T extends Comparable<T>> T mayor(T a, T b) declara el parámetro de tipo y su restricción. El cuerpo no usa > (los objetos de Java no lo soportan) sino a.compareTo(b) > 0, que es justo lo que la restricción Comparable<T> garantiza que existe. Con a=3, b=7, la llamada mayor(a, b) infiere T = Integer (los int se autoboxean), evalúa Integer.valueOf(3).compareTo(7) que da negativo, y devuelve 7. El detalle histórico clave: por el borrado de tipos (type erasure), en el bytecode compilado T ya no existe —la función opera sobre Comparable y hace un cast a Object—, de modo que en tiempo de ejecución Java no sabe que aquello «era un genérico de Integer». Bloch dedica varios ítems de Effective Java a las consecuencias de este borrado.

Go. Los genéricos llegaron a Go en la versión 1.18, y aquí se ven en func mayor[T cmp.Ordered](a, b T) T. La restricción cmp.Ordered es una constraint de la biblioteca estándar que agrupa todos los tipos ordenables (enteros, reales, cadenas), y precisamente por eso el cuerpo puede usar if a > b directamente. Con a=3, b=7, T se infiere como int, 3 > 7 es falso, la función devuelve 7, y fmt.Printf("max=%d\n", 7) imprime max=7. El tercer caso, 5 5, es la prueba del empate: 5 > 5 es falso, así que devuelve el segundo 5 —igual en los tres lenguajes—, produciendo max=5. Tres caminos hacia la misma salida: uno sin tipos (Python), uno con tipos borrados en runtime (Java), uno con tipos y restricción explícita (Go).

🔬 Comparación

Lenguaje Cómo expresa la genericidad
Python Sin parámetro de tipo; genérico por duck typing, verificado solo en ejecución.
JavaScript Igual que Python: dinámico, sin genéricos; la función sirve para todo lo comparable.
TypeScript <T> real en el fuente, pero borrado al compilar a JS: no queda tipo en runtime.
Java <T extends Comparable<T>>; genéricos con type erasure, sin info de tipo en runtime.
C# <T> where T : IComparable<T>; genéricos reificados, T disponible en tiempo de ejecución.
Go [T cmp.Ordered]; parámetros de tipo y constraints desde Go 1.18.
Rust <T: PartialOrd>; trait bounds y monomorfización (una copia por tipo usado).
C No tiene genéricos reales: una función por tipo, macros, void* o _Generic de C11.
SQL max() es una función polimórfica incorporada; no defines el genérico, ya viene dado.
PHP Dinámico como Python/JS; sin parámetros de tipo, comparación resuelta en ejecución.

La síntesis la ilumina la distinción de Sebesta: los estáticos (Java, C#, Go, Rust, TS) comprueban la restricción de T antes de ejecutar, aunque luego difieran en si el tipo sobrevive a runtime (borrado en Java/TS, reificado en C#, monomorfizado en Rust); los dinámicos (Python, JS, PHP) ofrecen la misma reutilización pero pagan la comprobación en tiempo de ejecución. Y C, en el otro extremo, recuerda que el polimorfismo paramétrico es un lujo del sistema de tipos: sin él, la única salida honesta es escribir una función por tipo. Pierce diría que solo los primeros garantizan parametricidad real: que el comportamiento es demostrablemente uniforme para todos los tipos que cumplen el contrato.

🧬 El concepto en la familia

En Kotlin la firma sería fun <T : Comparable<T>> mayor(a: T, b: T), casi calcada a Java pero sin el extends. En Haskell, el sistema de tipos hace la restricción explícita y elegante: mayor :: Ord a => a -> a -> a, donde Ord a => es literalmente «para todo a que pertenezca a la clase de los ordenables»; es la misma idea que T: PartialOrd de Rust o cmp.Ordered de Go. Swift usa func mayor<T: Comparable>(_ a: T, _ b: T) -> T, con la restricción tras los dos puntos como Rust. Reconocer el patrón —dónde va la variable de tipo, cómo se escribe su restricción— permite leer una firma genérica en cualquier lenguaje de la familia en segundos.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 078

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 077 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 079 ⏭️