/* CódigoComentado */

=> Volver al inicio

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

Publicado el: 2021-05-26

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]:

=> 1

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

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

...
impl 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] }
}
=> https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=11044917040771b0957eb121aa027ab1 

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() -> [(); 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.

}

=> https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=344dc12610471a108c3ba2a09fd1b400

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; N] {
    [T::default(); N]
}
=> https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=083449942941218e8f330b88371a99da 

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:

* Hacer un iterador para que cada valor se vaya generando uno a uno y no sea necesario el trait `Copy`.
* Generar un `Vec` gracias a ese iterador para después:
* Utilizando el trait `TryInto` convertir el `Vec` 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]

}

=> https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c771cf7f1b8f81acdadf4589f837eec9

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`
help: consider further restricting this bound
  |
3 | fn fill_default() -> [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).

=> unidad

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í:

=> array map

#![feature(array_map)]

fn fill_default() -> [T; N] {
    [0; N].map(|_| T::default())
}
Proxy Information
Original URL
gemini://blog.kirbylife.dev/post/crear-funcion-que-retorne-un-array-con-n-valores-por-defecto-en-rust-const-generics-7
Status Code
Success (20)
Meta
text/gemini; charset=utf-8; lang=en
Capsule Response Time
547.728617 milliseconds
Gemini-to-HTML Time
1.347197 milliseconds

This content has been proxied by September (3851b).