8

有人告诉我,Rust 在仿射逻辑中有一种语义——所以它有删除/弱化但没有复制/收缩。

以下编译:

fn throw_away<A, B>(x: A, _y: B) -> A {
    x
}

由于不允许重复,因此无法编译以下内容:

fn dup<A>(x: A) -> (A, A) {
    (x, x)
}

同样,这些都不编译:

fn throw_away3<A, B>(x: A, f: fn(A) -> B) -> A {
    x;
    f(x)
}

fn throw_away4<A, B>(x: A, f: fn(A) -> B) -> A {
    throw_away(x, f(x))
}

减弱也有目共睹

fn weaken<A, B, C>(f: fn(A) -> B) -> impl Fn(A, C) -> B {
    move |x: A, y: C| f(x)
}

我们没有返回fn(A, C) -> B,而是返回impl Fn(A, C) -> B。有没有办法返回fn(A, C) -> B呢?如果没有也没关系;我只是好奇。

我期望的另一件事是你可以提升A() -> A. 但是,Rust 中的函数可以多次复制和使用。例如,

fn app_twice(f: fn(A) -> A, x: A) -> A {
    f(f(x))
}

假设实际上有一个 function lift(x: A) -> fn() -> A,那么我们可以打破 move 语义。例如,这将允许

fn dup_allowed(x: A) -> (A, A) {
    let h = lift(x);
    (h(), h())
}

因此要提升Afn() -> A,我们需要知道该函数是“线性/仿射”或只能使用一次。Rust 为此提供了一种类型:FnOnce() -> A. 在下文中,第一个编译,第二个不编译。

fn app_once(f: impl FnOnce(A) -> A, x: A) -> A {
    f(x)
}

fn app_twice2(f: impl FnOnce(A) -> A, x: A) -> A {
    f(f(x))
}

以下函数是互逆的(可能我不太了解 Rust 的语义,无法说它们实际上是互逆的):

fn lift_up<A>(x: A) -> impl FnOnce() -> A {
    move || x
}

fn lift_up_r<A>(f: impl FnOnce() -> A) -> A {
    f()
}

由于fn dup<A>(x: A) -> (A, A) { (x,x) }无法编译,我认为以下可能是一个问题:

fn dup<A>(x: fn() -> A) -> (A, A) {
    (x(), x())
}

似乎 Rust 正在为fn(A) -> B类型做一些特别的事情。

为什么我不必在上面声明 x 是可重用/可复制的?

也许正在发生一些不同的事情。声明的函数有点特殊fn f(x: A) -> B { ... },就是特别的见证A -> B。因此,如果f需要多次使用,可以根据需要进行多次批评,但fn(A) -> B完全不同:它不是构造的东西,而是假设的东西,并且必须使用fn(A) -> B可复制的东西。事实上,我一直认为它更像是一个可自由复制的实体。这是我的粗略类比:

  • fn my_fun<A,B>(x :A) -> B { M } “是” x:A |- M:B
  • fn(A) -> B"is" !(A -o B) 因此可以自由复制
  • 因此fn() -> A“是” !(() -o A) = !A 因此fn () -> A是 A 上的 (co)free 重复
  • fn dup_arg<A: Copy>(x: A) -> B { M }“说” A 有重复或者是一个类固醇
  • impl FnOnce (A) -> B“是” A -o B

但这不可能是正确的......为了什么impl Fn(A) -> B?从玩了一下,似乎fn(A) -> B比 更严格Fn(A) -> B。我错过了什么?

4

1 回答 1

7
fn weaken<A, B, C>(f: fn(A) -> B) -> impl Fn(A, C) -> B {
    move |x: A, y: C| f(x)
}

fn(A, C) -> B,我们没有返回,而是返回了impl Fn(A, C) -> B。有没有办法返回fn(A, C) -> B呢?如果没有也没关系;我只是好奇。

不,因为fn根据定义 a 不是闭包:它不能包含任何未编译到程序中的状态(在本例中为 的值f)。这与您的下一个观察密切相关:因为afn不能关闭任何东西,所以它通常不能包含任何非Copy类型,因此总是可以多次调用或复制自身,而不会违反我们正在讨论的属性。

准确地说:所有fn(..) -> _类型都实现FnCopy(以及FnOnce)。

  • Copy标记特征(“标记”意味着它不提供任何方法),其特殊目的是告诉编译器可以自由地在多次使用时自动复制类型的位。任何实施Copy都是选择退出移动但不复制系统 - 但不能因此违反不同类型的非复制性。
  • Fn是可以通过不可变引用调用的函数的特征(不修改或使用函数本身)。这在原则上与 是分开的Copy,但在效果上非常相似;最终可能产生的差异(其中一些在普通代码中不会发生)是:
    • 如果一个函数实现Fn但没有实现Copyor Clone,那么您不能函数存储在多个位置,但您可以根据需要多次调用它。
    • 如果一个函数实现Copy但没有Fn(仅FnOnce)实现,那么 this 是不可见的,因为它的每次调用(除了最后一个)都会隐式复制它。
    • 如果一个函数实现Clone但没有实现Fnor Copy,那么.clone()每次调用它时都必须使用它(最后一个除外)。

事实上,以下函数是互为逆的(可能,我不太了解 rust 的语义,无法说它们实际上是互为逆的):

fn lift_up<A> (x:A) -> impl FnOnce () -> A {move | | x}
fn lift_up_r<A> (f : impl FnOnce () -> A) -> A {f()}

lift_up_r接受lift_up没有产生的功能;例如,如果f有副作用、恐慌或挂起,则let f = lift_up(lift_up_r(f));具有该效果。忽略这一点,它们是相反的。没有这个警告的更好的一对逆将是用于将值移入 astruct并退出的函数——这实际上是在做的,除了允许不属于该特定结构类型的输入。


由于fn dup (x:A) -> (A,A) {(x,x)}无法编译,我认为以下可能是一个问题:

fn dup<A> (x : fn() -> A) -> (A,A) {(x(),x()}

但似乎 rust 对 fn(A) -> B 类型做了一些特别的事情。最后,我的问题:为什么我不必在上面声明 x 是可重用/可复制的?

当您有一个带有类型变量的泛型函数时fn dup<A>,编译器不会对A(Sized除非您选择退出该隐式绑定,因为使用非Sized值具有高度限制性并且通常不是您想要的)。特别是,它不假设A实现Copy.

另一方面,正如我上面提到的,所有fn类型都实现Fnand Copy,所以它们总是可以被复制和重用。

编写dup对通用函数进行操作但无法按您期望的方式编译的函数的方法是:

fn dup<A, F>(x: F) -> (A,A)
where
    F: FnOnce() -> A
{
    (x(),x())
}

在这里,我们告诉编译器这F是一种通过调用它来消耗的函数类型,而不是告诉它任何复制F. 因此,它无法使用“错误[E0382]:使用移动值:x”进行编译。进行此编译的最短方法是添加 bound F: Copy,最通用的方法是添加F: Clone和显式.clone()调用。


也许正在发生一些不同的事情。声明的函数有点特殊 fn f(x:A) -> B {...} 是 A -> B 的特定见证。因此,如果 f 需要多次使用,则可以根据需要对其进行多次repromped . 但是 fn(A) -> B 是完全不同的东西:它不是一个构造的东西,而是一个假设的东西,并且必须使用 fn(A) -> Bs 是可复制的。事实上,我一直认为它更像是一个可自由复制的实体。

我不是逻辑学家,但我认为前半部分是不正确的。特别是(除了一些与泛型无关的考虑之外)“声明的函数”没有任何类型的值fn(A) -> B不具有的属性。相反,type 的值fn(A) -> B 可以被复制,而这种可复制性直接对应于“它可以被责备”这一事实,因为(直到我们开始引入 JIT 代码生成之类的想法)type的每个fn(A) -> B都指的是一段已编译的代码(并且没有其他数据) - 因此编译器已经检查并授予程序许可以在运行时根据需要多次重用它的引理。

impl Fn(A) -> B 是什么?从玩了一下,似乎 fn(A) -> B 比 Fn(A) -> B 更严格。我错过了什么?

语法服务于不同的impl角色,但在参数位置它几乎完全是泛型的简写。如果我写

fn foo<A, B>(f: impl Fn(A) -> B) {}

那么这相当于

fn foo<A, B, F>(f: F) 
where
   F: Fn(A) -> B
{}

除了当存在任何参数类型时不允许调用者指定任何参数impl(这与您的兴趣无关,但为了准确性我提到它)。因此,我们告诉编译器F可以是任何东西,只要它可以作为可重用函数调用。特别是,我们没有指定F: Copyor F: Clonefn(A) -> B另一方面,它是实现Fn(A) -> B and Copy的具体类型,因此您可以免费获得它。

在返回位置,fn ... -> impl Fn(A) -> Bimpl表示存在类型:您断言存在某种实现Fn该函数将返回的类型。编译器跟踪具体类型以生成代码,但您的程序避免命名它。这在返回闭包时是必需的,但在返回不关闭任何东西的函数时是可选的:例如,您可以编写

fn foo<A>() -> fn(A) -> A {
    |x| x
}
于 2021-05-12T14:59:23.563 回答