Title: Crear función que retorne un array con N valores por defecto en Rust - Const Generics

Date: 2021-05-26
Content:

Hace unos días una persona en un grupo de Telegram preguntó el porqué solo se podían hacer arrays con valores por defecto de máximo 32 de longitud y la respuesta a ese predicamento realmente es un tanto absurda pero desde luego es muy interesante y radica en que... *redoble de tambores* ... las funciones para generar los arrays se debían escribir a mano, y de manera arbitraria decidieron que iban a escribir funciones para generar arrays de hasta 32 elementos.
Ósea que tienen escritas las funciones mas o menos así [1]:

impl<T> Default for [T; 0] where T: Default {
                        ^
                        ╚═══════ Array de longitud 0
    fn default() -> Self {  
        []
        ^
        ╚═══════ Retorna un array vacío
    }
}

impl<T> Default for [T; 1] where T: Default {
                        ^
                        ╚═══════ Array de longitud 1
    fn default() -> Self {
        [T::default()]
         ^ ^ ^ ^ ^ ^
         ╚═╩═╩═╩═╩═╩═════ Retorna un array con un elemento
    }
}

...
impl<T> Default for [T; 32] where T: Default {
                        ^
                        ╚═══════ Array de longitud 32
    fn default() -> Self {
        [T::default(), T::default(), T::default(), T::default(), ...]
         ^ ^ ^ ^ ^ ^
         ╚═╩═╩═╩═╩═╩═════ Retorna un array con 32 elementos
                          ESCRITOS A MANO
    }
}

y solo había de dos sopas, o ponían a una persona becada a implementar manualmente el resto de casos desde 33 hasta el máximo de usize o encontraban una forma de resolverlo de una manera inteligente, y el equipo de Rust se puso a hacer lo 2do y agregaron en esta nueva release una cosa maravillosa llamada const generics.
Los const generics, como su nombre lo deja entrever, es la posibilidad de utilizar valores constantes como datos genéricos dentro de una función y poder colocar esos constantes genéricos en el lugar en el que debe ir un valor constante.
Explicado de esta manera puede quedar mas o menos confuso, pero veamos un ejemplo sin const generics y uno con para ver la enorme maravilla que esto supone.

Entendiendo los Const Generics

El ejemplo será el siguiente: Se tiene que hacer una función que retorne un array vacío de tipos unidad (suena feo en español "unit type", ya me disculparán), la longitud de ese array puede ser cualquier valor de 0 hasta el máximo de usize.

Sin Const Generics

trait Foo {
    fn zeros() -> Self;
}
impl Foo for [(); 0] {
    fn zeros() -> Self { [] }
}
impl Foo for [(); 1] {
    fn zeros() -> Self { [()] }
}
... // Aquí irían las implementaciones para cada longitud de array
impl Foo for [(); usize::MAX] {
    fn zeros() -> Self { [(); usize::MAX] }
}

playground
Como se puede apreciar, se debe crear un trait que contenga la función que nos interesa que haga la acción y que retorne un Self y después implementar cada caso a mano y después poderla mandarla a llamar así:

let x: [(); 10] = Foo::zeros();
println!("{:?}", x);
// [(), (), (), (), (), (), (), (), (), ()]

Como pueden intuir este método deja muchas cosas que desear, principalmente lo de estar implementando a mano cada uno de los casos para los arrays... ahora les mostraré lo sencillo que es implementarlo gracias a los Const Generics.

Con Const Generics

fn zeros<const N: usize>() -> [(); N] {
         ^ ^ ^ ^  ^ ^ ^            ^
         ║ ║ ║ ║  ║ ║ ║            ╚═ Se puede colocar el const generic en el lugar de la longitud
         ║ ║ ║ ║  ║ ║ ║               (ahí solo pueden ir valores constantes).
         ║ ║ ║ ║  ╚═╩═╩═ El tipo del const generic.
         ║ ║ ║ ╚═ Nombre del const generic.
         ╚═╩═╩═ Palabra reservada para indicar que será un valor const generic.
    [(); N]
         ^
         ╚═ Se crea el array de valores unidad y se coloca el const generic en el tamaño del array.
}

playground
Como se ve, el ejemplo con const generic queda mucho mas limpio, se ve de manera clara lo que hace y no se necesita un trait que contenta a la función, puede estar suelta y ser independiente. Y para utilizarla es idéntico a la versión anterior:

let x: [(); 10] = zeros();
println!("{:?}", x);
// [(), (), (), (), (), (), (), (), (), ()]

Generando arrays con valores por defecto

Tomando en cuenta lo aprendido anteriormente podríamos pensar que generar un array con valores por defecto puede ser tan simple como recibir un genérico que tenga implementado el trait Default y después generar el array tomando su valor por defecto y listo, pero veamos que no es tan sencillo:

fn fill_default<T: Default, const N: usize>() -> [T; N] {
    [T::default(); N]
}

playground (no compila)
Ya que si es implementado así y lo si intentamos compilar nos saldrá un error parecido a este:

error[E0277]: the trait bound `T: Copy` is not satisfied
 --> src/main.rs:2:5
  |
2 |     [T::default(); N]
  |     ^^^^^^^^^^^^^^^^^ the trait `Copy` is not implemented for `T`
  |
  = note: the `Copy` trait is required because the repeated element will be copied
help: consider further restricting this bound
  |
1 | fn fill_default<T: Default + std::marker::Copy, const N: usize>() -> [T; N] {
  |                            ^^^^^^^^^^^^^^^^^^^

Debido a que la manera de generar arrays con la sintaxis [Valor; longitud] necesita que el valor implemente el trait Copy, para que se genere todo de golpe, y el reto que se propone en esta publicación es necesitar unicamente que T implemente a Default y nada más, así que tendremos que encontrar otra forma de realizarlo.
Luego de darle muchas vueltas decidí atacar el problema de la siguiente manera:

  1. Hacer un iterador para que cada valor se vaya generando uno a uno y no sea necesario el trait Copy.
  2. Generar un Vec<T> gracias a ese iterador para después:
  3. Utilizando el trait TryInto convertir el Vec<T> a un array.

Y el resultado fue el siguiente:

use std::convert::TryInto;
                  ^ ^ ^ ^
                  ╚═╩═╩═╩═ Nos traemos el trait TryInto

fn fill_default<T: Default, const N: usize>() -> [T; N] {
    std::iter::repeat_with(|| T::default())
        .take(N)             <═╦═ iter
        .collect::<Vec<T>>() <═╬═ Vec<T>
        .try_into()          <═╬═ Result<[T; N], _>
        .map_err(|_| ())     <═╬═ Result<[T; N], ()>
        .unwrap()            <═╩═ [T; N]
}

Playground
Y ¡¡listo!! despues de pasear un poco los datos (que sí iter, luego Vec, luego Result, etc...) ya tenemos nuestra función que genera un array de tamaño N con valores por defecto sin depender de otro trait como Copy o Debug.

Curiosidades encontradas

Aunque el problema ya está resuelto no quiero dejar pasar un par de curiosidades con respecto a este problema ya que, como pueden ver en el pedazo de código anterior nos encontramos con una linea particularmente curiosa... y sí, me refiero a esta:

.map_err(|_| ())

Ya que en un principio podría parecer que no es necesaria por que solo está generando un Result de otro Result, pero si nos deshacemos de ella nos encontraremos con este curioso error:

error[E0277]: `T` doesn't implement `Debug`
 --> src/main.rs:8:10
  |
8 |         .unwrap()
  |          ^^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |
  = note: required because of the requirements on the impl of `Debug` for `Vec<T>`
help: consider further restricting this bound
  |
3 | fn fill_default<T: Default + std::fmt::Debug, const N: usize>() -> [T; N] {
  |                            ^^^^^^^^^^^^^^^^^

Así es, el compilar nos está pidiendo que T implemente también el trait Debug lo cual es bastante peculiar (o me lo pareció en un principio) pero después de analizarlo me hizo todo el sentido del mundo ya que, si llegase a fallar la conversión de Vec<T> a [T; N] el compilador deberá saber como mostrar a T y eso se logra con el trait Debug. Por lo tanto, la linea .map_err(|_| ()) nos está ayudando a disfrazar el error reemplazandolo por una unidad (que si tiene implementado el trait Debug).
Otra de las curiosidades es que, sí estás utlizando la versión noctura del compilador, podremos utilizar una función que aún no es estable del todo llamada "array map" y con eso sí que reducimos bastante el código sin comprometer la legibilidad del mismo ya que bastará con generar un array del tamaño que queramos y al que después le mapearemos la función que le colocará los valores deseados, algo mas o menos así:

#![feature(array_map)]

fn fill_default<T: Default, const N: usize>() -> [T; N] {
    [0; N].map(|_| T::default())
}

Playground (compila con la versión nocturna)
Y hace basicamente lo mismo que se trató en la parte principal de este texto pero de una manera directa y sin tanta parafernalia.

Esta publicación no sería posible sin la inestimable ayuda de Categulario.

Fuentes

  1. https://doc.rust-lang.org/std/default/trait.Default.html
  2. https://github.com/rust-lang/rust/issues/75243
  3. https://www.joshmcguigan.com/blog/array-initialization-rust/
  4. https://stackoverflow.com/questions/67963788/why-tryinto-needs-copy-trait-in-t/67964077?noredirect=1#comment120159571_67964077
  5. (manera utilizando unsafe de hacerlo, es una forma fea y por eso no la mostré arriba) https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=48f7b644bb823912aa4a904e3189a0bb