微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

为什么我不必声明 x 是可重用/可复制的仿射语义和函数类型?

如何解决为什么我不必声明 x 是可重用/可复制的仿射语义和函数类型?

有人告诉我 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,f: fn(A) -> B) -> A {
    x;
    f(x)
}

fn throw_away4<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))
}

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

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

因此要将A提升到fn() -> 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 "is" !(() -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) -> BFn(A) -> B更严格。我错过了什么?

解决方法

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

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

  • Copymarker trait('marker' 意味着它不提供任何方法),它的特殊目的是告诉编译器它可以自由地自动复制类型的位每当它被多次使用时。任何实现 Copy 的东西都选择退出移动但不复制系统——但不能因此违反不同类型的非复制性。
  • Fn 是可以通过不可变引用(不修改或使用函数本身)调用的函数的特征。这在原则上与 Copy 是分开的,但实际上非常相似;人们最终可能会遇到的差异(其中一些不能在普通代码中发生)是:
    • 如果一个函数实现了 Fn 但没有实现 CopyClone,那么你不能存储该函数的多个地方,但你可以调用它随心所欲。
    • 如果一个函数实现了 Copy 但没有实现 Fn(只有 FnOnce),那么这是不可见的,因为它的每次调用(除了最后一次)都会隐式复制它。
    • 如果函数实现了 Clone 但没有实现 FnCopy,那么每次调用它时都必须.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)); 具有该效果。忽略这一点,它们是相反的。更好的一对反函数是将值移入 struct 并退出的函数——这是有效的,除了允许不属于该特定结构类型的输入。>


由于 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 类型都实现了 FnCopy,因此它们始终可以被复制和重用。

编写一个 dup 函数的方法是:

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

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


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

我不是逻辑学家,但我认为前半部分是不正确的。特别是,(除了一些与泛型无关的考虑之外)没有“声明的函数”具有类型 fn(A) -> B 的任意值不具有的属性。相反,fn(A) -> B 类型的值可以被复制,而这种可复制性直接对应于“它可以被责备”这一事实,因为(直到我们开始引入像 JIT 代码生成这样的想法) 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,F>(f: F) 
where
   F: Fn(A) -> B
{}

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

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

fn foo<A>() -> fn(A) -> A {
    |x| x
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。