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, 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, 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
- Escribe una
Pilagenérica con métodospush,pop,peek(ver el tope sin quitar), ytamano.
- Crea un enum genérico
Opcion. Escribe una función{ Algo(T), Nada } valor_o_defectoque devuelva el valor dentro de(opt: Opcion , defecto: T) -> T Algoo el defecto si esNada.
- Escribe una función genérica
buscar_primeroque devuelva el primer elemento que coincida envuelto en(items: Array, predicado: Fn) -> Opcion Opcion.Algo, oOpcion.Nada.
- Crea un enum
Resultadoy úsalo para escribirobtener_seguro(arr: Array, indice: int) -> Resultadoque devuelvaErrpara acceso fuera de límites.
Resumen
- Los generics te permiten escribir código que funciona con cualquier tipo:
fn f.(x: T) -> T - Los parámetros de tipo van entre corchetes angulares:
,,. - Los structs y enums pueden ser genéricos:
struct Caja,enum Opcion. - Los trait bounds restringen los generics:
significa "T debe implementar Display." - Nyx usa monomorfización — el código genérico se compila a versiones especializadas sin costo en tiempo de ejecución.
- Patrones genéricos comunes:
Optionpara valores opcionales,Resultpara manejo de errores,Pairpara tuplas.
Siguiente capítulo: Networking — Servidores TCP →