Реализовать Fn Trait (оператор вызова) для разных типов аргументов
Например, у меня есть простой классификатор
struct Clf {
x: f64
}
Классификатор возвращает 0, если наблюдаемое значение меньше x, и 1, если больше x.
Теперь я хочу реализовать оператор вызова для этого классификатора. Однако функция должна иметь возможность принимать в качестве аргументов либо float, либо вектор. В случае вектора на выходе получается вектор 0 или 1, который имеет тот же размер, что и входной вектор. Это должно работать так
let c = Clf { x : 0 };
let v = vec![-1, 0.5, 1];
println!("{}", c(0.5)); // prints 1
println!("{}", c(v)); // prints [0, 1, 1]
Как я могу написать
impl Fn for Clf{
extern "rust-call" fn call ...
...
}
в этом случае?
3 ответа
Краткий ответ: вы не можете. По крайней мере, это не будет работать так, как вы хотите. Я думаю, что лучший способ показать это - пройтись и посмотреть, что происходит, но общая идея заключается в том, что Rust не поддерживает перегрузку функций.
Для этого примера мы будем реализовывать FnOnce
, так как Fn
требует FnMut
что требует FnOnce
, Так что, если бы мы все это отсортировали, мы могли бы сделать это для других функций.
Во-первых, это нестабильно, поэтому нам нужны некоторые флаги
#![feature(unboxed_closures, fn_traits)]
Тогда давайте сделаем impl
для принятия f64
:
impl FnOnce<(f64,)> for Clf {
type Output = i32;
extern "rust-call" fn call_once(self, args: (f64,)) -> i32 {
if args.0 > self.x {
1
} else {
0
}
}
}
Аргументы в пользу Fn
Семейство черт поставляется через кортеж, так что это (f64,)
синтаксис; это кортеж только с одним элементом.
Это все хорошо, и теперь мы можем сделать c(0.5)
хотя будет потреблять c
пока мы не реализуем другие черты.
Теперь давайте сделаем то же самое для Vec
s:
impl FnOnce<(Vec<f64>,)> for Clf {
type Output = Vec<i32>;
extern "rust-call" fn call_once(self, args: (Vec<f64>,)) -> Vec<i32> {
args.0.iter().map(|&f| if f > self.x { 1 } else { 0 }).collect()
}
}
Теперь у нас есть проблема. Если мы попробуем c(v)
или даже c(0.5)
(который работал раньше), мы получаем ошибку о неизвестном типе функции. По сути, Rust не поддерживает перегрузку функций. Но мы все еще можем вызывать функции, используя UFC, где c(0.5)
становится FnOnce::call_once(c, (0.5,))
,
Не зная вашей более широкой картины, я бы хотел решить эту проблему, просто дав Clf
две функции вроде так:
impl Clf {
fn classify(&self, val: f64) -> u32 {
if val > self.x { 1 } else { 0 }
}
fn classify_vec(&self, vals: Vec<f64>) -> Vec<u32> {
vals.map(|v| self.classify(v)).collect()
}
}
Тогда ваш пример использования становится
let c = Clf { x : 0 };
let v = vec![-1, 0.5, 1];
println!("{}", c.classify(0.5)); // prints 1
println!("{}", c.classify_vec(v)); // prints [0, 1, 1]
Я бы на самом деле хотел сделать вторую функцию classify_slice
и возьми &[f64]
если быть более общим, то вы все равно можете использовать его с vecs, ссылаясь на них: c.classify_slice(&v)
,
Это действительно возможно, но вам нужна новая черта и куча беспорядка.
Если вы начнете с абстракции
enum VecOrScalar<T> {
Scalar(T),
Vector(Vec<T>),
}
use VecOrScalar::*;
Вы хотите способ использовать преобразования типа
T (hidden) -> VecOrScalar<T> -> T (known)
Vec<T> (hidden) -> VecOrScalar<T> -> Vec<T> (known)
потому что тогда вы можете взять "скрытый" тип T
заверните в VecOrScalar
и извлечь реальный тип T
с match
,
Вы также хотите
T (known) -> bool = T::Output
Vec<T> (known) -> Vec<bool> = Vec<T>::Output
но без HKT это немного сложно. Вместо этого вы можете сделать
T (known) -> VecOrScalar<T> -> T::Output
Vec<T> (known) -> VecOrScalar<T> -> Vec<T>::Output
если вы разрешите ветку, которая может паниковать.
Таким образом, черта будет
trait FromVecOrScalar<T> {
fn put(self) -> VecOrScalar<T>;
type Output;
fn get(out: VecOrScalar<bool>) -> Self::Output;
}
с реализациями
impl<T> FromVecOrScalar<T> for T {
fn put(self) -> VecOrScalar<T> {
Scalar(self)
}
type Output = bool;
fn get(out: VecOrScalar<bool>) -> Self::Output {
match out {
Scalar(val) => val,
Vector(_) => panic!("Wrong output type!"),
}
}
}
impl<T> FromVecOrScalar<T> for Vec<T> {
fn put(self) -> VecOrScalar<T> {
Vector(self)
}
type Output = Vec<bool>;
fn get(out: VecOrScalar<bool>) -> Self::Output {
match out {
Vector(val) => val,
Scalar(_) => panic!("Wrong output type!"),
}
}
}
Твой класс
#[derive(Copy, Clone)]
struct Clf {
x: f64,
}
сначала реализуем две ветви:
impl Clf {
fn calc_scalar(self, f: f64) -> bool {
f > self.x
}
fn calc_vector(self, v: Vec<f64>) -> Vec<bool> {
v.into_iter().map(|x| self.calc_scalar(x)).collect()
}
}
Затем он будет отправлять путем реализации FnOnce
за T: FromVecOrScalar<f64>
impl<T> FnOnce<(T,)> for Clf
where T: FromVecOrScalar<f64>
{
с типами
type Output = T::Output;
extern "rust-call" fn call_once(self, (arg,): (T,)) -> T::Output {
Диспетчер сначала упаковывает приватный тип, так что вы можете извлечь его с помощью enum
, а потом T::get
s результат, чтобы скрыть это снова.
match arg.put() {
Scalar(scalar) =>
T::get(Scalar(self.calc_scalar(scalar))),
Vector(vector) =>
T::get(Vector(self.calc_vector(vector))),
}
}
}
Тогда успех:
fn main() {
let c = Clf { x : 0.0 };
let v = vec![-1.0, 0.5, 1.0];
println!("{}", c(0.5f64));
println!("{:?}", c(v));
}
Поскольку компилятор может видеть всю эту малярию, он фактически полностью компилируется в практически ту же сборку, что и прямой вызов calc_
методы.
Но это не значит, что это приятно писать. Такая перегрузка - боль, хрупкая и, безусловно, плохая идея ™. Не делай этого, хотя приятно знать, что ты можешь.
Ты не можешь
Прежде всего, реализация Fn*
Семейство черт явно нестабильно и может изменяться в любое время, поэтому было бы неплохо полагаться на это.
Во-вторых, и что более важно, компилятор Rust просто не позволит вам вызвать значение, которое имеет Fn*
реализации для различных типов аргументов. Он просто не может понять, чего вы хотите, потому что, как правило, этого не происходит. Единственный способ обойти это - полностью указать черту, которую вы хотели назвать, но в этот момент вы потеряли все возможные эргономические преимущества этого подхода.
Просто определите и реализуйте свою собственную черту вместо того, чтобы пытаться использовать Fn*
черты. Я взял на себя смелость с вопросом, чтобы избежать / исправить сомнительные аспекты.
struct Clf {
x: f64,
}
trait ClfExt<T: ?Sized> {
type Result;
fn classify(&self, arg: &T) -> Self::Result;
}
impl ClfExt<f64> for Clf {
type Result = bool;
fn classify(&self, arg: &f64) -> Self::Result {
*arg > self.x
}
}
impl ClfExt<[f64]> for Clf {
type Result = Vec<bool>;
fn classify(&self, arg: &[f64]) -> Self::Result {
arg.iter()
.map(|v| self.classify(v))
.collect()
}
}
fn main() {
let c = Clf { x : 0.0 };
let v = vec![-1.0, 0.5, 1.0];
println!("{}", c.classify(&0.5f64));
println!("{:?}", c.classify(&v[..]));
}
Примечание: включено для полноты картины; на самом деле не делайте этого. Мало того, что это не поддерживается, это чертовски уродливо.
#![feature(fn_traits, unboxed_closures)]
#[derive(Copy, Clone)]
struct Clf {
x: f64,
}
impl FnOnce<(f64,)> for Clf {
type Output = bool;
extern "rust-call" fn call_once(self, args: (f64,)) -> Self::Output {
args.0 > self.x
}
}
impl<'a> FnOnce<(&'a [f64],)> for Clf {
type Output = Vec<bool>;
extern "rust-call" fn call_once(self, args: (&'a [f64],)) -> Self::Output {
args.0.iter().cloned()
.map(|v| { FnOnce::call_once(self, (v,)) })
.collect()
}
}
fn main() {
let c = Clf { x : 0.0 };
let v = vec![-1.0, 0.5, 1.0];
println!("{}", FnOnce::call_once(c, (0.5f64,)));
println!("{:?}", FnOnce::call_once(c, (&v[..],)));
}
Вы можете использовать ночные и нестабильные функции:
#![feature(fn_traits, unboxed_closures)]
struct Clf {
x: f64,
}
impl FnOnce<(f64,)> for Clf {
type Output = i32;
extern "rust-call" fn call_once(self, args: (f64,)) -> i32 {
if args.0 > self.x {
1
} else {
0
}
}
}
impl FnOnce<(Vec<f64>,)> for Clf {
type Output = Vec<i32>;
extern "rust-call" fn call_once(self, args: (Vec<f64>,)) -> Vec<i32> {
args.0
.iter()
.map(|&f| if f > self.x { 1 } else { 0 })
.collect()
}
}
fn main() {
let c = Clf { x: 0.0 };
let v = vec![-1.0, 0.5, 1.0];
println!("{:?}", c(0.5));
let c = Clf { x: 0.0 };
println!("{:?}", c(v));
}