¿Cómo necesito que un tipo genérico implemente una operación como Add, Sub, Mul o Div en una función genérica?
Estoy intentando implementar una función genérica en Rust donde el único requisito para el argumento es que se defina la operación de multiplicación. Estoy intentando implementar un "poder" genérico, pero utilizaré una cube
función más simple para ilustrar el problema:
use std::ops::Mul;
fn cube<T: Mul>(x: T) -> T {
x * x * x
}
fn main() {
println!("5^3 = {}", cube(5));
}
Al compilar me sale este error:
error[E0369]: binary operation `*` cannot be applied to type `<T as std::ops::Mul>::Output`
--> src/main.rs:4:5
|
4 | x * x * x
| ^^^^^^^^^
|
= note: an implementation of `std::ops::Mul` might be missing for `<T as std::ops::Mul>::Output`
¿Qué quiere decir esto? ¿Elegí el rasgo equivocado? ¿Cómo puedo resolver esto?
Analicemos un poco su ejemplo:
fn cube<T: Mul>(x: T) -> T {
let a = x * x;
let b = a * x;
b
}
¿Cuales son los tipos de a
y b
? En este caso, el tipo a
es <T as std::ops::Mul>::Output
: ¿le suena familiar por el mensaje de error? Luego, estamos intentando multiplicar ese tipo nuevamente x
, ¡pero no hay garantía de que Output
pueda multiplicarse por algo!
Hagamos lo más simple y digamos que T * T
debe resultar en T
:
fn cube<T: Mul<Output = T>>(x: T) -> T {
x * x * x
}
Desafortunadamente, esto genera dos errores similares:
error[E0382]: use of moved value: `x`
--> src/lib.rs:6:9
|
6 | x * x * x
| - ^ value used here after move
| |
| value moved here
|
= note: move occurs because `x` has type `T`, which does not implement the `Copy` trait
Lo cual se debe a que el Mul
rasgo toma argumentos por valor , por lo que agregamos Copy
para poder duplicar los valores.
También cambié a la where
cláusula porque me gusta más y es difícil de manejar tener tanto en línea:
fn cube<T>(x: T) -> T
where
T: Mul<Output = T> + Copy
{
x * x * x
}
Ver también:
- ¿Cómo implemento el rasgo Agregar para una referencia a una estructura?
- ¿Cómo escribir un rasgo destinado a agregar dos referencias de un tipo genérico?
El límite T: Mul
no implica que el resultado del operador binario también sea de tipo T
. El tipo de resultado es un tipo asociado de este rasgo: Output
.
El otro problema es que antes de Rust 1.0 los rasgos del operador cambiaban de paso por referencia a paso por valor. En código genérico, esto puede ser un poco molesto (al menos por ahora) porque estos operadores consumen sus operandos a menos que también requiera que los tipos sean Copy
.
Solo para completar (en caso de que no desee solicitar Copy
), permítame agregar información sobre una posible dirección alternativa.
Por el bien del código genérico, se recomienda a los autores de "tipos numéricos" que proporcionen implementaciones adicionales que no consuman estos rasgos de operador para que no necesite Copy
o Clone
. Por ejemplo, la biblioteca estándar ya proporciona las siguientes implementaciones:
f64 implements Mul< f64>
f64 implements Mul<&f64>
&f64 implements Mul< f64>
&f64 implements Mul<&f64>
Cada una de estas implementaciones tiene f64
como Output
tipo. Usar estos rasgos directamente no es agradable:
fn cube<T>(x: &T) -> T
where
for<'a> T: Mul<&'a T, Output = T>,
for<'a, 'b> &'a T: Mul<&'b T, Output = T>,
{
x * x * x
}
Con el tiempo, podríamos obtener algunos rasgos de nivel (ligeramente) más altos, lo que reduciría el ruido. Por ejemplo: T: Mul2
podría implicar T: Mul<T> + Mul<&T>
y &T: Mul<T> + Mul<&T>
, pero al momento de escribir esto, el compilador de Rust no parece capaz de manejar esto. Al menos no pude compilar exitosamente el siguiente código:
use std::ops::Mul;
pub trait Mul2
where
Self: Mul<Self, Output = Self>,
Self: for<'a> Mul<&'a Self, Output = Self>,
for<'a> &'a Self: Mul<Self, Output = Self>,
for<'a, 'b> &'a Self: Mul<&'b Self, Output = Self>,
{
}
impl<T> Mul2 for T
where
T: Mul<T, Output = T>,
T: for<'a> Mul<&'a T, Output = T>,
for<'a> &'a T: Mul<T, Output = T>,
for<'a, 'b> &'a T: Mul<&'b T, Output = T>,
{
}
fn cube<T: Mul2>(x: &T) -> T {
x * x * x
}
fn main() {
let c = cube(&2.3);
println!("Hello, world! {}", c)
}
Creo que es seguro decir que las cosas mejorarán en esta área. Por ahora, la capacidad de implementar genéricamente algoritmos numéricos en Rust no es tan buena como me gustaría.