Genéricos

En las funciones es necesario definir el tipo de cada parámetro de entrada:

fn devuelve_numero(numero: i32) -> i32 {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
}

Pero si lo que se necesita es una función que haga lo mismo para cualquier tipo de datos diferente de i32, se pueden usar genéricos. Un tipo genérico sirve para indicar que algo puede ser de diferente tipo.

También se puede decir que la función tiene un parámetro de tipo.

Los parámetros de tipo genérico se definen con los símbolos de menor y mayo que encierran el nombre que representa al parámetro de tipo. Normalmente se utiliza un carácter en mayúscula para representarlos (T, U, V, etc.), aunque no es obligatorio utilizar solo una letra.

La función anterior se puede convertir en genérica así:

fn devuelve_numero<T>(numero: T) -> T {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
    let numero_decimal = devuelve_numero(5.4);
}

Se observa que el parámetro de tipo genérico <T> va después del nombre de la función. Puede resultar más sencillo de comprender si se sustituye la T por un nombre más descriptivo como MiTipo:

fn devuelve_numero<MiTipo>(numero: MiTipo) -> MiTipo {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
    let numero_decimal = devuelve_numero(5.4);
}

Así que la parte de después del nombre de la función es lo que hace que la función tenga un parámetro de tipo genérico que el compilador "sustituye" por el tipo concreto generyo un función diferente para cada tipo que se use en el código.

Además, como se ha visto, en Rust existen algunos tipos que implementan determinados rasgos, como son Copy, Clone, Display, Debug y otros. Si un tipo es Debug, puede usar {:?} para imprimirlo. Esto genera un posible problema si en la función genérica se quisiera imprimir:

fn imprime_numero<T>(numero: T) {
    println!("Aquí está tu numero: {:?}", numero); // ⚠️
}

fn main() {
    imprime_numero(5);
}

imprime_numero necesita que el parámetro de tipo genérico implemente Debug para poder utilizar {:?}. Con la definición actual de la función, no es posible conocer si realmente T implementa o no Debug. Por lo tanto, el compilador emite error:

error[E0277]: `T` doesn't implement `Debug`
 --> src/main.rs:2:43
  |
2 |     println!("Aquí está tu numero: {:?}", numero); // ⚠️
  |                                           ^^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`

La solución es que se le indique a Rust que esta función genérica solo puede utilizar parámetros de tipo genérico que implementen Debug:

use std::fmt::Debug;
// Debug se encuentra en el módulo std::fmt.
// Ahora es posible usar solo Debug.

fn imprime_numero<T: Debug>(numero: T) { // Esto es lo importante <T: Debug>
    println!("Aquí está tu numero: {:?}", numero);
}

fn main() {
    imprime_numero(5);
    imprime_numero(5.4);
}

Ahora el compilador conoce que los tipos que van a usar esta función tienen definido Debug. Como tanto i32, como f64 tienen Debug definido, el código funciona.

Si se amplia el ejemplo, creyo una estructura que implemente Debug mediante #[derive(Debug)], se observa que la función también es válida:

use std::fmt::Debug;

#[derive(Debug)]
struct Animal {
    nombre: String,
    edad: u8,
}

fn imprime_elemento<T: Debug>(item: T) {
    println!("Aquí está tu elemento: {:?}", item);
}

fn main() {
    let charlie = Animal {
        nombre: "Charlie".to_string(),
        edad: 1,
    };

    let numero = 55;

    imprime_elemento(charlie);
    imprime_elemento(numero);
}

Que imprime:

Aquí está tu elemento: Animal { nombre: "Charlie", edad: 1 }
Aquí está tu elemento: 55

En ocasiones se necesita más de un tipo genérico para definir una función genérica. Para ello, se puede escribir cada tipo y pensar cómo se utiliza cada uno de ellos. En el siguiente ejemplo, se muestran dos tipos genéricos. Se desea imprimir un texto con el tipo T con {}, por lo que este tipo deberá implementar Display.

El segundo tipo es U y las dos variables num_1 y num_2 son de este tipo (U es algún tipo de número). Se trata de compararlas entre ellas, por lo que estos tipos deben implementar el rasgo PartialOrd, que es el que permite que los elementos de un tipo puedan usar <, > y ==, entre otros. También se quiere imprimir los números, por lo que el tipo U requiere Display.

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compara_e_imprime<T: Display, U: Display + PartialOrd>(texto: T, num_1: U, num_2: U) {
    println!("{} ¿Es {} mayor que {}? {}", texto, num_1, num_2, num_1 > num_2);
}

fn main() {
    compara_e_imprime("¡¡Escucha!!", 9, 8);
}

El resultado es ¡¡Escucha!! ¿Es 9 mayor que 8? true.

La declaración de la función fn compara_e_imprime<T: Display, U: Display + PartialOrd>(texto: T, num_1: U, num_2: U) significa:

  • El nombre de la función es compara_e_imprime.
  • El primer tipo es T y es genérico. Y debe poder usar {} para imprimir.
  • El segundo tipo es U y es genérico. Debe ser un tipo que pueda usar {} y también se tienen que poder comparar sus elementos con <, > y ==.

De este modo, se pueden pasar parámetros de distintos tipos a la función compara_e_imprime. Por ejemplo, el parámetro texto puede ser String, &str, i32 o cualquier otro que se pueda imprimir.

Para que las funciones genéricas sean más fácil de leer, también existe la siguiente sintaxis equivalente al código anterior. Se utiliza where para simplificar la declaración de la función.

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compara_e_imprime<T, U>(texto: T, num_1: U, num_2: U)
where
    T: Display,
    U: Display + PartialOrd,
{
    println!("{} ¿Es {} mayor que {}? {}", texto, num_1, num_2, num_1 > num_2);
}

fn main() {
    compara_e_imprime("¡¡Escucha!!", 9, 8);
}

Cuyo se tienen muchos tipos genéricos con rasgos concretos de implementación, resulta más legible esta sintaxis con where.

Hay que destacar también que:

  • Si se tiene un parámetro de tipo T y otro parámetro de tipo T, ambos tienen que ser del mismo tipo.
  • Si se tiene un parámetro de tipo T y otro parámetro de tipo U, pueden ser de diferente tipo, pero también pueden ser del mismo tipo.

Por ejemplo:

use std::fmt::Display;

fn diga_dos<T: Display, U: Display>(statement_1: T, statement_2: U) {
// El tipo T necesita Display, el tipo U necesita Display
    println!("Tengo dos cosas que decir: {} y {}", statement_1, statement_2);
}

fn main() {

    diga_dos("¡Hola!", String::from("Odio la arena.")); // Tipo T es &str, pero el tipo U es String.
    diga_dos(String::from("¿Dónde está Padme?"), String::from("¿Está bien?")); // Ambos tipos son String.
}

Esto imprime:

Tengo dos cosas que decir: ¡Hola! y Odio la arena.
Tengo dos cosas que decir: ¿Dónde está Padme? y ¿Está bien?