Índice

Generics

El problema

Ya has escrito funciones como encontrar_max que trabajan con arrays de enteros. Pero ¿qué pasa si quieres la misma lógica para strings? ¿O para structs? Sin generics, necesitarías escribir una función separada para cada tipo:

fn max_int(a: int, b: int) -> int {
    if a > b { return a }
    return b
}

fn max_string(a: String, b: String) -> String {
    if a > b { return a }
    return b
}

La lógica es idéntica — solo cambian los tipos. Los generics resuelven esta duplicación.

¿Qué son los generics?

Los generics te permiten escribir código que funciona con cualquier tipo. En lugar de escribir int o String directamente, usas un parámetro de tipo — un placeholder que se llena cuando la función se usa.

fn max_val<T>(a: T, b: T) -> T {
    if a > b { return a }
    return b
}

fn main() {
    print(max_val<int>(10, 20))            // 20
    print(max_val<String>("apple", "banana"))  // banana
}

El después del nombre de la función declara un parámetro de tipo llamado T. Cuando llamas max_val(10, 20), el compilador reemplaza cada T con int y genera una versión especializada de la función.

Funciones genéricas

Más ejemplos de funciones genéricas:

fn identidad<T>(x: T) -> T {
    return x
}

fn primero_de_dos<T>(a: T, b: T) -> T {
    return a
}

fn main() {
    let n: int = identidad<int>(42)
    let s: String = identidad<String>("hola")

    print(n)    // 42
    print(s)    // hola
}

Múltiples parámetros de tipo

Las funciones pueden tener más de un parámetro de tipo:

struct Par<A, B> {
    primero: A,
    segundo: B
}

fn crear_par<A, B>(a: A, b: B) -> Par<A, B> {
    return Par<A, B> { primero: a, segundo: b }
}

fn main() {
    let p1: Par<String, int> = crear_par<String, int>("edad", 30)
    print(p1.primero)     // edad
    print(p1.segundo)     // 30

    let p2: Par<int, int> = crear_par<int, int>(10, 20)
    print(p2.primero)     // 10
    print(p2.segundo)     // 20
}

Structs genéricos

Los structs también pueden ser genéricos:

struct Caja<T> {
    valor: T
}

fn main() {
    let caja_int: Caja<int> = Caja<int> { valor: 42 }
    let caja_str: Caja<String> = Caja<String> { valor: "hola" }

    print(caja_int.valor)    // 42
    print(caja_str.valor)    // hola
}

Enums genéricos

Los enums funcionan con generics naturalmente. De hecho, ya has visto un patrón como este:

enum Opcion<T> {
    Algo(T),
    Nada
}

fn division_segura(a: int, b: int) -> Opcion<int> {
    if b == 0 {
        return Opcion.Nada
    }
    return Opcion.Algo(a / b)
}

fn main() {
    let r1: Opcion<int> = division_segura(10, 3)
    let r2: Opcion<int> = division_segura(10, 0)

    let msg1: String = match r1 {
        Opcion.Algo(v) => "Resultado: " + int_to_string(v),
        Opcion.Nada => "Sin resultado"
    }

    let msg2: String = match r2 {
        Opcion.Algo(v) => "Resultado: " + int_to_string(v),
        Opcion.Nada => "Sin resultado"
    }

    print(msg1)    // Resultado: 3
    print(msg2)    // Sin resultado
}

Con generics, Opcion funciona para cualquier tipo — no solo int.

Generics con trait bounds

Puedes combinar generics con traits para decir "esto funciona para cualquier tipo que tenga ciertas capacidades":

trait Display {
    fn to_string(self) -> String
}

struct Persona {
    nombre: String,
    edad: int
}

impl Display for Persona {
    fn to_string(self) -> String {
        return self.nombre + " (" + int_to_string(self.edad) + ")"
    }
}

fn imprimir_todos<T: Display>(items: Array) {
    var i: int = 0
    while i < items.length() {
        let item: T = items[i]
        print(item.to_string())
        i += 1
    }
}

fn main() {
    let personas: Array = [
        Persona { nombre: "Alice", edad: 30 },
        Persona { nombre: "Bob", edad: 25 }
    ]

    imprimir_todos<Persona>(personas)
}

Salida:

Alice (30)
Bob (25)

Cómo funciona: monomorfización

Cuando el compilador ve max_val(10, 20), genera una versión de max_val donde cada T se reemplaza con int. Si también llamas max_val(...), genera una segunda versión. Este proceso se llama monomorfización.

El resultado es que el código genérico se ejecuta a la misma velocidad que código escrito a mano para cada tipo — no hay costo en tiempo de ejecución.

Tu código:              max_val<T>(a: T, b: T) -> T

El compilador genera:   max_val_int(a: int, b: int) -> int
                        max_val_string(a: String, b: String) -> String

Ejemplo práctico: una pila genérica

struct Pila<T> {
    items: Array
}

fn pila_nueva<T>() -> Pila<T> {
    return Pila<T> { items: [] }
}

fn pila_push<T>(s: Pila<T>, item: T) -> Pila<T> {
    s.items.push(item)
    return s
}

fn pila_pop<T>(s: Pila<T>) -> T {
    return s.items.pop()
}

fn pila_esta_vacia<T>(s: Pila<T>) -> bool {
    return s.items.length() == 0
}

fn pila_tamano<T>(s: Pila<T>) -> int {
    return s.items.length()
}

fn main() {
    var s: Pila<int> = pila_nueva<int>()
    s = pila_push<int>(s, 10)
    s = pila_push<int>(s, 20)
    s = pila_push<int>(s, 30)

    print(pila_tamano<int>(s))    // 3

    let tope: int = pila_pop<int>(s)
    print(tope)                    // 30
}

Ejemplo práctico: un tipo Result genérico

enum Resultado<T, E> {
    Ok(T),
    Err(E)
}

fn parsear_numero(s: String) -> Resultado<int, String> {
    if s.length() == 0 {
        return Resultado.Err("String vacío")
    }
    // Verificación simple: solo dígitos
    var i: int = 0
    while i < s.length() {
        let c: int = s.charAt(i)
        if c < 48 or c > 57 {
            return Resultado.Err("No es un número: " + s)
        }
        i += 1
    }
    return Resultado.Ok(string_to_int(s))
}

fn main() {
    let r1: Resultado<int, String> = parsear_numero("42")
    let r2: Resultado<int, String> = parsear_numero("abc")

    let msg1: String = match r1 {
        Resultado.Ok(n) => "Parseado: " + int_to_string(n),
        Resultado.Err(e) => "Error: " + e
    }

    let msg2: String = match r2 {
        Resultado.Ok(n) => "Parseado: " + int_to_string(n),
        Resultado.Err(e) => "Error: " + e
    }

    print(msg1)    // Parseado: 42
    print(msg2)    // Error: No es un número: abc
}

Ejercicios

  1. Escribe una función genérica intercambiar(p: Par) -> Par que intercambie los elementos de un par.
  1. Escribe una Pila genérica con métodos push, pop, peek (ver el tope sin quitar), y tamano.
  1. Crea un enum genérico Opcion { Algo(T), Nada }. Escribe una función valor_o_defecto(opt: Opcion, defecto: T) -> T que devuelva el valor dentro de Algo o el defecto si es Nada.
  1. Escribe una función genérica buscar_primero(items: Array, predicado: Fn) -> Opcion que devuelva el primer elemento que coincida envuelto en Opcion.Algo, o Opcion.Nada.
  1. Crea un enum Resultado y úsalo para escribir obtener_seguro(arr: Array, indice: int) -> Resultado que devuelva Err para acceso fuera de límites.

Resumen

Siguiente capítulo: Networking — Servidores TCP →

← Anterior: Traits e impl blocks Siguiente: Networking — Servidores TCP →