如何在 Rust 中实现通用链表映射函数?



我正在与 Rust 咬牙切齿,我正在尝试实现一个通用类型的链表。到目前为止,我的conslen功能正常工作,但我无法弄清楚map的问题。

use std::fmt;
#[derive(Debug)]
enum List<A> {
Empty,
Cons(A, Box<List<A>>),
}
fn cons<A>(x: A, xs: List<A>) -> List<A> {
return List::Cons(x, Box::new(xs));
}
fn len<A>(xs: List<A>) -> i32 {
match xs {
List::Empty => 0,
List::Cons(_, xs) => 1 + len(*xs),
}
}
fn map<A, B>(f: &Fn(A) -> B, xs: List<A>) -> List<B> {
match xs {
List::Empty => List::Empty,
List::Cons(x, xs) => cons(f(x), map(f, *xs)),
}
}
fn main() {
let xs = cons(1, cons(2, cons(3, List::Empty)));
println!("{:?}", xs);
println!("{:?}", len(xs));
let f = |x: i32| x * x;
println!("{:?})", map(f, xs));
}

错误

error[E0308]: mismatched types
--> src/main.rs:32:27
|
32 |     println!("{:?})", map(f, xs));
|                           ^ expected reference, found closure
|
= note: expected type `&std::ops::Fn(_) -> _`
found type `[closure@src/main.rs:31:13: 31:27]`

预期输出

Cons(1, Cons(2, Cons(3, Empty)))
3
Cons(1, Cons(4, Cons(9, Empty)))

我的特别问题是

println!("{:?})", map(f, xs));

如果我注释掉该行,则输出的前两行是正确的。我不确定我的map电话出了什么问题


更新

aochagavia 帮助我了解了函数引用问题和第一个所有权问题(显然是许多! - 我在使用我们在maplen中使用的相同技术时遇到问题,并收到一个新错误

我更新的map函数如下所示

fn map<A, B>(f: &Fn(A) -> B, xs: &List<A>) -> List<B> {
match *xs {
List::Empty => List::Empty,
List::Cons(x, ref xs) => cons(f(x), map(f, xs)),
}
}

我现在正在尝试这个

let f = |x: i32| x * x;
let ys = map(&f, &xs);
let zs = map(&f, &xs);
println!("{:?})", ys);
println!("{:?})", zs);

新的错误是这个

error[E0009]: cannot bind by-move and by-ref in the same pattern
--> src/main.rs:23:20
|
23 |         List::Cons(x, ref xs) => cons(f(x), map(f, xs)),
|                    ^  ------ both by-ref and by-move used
|                    |
|                    by-move pattern here

错误消息很大,因为它发生在宏中,但是如果您添加以下内容:let y = map(f, xs);你会得到一个更短(并且稍微更准确)的:

error[E0308]: mismatched types
--> <anon>:32:15
|
32 |   let y = map(f, xs);
|               ^ expected reference, found closure
|
= note: expected type `&std::ops::Fn(_) -> _`
found type `[closure@<anon>:31:11: 31:25]`

也就是说,您是通过值而不是按引用传递闭包!使用map(&f, xs)(注意与号)应该可以解决错误。但是,所有权还有另一个问题(见下文)。

所有权问题

len函数的类型签名为fn len<A> (xs: List<A>) -> i32。这意味着它将拥有该列表的所有权,以便计算其长度。但是,这不是您想要的,因为它会阻止您以后使用该列表!因此,您从编译器获得的错误。

解决这个问题的明智方法是让lenxs而不是消费它。喜欢这个:

fn len<A>(xs: &List<A>) -> i32 {
match *xs {
List::Empty => 0,
List::Cons(_, ref xs) => 1 + len(xs),
}
}

最后,您需要修改main函数以反映此更改,方法是调用如下所示的lenlen(&xs)(请注意与号,您可以将其视为借用运算符)。

使地图也借用xs

正如Naomik在评论中指出的那样,map似乎也是借用xs而不是消费它的候选人。可能的实现方式是:

fn map<A, B>(f: &Fn(&A) -> B, xs: &List<A>) -> List<B> {
match *xs {
List::Empty => List::Empty,
List::Cons(ref x, ref xs) => cons(f(x), map(f, xs)),
}
}

与原始版本的主要区别在于闭包现在采用&A而不是A(见Fn(&A) -> B)。这是很自然的,因为不可能消耗借用中包含的值(这意味着借用机制完全被破坏)。

总的来说,你需要像这样调用map

let f = |x: &i32| (*x) * (*x);
map(&f, &xs);

请注意,f现在借用其参数,而不是按照map的类型签名的要求使用它。

闭包中的一些额外背景

闭包在 Rust 中有点特别。你可以用一个很好的语法来构造它们,但最终它们只是碰巧实现FnFnMutFnOnce特征的结构。

如果要按值传递它们(而不是像在代码中那样按引用传递),可以使用以下类型签名使映射函数成为通用函数:

fn map<F, A, B> (f: F, xs: List<A>) -> List<B>
where F: Fn(A) -> B
{

这也为您提供了静态调度。如果你想了解更多,你可能应该阅读特质对象和静态/动态调度。

最新更新