Rust: `impl Trait` 的跟踪问题(RFC 1522、RFC 1951、RFC 2071)

创建于 2016-06-27  ·  417评论  ·  资料来源: rust-lang/rust

新的跟踪问题 = https://github.com/rust-lang/rust/issues/63066

实施状况

已实现RFC 1522 中指定的基本功能,但仍有一些修订需要改进:

RFC

有许多关于 impl trait 的 RFC,所有这些都由这个中心跟踪问题跟踪。

未解决的问题

该实施也提出了一些有趣的问题:

B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

最有用的评论

由于这是 FCP 关闭之前的最后一次机会,我想对自动自动特征提出最后一个论点。 我意识到这是最后一刻,所以在我们承诺当前的实施之前,我最多想正式解决这个问题。

为了澄清任何没有关注impl Trait ,这就是我要提出的问题。 当前,由impl X类型表示的类型自动实现自动特征,当且仅当它们背后的具体类型实现所述自动特征时。 具体来说,如果进行以下代码更改,该函数将继续编译,但任何依赖于它返回的类型实现Send的函数的使用都将失败。

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(更简单的例子:工作内部变化导致失败

这个问题不是很明确。 有一个非常慎重的决定让自动特征“泄漏”:如果我们不这样做,我们将不得不在每个返回非发送或非同步的函数上放置+ !Send + !Sync ,我们会与潜在的其他自定义自动特征有一个不清楚的故事,这些自动特征根本无法在函数返回的具体类型上实现。 这是我稍后会谈到的两个问题。

首先,我想简单说明我对这个问题的反对意见:这允许更改函数体以更改面向公众的 API。 这直接降低了代码的可维护性。

在 rust 的整个开发过程中,已经做出了在冗长而不是可用性方面犯错误的决定。 新人看到这些,以为是为了啰嗦而啰嗦,其实不然。 每个决定,无论是不让结构自动实现 Copy,还是在函数签名处明确所有类型,都是为了可维护性。

当我向人们介绍 Rust 时,当然,我可以向他们展示速度、生产力和内存安全性。 但是go有速度。 Ada 具有内存安全性。 Python 有生产力。 Rust 胜过所有这些,它具有可维护性。 当库作者想要更改算法以提高效率时,或者当他们想要重做 crate 的结构时,编译器会向他们保证,当他们出错时,编译器会告诉他们。 在 rust 中,我可以放心,我的代码将继续发挥作用,不仅在内存安全方面,而且在逻辑和接口方面也是如此。 _Rust 中的每个函数接口都可以完全由函数的类型声明来表示_。

稳定impl Trait很有可能违背这一信念。 当然,为了快速编写代码,它非常好,但如果我想制作原型,我会使用 python。 当需要长期可维护性而不是短期只写代码时,Rust 是首选语言。


我说这里只有“很大的机会”会变坏,因为这个问题也不是很明确。 首先,“自动特征”的整个想法是不明确的。 Send 和 Sync 是基于结构的内容实现的,而不是公共声明。 由于这个决定适用于生锈,因此类似的行为impl Trait也可以很好地发挥作用。

但是,函数和结构在代码库中的使用方式不同,而且它们不是同一个问题。

当修改结构的字段,甚至是私有字段时,会立即清楚地改变它的真实内容。 具有非发送或非同步字段的结构做出了该选择,并且库维护人员知道在 PR 更改结构的字段时要仔细检查。

在修改函数的内部时,很明显会影响性能和正确性。 然而,在 Rust 中,我们不需要检查我们是否返回了正确的类型。 函数声明是我们必须坚持的硬性契约, rustc小心翼翼。 这是结构和函数返回的自动特征之间的一条细线,但更改函数的内部结构更加常规。 一旦我们有了完整的生成器驱动的Future ,修改返回-> impl Future函数将更加常规。 如果编译器没有捕捉到修改的发送/同步实现,这些都将是作者需要筛选的更改。

为了解决这个问题,我们可以决定这是一个可接受的维护负担,就像最初的 RFC 讨论所做的那样保守的 impl trait RFC 中的这一部分列出了泄漏自动特征的最大论据(“OIBIT”是自动特征的旧名称)。

我已经列出了我对此的主要回应,但这是最后一点。 改变结构的布局并不常见。 可以防备。 确保函数继续实现相同的自动特征的维护负担比结构更大,这仅仅是因为函数变化更多。


最后一点,我想说自动自动特征不是唯一的选择。 这是我们选择的选项,但选择退出自动特征的替代方案仍然是替代方案。

我们可以要求返回非发送/非同步项目的函数以状态+ !Send + !Sync或返回具有这些界限的特征(可能是别名?)。 这不是一个好的决定,但它可能比我们目前选择的要好。

至于对自定义自动特征的关注,我认为任何新的自动特征都不应仅针对在自动特征之后引入的新类型实现。 这可能会提供比我现在可以解决的更多问题,但这不是我们无法通过更多设计解决的问题。


这已经很晚了,而且很啰嗦,我敢肯定我以前曾提出过这些反对意见。 我很高兴能够最后一次发表评论,并确保我们对我们正在做出的决定完全满意。

感谢您的阅读,我希望最终的决定能让 Rust 朝着最好的方向前进。

所有417条评论

@aturon我们真的可以将 RFC 放入存储库吗? ( @mbrubeck在那里评论说这是一个问题。)

完毕。

第一次尝试是#35091(第二次,如果你算上去年的

我遇到的一个问题是生命周期。 类型推断喜欢放置区域变量 _everywhere_ 并且没有任何区域检查更改,这些变量不会推断出除本地范围之外的任何内容。
但是,具体类型_必须_是可导出的,因此我将其限制为'static并明确命名了早期绑定的生命周期参数,但如果涉及任何函数,它_never_任何这些 - 即使是字符串文字也不会推断'static ,它几乎完全没用。

我想到的对区域检查本身的影响为 0 的一件事是擦除生命周期:

  • 没有什么暴露impl Trait的具体类型应该关心生命周期 - 快速搜索Reveal::All表明编译器中已经存在这种情况
  • 在函数的返回类型中,需要在impl Trait所有具体类型上放置一个边界,它比该函数的调用寿命更长 - 这意味着任何生命周期必然是'static或函数的生命周期参数之一 - _even_ 如果我们不知道哪个(例如“ 'a'b最短的”)
  • 我们必须为impl Trait的隐式生命周期参数选择一个方差(即在范围内的所有生命周期参数上,与类型参数相同):不变性是最简单的,并且给被调用者更多的控制权,而逆变让调用者做更多,并且需要检查返回类型中的每个生命周期是否处于逆变位置(与协变类型参数而不是不变的相同)
  • 自动特征泄漏机制要求可以在另一个函数中将特征绑定放在具体类型上 - 因为我们已经删除了生命周期并且不知道生命周期在哪里,所以必须替换具体类型中的每个已擦除生命周期具有保证不短于所有实际生命周期参数中最短生命周期的新推理变量; 问题在于 trait impls 最终可能需要更强的生命周期关系(例如X<'a, 'a>X<'static> ),必须检测到并出错,因为它们不能被证明寿命

关于自动特征泄漏的最后一点是我唯一担心的,其他一切似乎都是直截了当的。
目前尚不完全清楚我们可以按原样重用多少区域检查。 希望都是。

抄送@rust-lang/lang

@eddyb

但是对于impl Trait来说,生命_are_ 很重要-例如

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

我在 RFC 线程中多次提到过

无特征对象版本:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

这取决于as_debug的定义是否为 UB。

@arielb1啊,对,我忘记了我这样做的原因之一是只捕获生命周期参数,而不是匿名的后期绑定参数,除非它真的不起作用。

@arielb1我们是否可以在签名中的具体类型预擦除和后期绑定生命周期之间建立严格的寿命关系? 否则,只查看生命周期关系并 insta-fail 任何直接或 _indirect_ 'a outlives 'b可能不是一个坏主意,其中'a'static或生命周期参数之外的 _anything_并且'b出现在impl Trait的具体类型中。

很抱歉花点时间在这里回信。 所以我一直在想
关于这个问题。 我的感觉是,我们最终必须(并且
想要)用一种新的约束来扩展 regionck —— 我称之为
\in约束,因为它允许您说出类似'0 \in {'a, 'b, 'c} ,这意味着用于'0必须是
'a'b'c 。 我不确定整合的最佳方式
这会自行解决——当然如果\in集合是单例
设置,它只是一个等价关系(我们目前没有
一流的东西,但它可以由两个界限组成),但是
否则会使事情变得复杂。

这一切都与我制作区域约束集的愿望有关
比我们今天拥有的更具表现力。 当然可以组成一个
\in约束来自OR==约束。 但当然更多
表达性约束更难解决, \in也不例外。

无论如何,让我在这里阐述一下我的想法。 让我们一起工作
例子:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

我认为impl Trait最准确的脱糖可能是
新型:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

现在impl Iterator<Item=u32>中的foo行为应该与
FooReturn<'a,'b>会表现。 虽然这不是一个完美的匹配。 一
例如,差异是方差,正如 eddyb 提出的那样——我是
假设我们将使impl Foo类类型在类型上保持不变
foo 。 但是,自动特征行为可以解决。
(匹配可能不理想的另一个领域是,如果我们添加
能够“穿透” impl Iterator抽象,因此代码
“内部”抽象知道精确的类型——然后它会排序
有一个隐式的“展开”操作发生。)

在某些方面,更好的匹配是考虑一种综合性状:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

现在我们可以认为impl Iterator类型类似于<() as FooReturn<'a,'b>>::Type 。 这也不是一个完美的匹配,因为我们
通常会使其正常化。 您可能会想象使用专业化
为了防止这种情况:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

在这种情况下, <() as FooReturn<'a,'b>>::Type不会正常化,
我们有一个更接近的比赛。 特别是方差的表现
对; 如果我们想要有一些“内部”的类型
抽象,它们将是相同的,但它们被允许
规范化。 然而,有一个问题:auto trait 的东西并没有
很管用。 (我们可能想在这里考虑协调事物,
实际上。)

无论如何,我探索这些潜在的脱糖剂的目的不是
建议我们将“impl Trait”作为_actual_desugaring
(虽然它可能很好......)但是给我们的工作一个直觉。 一世
认为第二次脱糖——就预测而言——是
对指导我们前进非常有帮助。

这个投影脱糖是一个非常有用的指南的一个地方是
“寿命”关系。 如果我们想检查是否<() as FooReturn<'a,'b>>::Type: 'x ,RFC 1214 告诉我们可以证明这一点
只要'a: 'x _and_ 'b: 'x成立。 这是我认为我们想要的
处理 impl trait 的事情。

在跨时,对于自动特征,我们必须知道XXX
当然是。 我假设这里的基本思想是创建一个类型
XXX变量并检查返回的实际值
都可以与XXX统一。 理论上,该类型变量应该
告诉我们我们的答案。 但当然问题是这种类型
变量可能会引用许多不在范围内的区域
fn 签名——例如,fn 主体的区域。 (同样的问题
不会出现在类型中; 即使,从技术上讲,你可以把
例如,在 fn 主体中的结构声明,它将是不可命名的,
这是一种人为的限制——一个人也可以移动
fn 之外的结构。)

如果您同时查看 struct desugaring 或 impl,则有一个
(隐含在 Rust 的词法结构中) XXX可以的限制
只命名'static或生命周期,如'a'b ,其中
出现在函数签名中。 那是我们不是的东西
在这里建模。 我不确定最好的方法——某种类型
推理方案对范围有更直接的表示,并且
我一直想把它添加到 Rust 中,以帮助我们处理闭包。 但
我想首先让我们考虑一下较小的三角洲。

这就是\in约束的来源。 可以想象添加
一个类型检查规则(基本上) FR(XXX) \subset {'a, 'b} --
意味着出现在 XXX 中的“自由区域”只能是'a
'b 。 这最终会转化为\in要求
XXX中出现的各个区域。

让我们看一个实际的例子:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

在这里,如果condition为 true 的类型将类似于
Cloned<SliceIter<'a, i32>> 。 但是如果condition是假的,我们会
想要Cloned<SliceIter<'b, i32>> 。 当然,在这两种情况下,我们都会
最后得到类似的东西(使用类型/区域变量的数字):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

如果我们然后将变量 0 实例化为Cloned<SliceIter<'2, i32>>
我们有'0: '2'1: '2或一组区域关系
喜欢:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

那么我们应该为'2使用什么值呢? 我们还有额外的
'2 in {'a, 'b}约束。 有了写的 fn,我想我们
必须报告错误,因为'a'b都不是
正确的选择。 不过有趣的是,如果我们添加了约束'a: 'b ,那么就会有一个正确的值( 'b )。

请注意,如果我们只运行 _normal_ 算法,我们最终会得到
'2'body 。 我不确定如何处理\in关系
除了详尽的搜索(虽然我可以想象一些特别的
例)。

好的,这就是我所得到的。 =)

在 PR #35091 上, @arielb1写道:

我不喜欢“在 impl trait 中捕获所有生命周期”的方法,并且更喜欢类似生命周期省略的方法。

我认为在这里讨论会更有意义。 @arielb1 ,您能详细说明您的想法吗? 就我上面所做的类比而言,我猜您基本上是在谈论“修剪”作为新类型或投影中的参数出现的生命周期集(即<() as FooReturn<'a>>::Type而不是<() as FooReturn<'a,'b>>::Type还是什么?

我认为存在的生命周期省略规则在这方面不会是一个很好的指导:如果我们只选择&self的生命周期来包含,那么我们不一定能够包含来自Self结构的类型参数,也不是来自方法的类型参数,因为它们可能具有要求我们命名其他一些生命周期的 WF 条件。

无论如何,很高兴看到一些示例来说明您所考虑的规则,以及它们的任何优点。 :) (另外,我想我们需要一些语法来覆盖选择。)在所有其他条件相同的情况下,如果我们可以避免从 N 个生命周期中进行选择,我更愿意这样做。

我还没有看到任何地方讨论过impl Trait与隐私的交互。
现在fn f() -> impl Trait可以返回一个私有类型S: Trait类似于特征对象fn f() -> Box<Trait> 。 即私有类型的对象可以以匿名形式在其模块之外自由移动。
这似乎是合理和可取的 - 类型本身是一个实现细节,只有通过公共特征Trait可用的接口是公共的。
然而,特征对象和impl Trait之间有一个区别。 仅使用 trait 对象,所有私有类型的 trait 方法都可以获得内部链接,它们仍然可以通过函数指针调用。 使用impl Trait的私有类型的 trait 方法可以直接从其他翻译单元调用。 对符号进行“内部化”的算法将不得不更加努力地仅针对未使用impl Trait匿名的类型或非常悲观的类型来内部化方法。

@nikomatsakis

foo的“显式”方式是

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

这里没有关于生命周期限制的问题。 显然,每次都必须编写生命周期界限将是相当重复的。 然而,我们处理这种重复的方式通常是通过生命周期省略。 在foo的情况下,省略会失败并强制程序员显式指定生命周期。

我反对像@eddyb仅在impl Trait的特定情况下添加显式敏感的生命周期省略,而不是其他情况。

@arielb1嗯,我不是 100% 确定如何根据我讨论的“脱糖”来考虑这个提议的语法。 它允许您指定似乎是生命周期的限制,但我们试图推断的主要是隐藏类型中出现的生命周期。 这是否表明最多可以“隐藏”一个生命周期(并且必须准确指定它?)

似乎并不总是“单个生命周期参数”就足够了:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

在这种情况下,隐藏的迭代器类型指的是'a'b (虽然它在这两种情况下都是_is_ 变体;但我想我们可以想出一个不变的例子)。

所以@aturon和我讨论了这个问题,我想分享一下。 这里确实有几个正交问题,我想将它们分开。 第一个问题是“隐藏类型中可能使用哪些类型/生命周期参数?” 就(准)脱糖到default type ,这归结为“我们引入的特征上出现了什么类型参数”。 因此,例如,如果这个函数:

fn foo<'a, 'b, T>() -> impl Trait { ... }

会被脱糖成类似的东西:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

那么这个问题归结为“特征Foo及其 impl 上出现了哪些类型参数”? 基本上,这里的... 。 显然,这包括Trait本身使用的类型参数集,但是还有哪些额外的类型参数呢? (正如我之前提到的,除了自动特征的泄漏之外,这种脱糖是 100% 忠实的,我认为我们也应该为特化的 impl 泄漏自动特征。)

我们一直使用的默认答案是“所有这些”,所以这里...将是'a, 'b, T (以及可能出现的任何匿名参数)。 这_可能_是一个合理的默认值,但它_必然_不是最好的默认值。 (正如@arielb1指出的那样。)

这对寿命关系有影响,因为为了确定<() as Foo<...>>::Type (指impl Trait某些特定的、不透明的实例化)寿命超过'x ,我们必须有效地证明那...: 'x (即每个生命周期和类型参数)。

这就是为什么我说考虑生命周期参数是不够的:想象我们有一些对foo调用,例如foo::<'a0, 'b0, &'c0 i32> 。 这意味着所有三个生命周期'[abc]0必须比'x活得更久——换句话说,只要返回值在使用中,这将延长提供给函数的所有数据的贷款. 但是,正如@arielb1指出的那样,省略表明这通常会比必要的时间长。

所以我想我们需要的是:

  • 确定一个合理的默认值,也许使用省略的直觉;
  • 当默认值不合适时有一个明确的语法。

@aturon 吐出impl<...> Trait这样的显式语法,这似乎是合理的。 因此,可以写:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

表示隐藏类型实际上不是指'a'b而只是T 。 或者可以写impl<'a> Trait来表示'bT都没有被捕获。

至于默认值,拥有更多数据似乎非常有用——但省略的一般逻辑表明,如果适用,我们最好捕获以self类型命名的所有参数。 例如,如果您有fn foo<'a,'b>(&'a self, v: &'b [u8])并且类型是Bar<'c, X> ,那么self将是&'a Bar<'c, X> ,因此我们将捕获'a'cX默认情况下,但不是'b


另一个相关注意事项是终身绑定的含义。 我认为健全的生命周期界限有一个不应该改变的现有含义:如果我们写impl (Trait+'a)这意味着隐藏类型T 'a寿命更长。 类似地,可以写impl (Trait+'static)来表示不存在借用指针(即使捕获了一些生命周期)。 在推断隐藏类型T ,这将暗示像$T: 'static这样的生命周期绑定,其中$T是我们为隐藏类型创建的推断变量。 这将以通常的方式处理。 从调用者的角度来看,隐藏类型是隐藏的, 'static绑定将允许我们得出结论,即使捕获了生命周期参数, impl (Trait+'static)寿命也会比'static

在这里,它的行为与脱糖的行为完全相同:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

所有这些都与推理正交。 我们仍然希望(我认为)添加“选择”约束的概念,并通过一些启发式和可能的详尽搜索来修改推理(来自 RFC 1214 的经验表明,具有保守回退的启发式实际上可以让我们走得很远;我不知道人们在这方面遇到了限制,尽管某处可能存在问题)。 当然,添加像'static或 'a` 这样的生命周期边界可能会影响推理,因此会有所帮助,但这并不是一个完美的解决方案:一方面,它们对调用者可见并成为 API 的一部分,这可能不是所希望的。

可能的选项:

与输出参数省略绑定的显式生命周期

就像今天的 trait 对象一样, impl Trait对象有一个生命周期绑定参数,这是使用省略规则推断出来的。

缺点:不符合人体工学
优点:清晰

带有“所有通用”省略的显式生命周期界限

就像今天的 trait 对象一样, impl Trait对象有一个生命周期绑定参数。

但是,省略创建了一个新的早期绑定参数,它比所有显式参数都长:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

缺点:增加了一个早绑定参数

更多的。

我通过 impl Trait +'a 和借用遇到了这个问题: https :

如果我正确理解了这种变化(而且这种变化的可能性可能很低!),那么我认为这个操场代码应该可以工作:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

ThingOneThingTwo实现了Thing特征。 build表示它将返回实现Thing ,它确实如此。 然而它没有编译。 所以我显然误解了一些东西。

那个“东西”必须有一个类型,但在你的情况下,你有两种冲突的类型。 @nikomatsakis之前曾建议通过在出现类型不匹配时创建例如ThingOne | ThingTwo来进行这项工作。

@eddyb你能详细说明一下ThingOne | ThingTwo吗? 如果我们只知道运行时的类型,你不需要有Box吗? 还是一种enum

是的,它可能是一个临时的enum类型,在可能的情况下将特征方法调用委托给它的变体。

我以前也想要那种东西。 匿名枚举 RFC: https :

如果它是推理驱动的,这是一个罕见的情况,因为如果你只在不匹配的情况下创建这些类型,则变体是不同的(这是广义形式的问题)。
你也可以从没有模式匹配中得到一些东西(除非在明显不相交的情况下?)。
但是,即使您设法获得T | T ,IMO 委托糖在所有相关情况下都会“正常工作”。

你能拼出这些句子的其他隐含部分吗? 我不明白其中的大部分内容,并怀疑我错过了一些上下文。 你是否隐含地回应了联合类型的问题? RFC 只是匿名枚举,而不是联合类型 - (T|T)Result<T, T>完全一样有问题。

哦,没关系,我把提案弄糊涂了(在我整理出我失败的硬盘之前,我也一直停留在移动设备上,所以很抱歉听起来像在 Twitter 上)。

我发现(位置,即T|U != U|T )匿名枚举很有趣,我相信如果我们有可变参数泛型(你可以通过使用hlist来回避这个)和const 泛型(同上,带有 peano 数字)。

但是,与此同时,如果我们对某些东西有语言支持,那就是联合类型,而不是匿名枚举。 例如,不是Result而是错误类型(为它们绕过命名包装器的乏味)。

我不确定这是否是正确的地方,但为什么需要像impl这样的关键字? 我找不到讨论(可能是我的错)。

如果一个函数返回 impl Trait,它的主体可以返回实现 Trait 的任何类型的值

自从

fn bar(a: &Foo) {
  ...
}

意思是“接受对实现特征Foo的类型的引用”我期望

fn bar() -> Foo {
  ...
}

意思是“返回一个实现特征Foo ”。 这是不可能的吗?

@kud1ing原因是如果将来添加对动态大小的返回值的支持,则不会消除具有返回动态大小的类型Trait的函数的可能性。 目前Trait已经是一个有效的 DST,只是不可能返回 DST,因此您需要将其装箱以使其成为一个大小合适的类型。

编辑:在链接的 RFC 线程上对此进行了一些讨论。

好吧,一方面,无论是否添加动态大小的返回值,我都更喜欢当前的语法。 与 trait 对象发生的情况不同,这不是类型擦除,并且任何巧合,如“参数f: &Foo都会采用 impls Foo东西,而这会返回 impls Foo ”可能会产生误导。

我从 RFC 讨论中收集到,现在impl是一个占位符实现,并且没有非常需要impl 。 如果返回值不是 DST,是否有任何理由不想要impl特征?

我认为当前处理“自动特征泄漏”的 impl 技术是有问题的。 相反,我们应该强制执行 DAG 排序,这样如果你定义了一个 fn fn foo() -> impl Iterator并且你有一个调用者fn bar() { ... foo() ... } ,那么我们必须在bar()之前对foo()进行类型检查bar() (以便我们知道隐藏类型是什么)。 如果出现循环,我们会报告错误。 这是一个保守的立场——我们可能会做得更好——但我认为目前的技术,我们收集自动特征义务并在最后检查它们,一般来说是行不通的。 例如,它不适用于专业化。

(另一种可能比要求严格的 DAG 更宽松的可能性是在某种程度上“一起”对两个 fns 进行类型检查。我认为只有在我们重新构建 trait system impl 之后才需要考虑这一点。)

@Nercury我不明白。 您是否在问是否有理由不希望fn foo() -> Trait表示-> impl Trait

@nikomatsakis是的,我正是在问这个问题,对于令人毛骨悚然的语言感到抱歉:)。 我认为在没有 impl 关键字的情况下执行此操作会更简单,因为这种行为正是人们所期望的(当返回具体类型而不是 trait 返回类型时)。 但是,我可能会遗漏一些东西,这就是我问的原因。

不同之处在于返回impl Trait函数总是返回相同的类型——它基本上是返回类型推断。 IIUC,仅返回Trait函数将能够动态返回该特征的任何实现,但调用者需要准备通过类似box foo()为返回值分配空间。

@Nercury简单的原因是-> Trait语法已经有意义,所以我们必须为这个特性使用别的东西。

我实际上已经看到人们默认情况下会期望这两种行为,而且这种混淆经常出现,老实说,我宁愿fn foo() -> Trait没有任何意义(或者默认情况下是警告)并且有明确的“在编译时我可以选择但调用者看不到的某种类型”案例和“可以动态分派到任何实现 Trait 的类型的特征对象”案例的关键字,例如fn foo() -> impl Traitfn foo() -> dyn Trait 。 但显然那些船已经航行了。

为什么编译器不生成一个包含函数所有不同返回类型的枚举,实现通过参数传递给每个变体的特征,而是返回它?

这将绕过唯一的一种返回类型允许规则。

@NeoLegends手动执行此操作相当普遍,并且它的一些糖可能很好并且在过去已经提出,但它是与返回impl Trait或特征对象的第三组完全不同的语义,所以它不是真的与这个讨论有关。

@Ixrec是的,我知道这是手动完成的,但是匿名枚举作为编译器生成的返回类型的真正用例是您无法拼出的类型,例如迭代器或未来适配器的长链。

这种不同的语义如何? 匿名枚举(就编译器生成它们而言,而不是根据匿名枚举 RFC)作为返回值只有在有一个通用 API (例如抽象出不同变体的特征)时才真正有意义。 我建议的功能仍然看起来和行为类似于常规 impl Trait,只是通过编译器生成的枚举删除了一种类型限制,API 的使用者永远不会直接看到。 消费者应该始终只看到“impl Trait”。

匿名、自动生成的枚举为impl Trait了一个很容易错过的隐藏成本,因此需要考虑这一点。

我怀疑“自动枚举传递”的东西只对对象安全的特征有意义。 impl Trait本身也一样吗?

@rpjohnst除非这是实际的方法变体在板条箱元数据中并在调用站点单态化。 当然,这要求从一种变体到另一种变体的更改不会破坏调用者。 这可能太神奇了。

@glaebhoerl

我怀疑“自动枚举传递”的东西只对对象安全的特征有意义。 impl Trait 本身也是如此吗?

这是一个有趣的观点! 我一直在争论什么是“desugar” impl trait 的正确方法,实际上是在暗示我们可能更想将它视为“具有私有字段的结构”而不是“抽象类型投影” “ 解释。 然而,这似乎暗示了一些很像广义新类型派生的东西,当与类型族结合时,这当然不可靠的。 我承认没有完全理解这种“缓存中”的不健全性,但似乎每当我们想从 impl 自动生成某种类型F<T>的特征实现时,我们都必须非常谨慎为T

@nikomatsakis

问题是,用 Rust 术语来说

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

tl;dr 是广义的 newtype 派生是(并且现在)通过简单地transmute对 vtable 实现的——毕竟,vtable 由类型上的函数组成,并且类型和它的 newtype 具有相同的表示,所以应该没问题吧? 但是,如果这些函数也使用由给定类型的标识(而不是表示)上的类型级分支确定的类型 - 例如,使用类型函数或关联类型(或在 Haskell,GADTs 中),则它会中断。 因为不能保证这些类型的表示也是兼容的。

请注意,此问题仅可能是因为使用了不安全的转换。 如果它只是生成无聊的样板代码来包装/解包 newtype 并将每个方法从基本类型分派到它的实现(就像 Rust IIRC 的一些自动委托提案?),那么最坏的可能结果将是一个类型错误或者可能是 ICE。 毕竟,通过构造,如果你不使用不安全的代码,你就不会得到不安全的结果。 同样,如果我们为某种“自动枚举传递”生成代码,但没有使用任何unsafe原语来这样做,就不会有任何危险。

(我不确定这是否或如何与我最初的问题相关,即与impl Trait和/或自动枚举传递一起使用的特征是否必须是对象安全的?)

@rpjohnst可以让枚举案例选择加入以标记成本:

fn foo() -> enum impl Trait { ... }

不过,这几乎可以肯定是不同 RFC 的食物。

@glaebhoerl是的,我花了一些时间来研究这个问题,并且相当确信这至少在这里不是问题。

抱歉,如果这是显而易见的事情,但我试图理解为什么impl Trait不能出现在特征方法的返回类型中的原因,或者它首先是否有意义? 例如:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor这完全有道理,AFAIK 的目的是让这项工作发挥作用,但尚未实施。

有点道理,但它的基本功能不同(顺便说一句,这已经讨论了很多):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

具体来说, impl Trait impl Trait for Type关联类型的 RHS 中的trait有可能。

我知道这可能为时已晚,而且主要是自行车脱落,但它是否在任何地方都有记录,为什么要引入关键字impl ? 在我看来,我们在当前的 Rust 代码中已经有一种方法可以说“编译器会找出这里的类型”,即_ 。 我们能不能在这里重用它来给出语法:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo这不是该功能的作用,类型不是从函数返回的类型,而是一种“语义包装器”,它隐藏了除了所选API(和OIBIT,因为它们很痛苦)之外的所有内容。

我们可以允许某些函数通过强制 DAG 来推断其签名中的类型,但这样的功能从未被批准并且不太可能被添加到 Rust,因为它会触及“全局推断”。

建议使用的@Trait语法来替代impl Trait ,提到这里

更容易扩展到其他类型的位置和组合,如Box<@MyTrait>&@MyTrait

@Traitany T where T: Trait~Traitsome T where T: Trait

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

fn func(t: T) -> V ,不需要区分任何 t 或一些 v,因此作为 trait。

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

仍然有效。

@JF-Liu 我个人反对将anysome合并到一个关键字/印记中,但您在技术上是正确的,我们可以有一个印记并像原来的impl Trait一样使用它

@JF-Liu @eddyb从语言中删除

@也用于模式匹配,而不是从语言中删除。

我想到的是 AFAIK 的印记被过度使用了。

语法bikesheding:我对impl Trait表示法非常不满意,因为使用关键字(编辑器中的粗体)来命名类型太响了。 还记得 C 的struct和 Stroustroup大声语法观察(幻灯片 14)吗?

https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 中@konstin建议使用<Trait>语法。 它看起来非常好,尤其是在输入位置:

fn take_iterator(iterator: <Iterator<Item=i32>>)

我看到它会与 UFCS 有点冲突,但也许这可以解决?

我也觉得使用尖括号而不是 impl Trait 是一个更好的选择,至少在返回类型位置,例如:

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait>语法与泛型冲突,请考虑:

Vec<<FnOnce() -> bool>>Vec<@FnOnce() -> bool>

如果允许Vec<FnOnce() -> bool> ,那么<Trait>是个好主意,它表示与泛型类型参数等价。 但是由于Box<Trait>Box<@Trait> ,因此必须放弃<Trait>语法。

我更喜欢impl关键字语法,因为当您快速阅读文档时,这样可以减少误读原型的方式。
你怎么认为 ?

我只是意识到我确实在内部线程中为这个 rfc 提出了一个超集(感谢@matklad指向我这里):

允许使用函数参数和返回类型中的特征,方法是用尖括号括起来,如下例所示:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

然后编译器将使用当前应用于泛型的相同规则对参数进行单态化。 返回类型可以例如从函数实现派生。 这确实意味着您不能简单地在Box<Trait_with_transform>上调用此方法或在一般动态调度的对象上使用它,但它仍然会使规则更加宽松。 我还没有通读所有的 RFC 讨论,所以也许我已经错过了更好的解决方案。

我更喜欢 impl 关键字语法,因为当您快速阅读文档时,这样可以减少误读原型的方式。

语法突出显示中的不同颜色应该可以解决问题。

Stroustrup 的这篇论文在第 7 节讨论了 C++ 概念的类似句法选择: http :

不要对泛型和存在型使用相同的语法。 它们不是同一件事。 泛型允许调用者决定具体类型是什么,而(这个受限子集)existentials 允许被调用的函数决定具体类型是什么。 这个例子:

fn transform(iter: <Iterator>) -> <Iterator>

应该等于这个

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

或者它应该等同于这个

fn transform(iter: impl Iterator) -> impl Iterator

最后一个示例即使在夜间也无法正确编译,并且它实际上不能使用迭代器特征调用,但是像FromIter这样的特征将允许调用者构造一个实例并将其传递给函数而不能确定他们传递的具体类型。

也许语法应该相似,但不应该相同。

无需区分类型名称中的任何(泛型)或某些(存在),这取决于使用类型的位置。 在变量中使用时,参数和结构字段总是接受任何 T,在 fn 中使用时返回类型总是得到一些 T。

  • 使用Type , &Type , Box<Type>用于具体数据类型,静态调度
  • 使用@Trait&@TraitBox<@Trait>和泛型类型参数用于抽象数据类型、静态调度
  • 使用&TraitBox<Trait>抽象数据类型,动态调度

fn func(x: @Trait)等价于fn func<T: Trait>(x: T)
fn func<T1: Trait, T2: Trait>(x: T1, y: T2)可以简单地写成fn func(x: <strong i="22">@Trait</strong>, y: @Trait)
T参数在fn func<T: Trait>(x: T, y: T)仍然需要。

struct Foo { field: <strong i="28">@Trait</strong> }等价于struct Foo<T: Trait> { field: T }

在变量中使用时,参数和结构字段总是接受任何 T,在 fn 中使用时返回类型总是得到一些 T。

您现在可以使用现有的通用语法在稳定的 Rust 中返回任何特征。 这是一个非常常用的功能。 serde_json::de::from_slice&[u8]作为参数并返回T where T: Deserialize

您还可以有意义地返回一些特征,这就是我们正在讨论的功能。 你不能对反序列化函数使用existentials,就像你不能使用泛型返回未装箱的闭包一样。 它们是不同的特征。

对于一个更熟悉的示例, Iterator::collect可以返回任何T where T: FromIterator<Self::Item> ,这意味着我的首选表示法: fn collect(self) -> any FromIterator<Self::Item>

语法怎么样
fn foo () -> _ : Trait { ... }
对于返回值和
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
参数?

对我来说,真正的新建议都没有接近impl Trait的优雅。 impl是每个 rust 程序员都知道的关键字,因为它用于实现特征,它实际上暗示了该功能本身在做什么。

是的,坚持现有的关键字对我来说似乎很理想; 我希望看到impl用于存在主义,而for用于通用。

我个人反对将anysome混为一个关键字/印记

@eddyb我不认为这是一种混为一谈。 它自然地遵循以下规则:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

编辑:它是单向的,而不是同构的。


不相关:是否有任何相关提案也允许impl Trait处于其他协变位置,例如

~生锈fn 富(回调:F)-> R其中 F: FnOnce(impl SomeTrait) -> R {回调(create_something())}~

现在,这不是一个必要的特性,因为你总是可以为impl SomeTrait设置一个具体的时间,这会损害可读性,但除此之外没什么大不了的。

但是,如果 RFC 1522 特性稳定,那么如果create_something导致impl SomeTrait (至少没有装箱),就不可能为上述程序

@Rufflewind在现实世界中,事情并没有那么明确,而且这个功能是一个非常特殊的存在主义品牌(Rust 现在有几个)。

但即便如此,您所拥有的只是使用协方差来确定impl Trait在函数参数内部和外部的含义。

这还不够:

  • 使用与默认值相反的
  • 消除字段类型内部的歧义(其中anysome同样可取)

@Rufflewind这似乎是impl Trait的错误括号。 我知道 Haskell 利用这种关系仅使用forall关键字来表示普遍性和存在性,但在我们讨论的上下文中它不起作用。

以这个定义为例:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

如果我们使用“参数中的impl是通用的,返回类型中的impl是存在的”的规则,那么foo函数项类型的类型逻辑上是 this(在组成的类型符号):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

天真地将impl视为技术上仅意味着普遍或仅意味着存在,并让逻辑自行解决实际上并没有奏效。 你会得到这个:

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

或这个:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

而且这些都没有减少到我们想要的逻辑规则。 所以最终any / some确实捕捉到了一个重要的区别,你不能用单个关键字捕捉到。 在std中甚至有一些合理的例子,你希望通用的返回位置。 例如,这个Iterator方法:

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

并且没有办法用impl和参数/返回规则来编写它。

tl; dr impl上下文中表示普遍或存在确实确实赋予了它两个不同的含义。


作为参考,在我的符号中, @Rufflewind提到的 forall/exists 关系

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

这与 trait 对象(existentials)的概念等同于泛型(universals)有关,但与这个impl Trait问题无关。

也就是说,我不再强烈支持any / some 。 我想准确地说出我们正在谈论的内容,并且any / some具有这种理论上和视觉上的美感,但是我可以将impl与上下文一起使用规则。 我认为它涵盖了所有常见情况,它避免了上下文关键字语法问题,其余的我们可以使用命名类型参数。

在那一点上,为了匹配普遍性的全部普遍性,我认为我们最终需要一个命名存在的语法,它支持任意 where 子句以及在签名中的多个位置使用相同存在的能力。

总之,我很高兴:

  • impl Trait作为普遍和存在(上下文)的简写。
  • 命名类型参数作为通用和存在的完全通用的速记。 (不太常见。)

天真地将 impl 视为技术上仅意味着普遍或仅意味着存在,并让逻辑自行解决实际上并不能解决问题。 你会得到这个:

@solson对我来说,“天真的”翻译会导致存在量词紧挨着被量化的类型。 因此

~生锈(impl MyTrait)~

只是语法糖

~生锈(存在T)~

这是一个简单的局部变换。 因此,遵循“ impl始终是存在的”规则的天真翻译将导致:

~生锈fn(存在T) -> (存在R)~

然后,如果你把量词从函数参数中拉出来,它就变成了

~生锈为了fn(T) -> (存在R)~

因此,即使T相对于自身而言总是存在的,但相对于整个函数类型而言,它似乎是通用的


IMO,我认为impl也可能成为存在类型的事实上的关键字。 将来,也许可以想象构建更复杂的存在类型,例如:

~~生锈(暗示(向量, T))~ ~

类似于通用类型(通过 HRTB)

~生锈(for<'a> FnOnce(&'a T))~

@Rufflewind该视图不起作用,因为fn(T) -> (exists<R: ReturnTrait>(R))在逻辑上不等同于exists<R: ReturnTrait>(fn(T) -> R) ,这就是返回类型impl Trait真正含义。

(至少不是在通常应用于类型系统的构造逻辑中,其中为存在选择的特定见证是相关的。前者意味着函数可以根据参数选择不同的类型来返回,而后者意味着存在该函数的所有调用的一种特定类型,如impl Trait 。)

我也觉得我们离得有点远了。 我认为上下文impl是一个可以做出的妥协,而且我不认为达到这种理由是必要的或特别有帮助的(我们当然不会根据这种逻辑联系来教授规则)。

@solson是的,你是对的:存在主义不能浮出水面。 这个一般不成立:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

而这些确实普遍存在:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

最后一个负责将参数中的存在主义重新解释为泛型。

编辑:糟糕, (∀A. g(A) → T) → (∃A. g(A)) → T确实成立。

我发布了一个 RFC,其中包含扩展和稳定impl Trait的详细建议。 它借鉴了有关此主题和早期主题的大量讨论。

值得注意的是https://github.com/rust-lang/rfcs/pull/1951已被接受。

目前这方面的情况如何? 我们有一个登陆的 RFC,我们有人在使用初始实现,但我不清楚要执行哪些项目。

在 #43869 中发现-> impl Trait函数不支持纯发散体:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

这是预期的(因为!不暗示Iterator ),还是被认为是错误?

定义推断类型怎么样,它不仅可以用作返回值,而且可以用作当前可以使用的任何类型(我猜)?
就像是:
type Foo: FnOnce() -> f32 = #[infer];
或使用关键字:
infer Foo: FnOnce() -> f32;

然后类型Foo可以用作返回类型、参数类型或任何其他类型可以使用的类型,但是在需要不同类型的两个不同地方使用它是非法的,即使那type 在这两种情况下都实现了FnOnce() -> f32 。 例如,以下内容将无法编译:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

这不应该编译,因为即使return_closurereturn_closure2的返回类型都是FnOnce() -> f32 ,它们的类型实际上是不同的,因为在 Rust 中没有两个闭包具有相同的类型. 因此,要编译上述内容,您需要定义两种不同的推断类型:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

看到代码我觉得这里发生的事情是相当明显的,即使你事先不知道 infer 关键字是做什么的,而且它非常灵活。

infer 关键字(或宏)本质上会告诉编译器根据使用的位置来确定类型是什么。 如果编译器无法推断类型,它会抛出一个错误,这可能发生在没有足够的信息来缩小它必须是什么类型时(例如,如果推断的类型没有在任何地方使用,尽管也许最好将该特定情况作为警告),或者当无法找到适合其使用的任何地方的类型时(如上面的示例)。

@cramertj啊,这就是为什么这个问题变得如此沉默的原因..

所以, @cramertj向我询问我认为如何最好地解决他们在 PR 中遇到的后期绑定区域的问题。 我的看法是,我们可能想稍微“改造”我们的实现以尝试并期待anonymous type Foo模型。

对于上下文,这个想法大致是

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

会(有点)脱糖成这样的东西

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

请注意,在这种形式中,您可以看到哪些泛型参数被捕获,因为它们显示为Foo ——值得注意的是, 'b没有被捕获,因为它没有出现在无论如何,但类型参数TU始终是。

无论如何,目前在编译器中,当你有一个impl Debug引用时,我们会创建一个 def-id - 实际上 - 代表这个匿名类型。 然后我们有generics_of查询,它计算它的通用参数。 现在,这返回与“封闭”上下文相同的内容——即函数foo 。 这就是我们想要改变的。

在“另一边”,即在foo的签名中,我们将impl FooTyAnon 。 这基本上是正确的—— TyAnon表示我们在上面的脱糖中看到的对Foo的引用。 但是我们获得这种类型的“substs”的方式是使用“identity”函数,这显然是错误的——或者至少不能概括。

所以特别是这里发生了一种“命名空间违规”。 当我们为一个项目生成“身份”替代品时,通常会给我们在类型检查该项目时使用的替换——也就是说,它的所有通用参数都在范围内。 但是在这种情况下,我们创建了对出现在函数foo()内部的Foo的引用,因此我们希望foo()的泛型参数出现在Substs ,而不是Foo 。 这恰好起作用,因为现在它们是相同的,但它并不真正正确

我认为我们应该做的是这样的:

首先,当我们计算Foo的泛型类型参数(即匿名类型本身)时,我们将开始构造一组新的泛型。 自然会包括类型。 但是对于一生,我们会遍历特征边界并识别其中出现的每个区域。 这与cramertj 编写的现有代码非常相似,只是我们不想累积 def-id,因为并非范围内的所有区域都有 def-id。

我认为我们要做的是累积出现的区域集并将它们按某种顺序排列,并foo()的角度跟踪FreeRegion的概念,这几乎可以工作,但我们不再将FreeRegion用于早期绑定的内容,仅用于后期绑定的内容。)

也许最简单和最好的选择是只使用Region<'tcx> ,但是当你去“取消”任何引入的活页夹时,你必须改变 debruijn 索引深度。 不过,这也许是最好的选择。

所以基本上当我们在visit_lifetime获得回调时,我们会将它们转换为以初始深度表示的Region<'tcx> (我们必须在通过活页夹时进行跟踪)。 我们会将它们累积到一个向量中,消除重复项。

完成后,我们有两件事:

  • 首先,对于向量中的每个区域,我们需要创建一个通用区域参数。 他们都可以有匿名名称或其他任何东西,这并不重要(尽管也许我们需要他们有 def-id 或其他东西......?我必须看看RegionParameterDef数据结构......) .
  • 其次,向量中的区域也是我们想要用于“substs”的东西。

好的,对不起,如果这是一个神秘的。 我不知道如何说得更清楚。 不过我不确定——现在,我觉得我们对区域的处理非常复杂,所以也许有一种方法可以重构事情以使其更加统一? 我敢打赌, @eddyb在这里有一些想法。 ;)

@nikomatsakis我相信很多内容与我告诉@cramertj 的内容相似,但更加充实!

我一直在考虑存在主义impl Trait ,我遇到了一个奇怪的案例,我认为我们应该谨慎行事。 考虑这个函数:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

正如您可以在 play 上验证的那样,此代码今天可以编译。 但是,如果我们深入研究正在发生的事情,它会突出一些与我有关的“前向兼容性”危险。

具体来说,很清楚我们如何推断此处返回的类型( () )。 我们如何推断impl Debug参数的类型不太清楚。 也就是说,您可以将此返回值视为-> ?T where ?T: Foo<?U> 。 我们必须根据?T = ()的事实来推断?T?U的值。

现在,我们通过利用仅存在一个 impl 的事实来做到这一点。 然而,这是一个脆弱的属性。 如果添加了新的 impl,代码将不再编译,因为现在我们无法唯一确定?U必须是什么。

这可能发生在 Rust 的许多场景中——这已经足够令人担忧了,但是是正交的——但是impl Trait情况有一些不同。 impl Trait的情况下,我们没有办法让用户添加类型注释来指导推理! 我们也没有真正的计划。 唯一的解决方案是将 fn 接口更改为impl Foo<()>或其他明确的内容。

将来,使用abstract type ,可以想象允许用户显式给出隐藏值(或者可能只是不完整的提示,使用_ ),这可以帮助推理,同时大致保持相同的公共接口

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

尽管如此,我认为避免稳定存在 impl Trait 的“嵌套”使用是谨慎的,除非在关联的类型绑定中(例如, impl Iterator<Item = impl Debug>不会受到这些歧义的影响)。

在 impl Trait 的情况下,我们没有办法让用户添加类型注释来指导推理! 我们也没有真正的计划。

也许它看起来像 UFCS? 例如<() as Foo<()>> - 不会像裸露的as那样改变类型,只是消除歧义。 这是当前无效的语法,因为它期望::和更多遵循。

我刚刚发现了一个关于Fn impl Trait 类型推断的有趣案例:
以下代码编译得很好

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

如果我们注释掉 Sub-line,就会抛出编译错误

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien这似乎与impl Trait无关——一般来说推理是正确的。 尝试对您的示例进行轻微修改:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

看起来现在关闭了:

与省略交互时的 ICE

我在本期或讨论中没有看到的一件事是能够在结构字段中存储闭包和生成器(调用者未提供)。 现在,这是可能的,但看起来很难看:您必须为每个闭包/生成器字段添加一个类型参数到结构,然后在构造函数的签名中,将该类型参数替换为impl FnMut/impl Generator这是一个示例,它可以工作,非常酷! 但它还有很多不足之处。 如果您可以摆脱类型参数会更好:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait可能不是正确的方法——如果我正确阅读并理解了 RFC 2071,可能是抽象类型。 我们需要的是可以在结构定义中写入的内容,以便可以推断出实际类型( [generator@src/main.rs:15:17: 21:10 _] )。

我相信@mikeyhew抽象类型确实是我们期望的工作方式。 语法看起来大致像

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

如果我想将其他人的impl Generator放入我的结构中,但他们没有制作abstract type供我使用,是否有备用路径?

@scottmcm您仍然可以声明自己的abstract type

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj等等,抽象类型已经在夜间出现了?! 公关在哪里?

@alexreg不,他们不是。

编辑:您好,来自未来的访客! 下面的问题已经解决。


我想提请注意 #47348 中出现的这个时髦的边缘使用案例

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

是否应该允许像这样返回对impl Trait的投影? (因为目前,__它是.__)

我找不到任何关于这种用法的讨论,也找不到任何测试用例。

@ExpHP嗯。 看起来确实有问题,原因与impl Foo<impl Bar>有问题的原因相同。 基本上,我们对所讨论的类型没有任何真正的限制——只有从它投射出来的东西。

我认为我们想重用 impls 中围绕“约束类型参数”的逻辑。 简而言之,指定返回类型应该“约束” impl Sub 。 我指的功能是这个:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

对于喜欢复选框的人来说,有一点点分类:

  • #46464 完成 -> 复选框
  • #48072 完成 -> 复选框

@rfcbot fcp 合并

我建议我们稳定conservative_impl_traituniversal_impl_trait功能,并进行一项待定更改(对 https://github.com/rust-lang/rust/issues/46541 的修复)。

记录当前语义的测试

这些功能的测试可以在以下目录中找到:

运行通行证/ impl-trait
ui/impl-trait
编译失败/impl-trait

实施过程中解决的问题

impl Trait的解析细节在RFC 2250中得到解决,并在https://github.com/rust-lang/rust/pull/45294 中实现

impl Trait已被禁止嵌套非关联类型位置和某些合格的路径位置,以防止歧义。 这是在https://github.com/rust-lang/rust/pull/48084中实现的

剩余的不稳定特征

在此稳定之后,将可以在非特征函数的参数位置和返回位置使用impl Trait 。 但是,仍然不允许在Fn语法中的任何地方使用impl Trait ,以允许未来的设计迭代。 此外,不允许手动指定在参数位置使用impl Trait的函数的类型参数。

团队成员@cramertj已提议将其合并。 下一步是由其他标记的团队进行审查:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

目前没有列出任何问题。

一旦大多数审阅者批准(并且没有人反对),这将进入其最终评论期。 如果您发现在此过程中任何时候都没有提出的重大问题,请说出来!

有关标记团队成员可以给我的命令的信息,请参阅此文档

在此稳定之后,将可以在非 trait 函数的参数位置和返回位置使用 impl Trait。 但是,仍然不允许在 Fn 语法中的任何地方使用 impl Trait,以允许未来的设计迭代。 此外,不允许手动指定在参数位置使用 impl Trait 的函数的类型参数。

什么使用的状态impl Trait的特征函数参数/返回岗位,或在FN语法,对于这个问题?

@alexreg Return-position impl Trait in traits 在 RFC 上被阻止,尽管 RFC 2071 将允许实现类似的功能。 我所知道的任何技术特性都没有阻止特征中的参数位置impl Trait ,但它在 RFC 中没有明确允许,因此暂时省略了它。

impl TraitFn语法的参数位置在类型级 HRTB 上被阻止,因为有些人认为T: Fn(impl Trait)应该脱糖为T: for<X: Trait> Fn(X)impl TraitFn语法的返回位置并没有因为我知道的任何技术原因而被阻止,但在 RFC 中它被禁止等待进一步的设计工作——我希望在稳定它之前,请参阅另一个 RFC 或至少一个单独的 FCP。

@cramertj好的,感谢您的更新。 希望我们可以看到这两个没有被任何东西阻止的功能在经过一些讨论后很快得到批准。 脱糖是有意义的,在参数位置,一个参数foo: T其中T: Trait相当于foo: impl Trait ,除非我弄错了。

关注: https :

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm不,目前不可能。 该函数返回! ,它没有实现您提供的特征,我们也没有将其转换为适当类型的机制。 这是不幸的,但现在没有一种简单的方法可以解决它(除了为!实现更多特征之外)。 它还可以向后兼容以在将来修复,因为使其工作将允许严格地编译更多代码。

涡轮鱼的问题只解决了一半。 我们至少应该对有效公共函数的参数中的impl Trait发出警告,考虑到参数中的impl Trait公共检查中新的私有类型的私有类型。

其动机是通过将参数从显式泛型更改为impl Trait来防止库破坏用户的涡轮鱼。 我们还没有一个很好的库参考指南来了解什么是突破性变化,什么不是突破性变化,测试不太可能捕捉到这一点。 这个问题没有得到充分讨论,如果我们希望在完全决定之前稳定下来,我们至少应该把枪从 lib 作者的脚下移开。

动机是通过将参数从显式泛型更改为impl Trait来防止库破坏用户的涡轮鱼。

我希望当这种情况开始发生并且人们开始抱怨 lang-team 时,目前有疑问的人会相信impl Trait应该支持显式地为 turbofish 提供类型参数。

@leodasvacas

涡轮鱼的问题只解决了一半。 我们至少应该对有效公共函数的参数中的 impl Trait 提出警告,考虑到参数中的 impl Trait 是新的 private in public 检查的私有类型。

我不同意 - 这已经解决了。 我们暂时完全禁止涡轮鱼使用这些功能。 将公共函数的签名更改为使用impl Trait而不是显式泛型参数是一项重大更改。

如果我们将来确实允许这些函数使用 turbofish,它可能只允许指定非impl Trait类型的参数。

:bell: 根据上面的评论

我应该补充一点,我不想在https://github.com/rust-lang/rust/pull/49041登陆之前稳定下来。 (但希望那会很快。)

所以#49041 包含对#46541 的修复,但该修复的影响比我预期的要大——例如,编译器现在不引导——而且它让我对这里的正确路线有一定的停顿。 #49041 中的问题是我们可能会意外地让生命周期泄漏,而我们不应该这样做。 这是它在编译器中的表现方式。 我们可能有这样的方法:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

这里的关键是TyCtxt对于'tcx'gcx是不变的,所以它们必须出现在返回类型中。 然而只有'cx'tcx出现在 impl trait bounds 中,所以只有这两个生命周期应该被“捕获”。 旧的编译器之所以接受这一点是因为'gcx: 'cx ,但如果您考虑到我们想到的脱糖,那并不是真正正确的。 脱糖会创建一个像这样的抽象类型:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

然而这个抽象类型的值是Baz<'cx, 'gcx, 'tcx> -- 但'gcx不在范围内!

这里的解决方法是我们必须在边界内命名'gcx 。 这有点烦人; 我们不能使用'cx + 'gcx 。 我们可以假设做一个虚拟特征:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

然后返回类似impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx>

我忘了注意:如果声明的返回类型是dyn Iterator<Item = &'tcx Foo> + 'cx ,那没关系,因为dyn类型预计会擦除生命周期。 因此,我不相信任何不健全可能在这里,假设你什么都做不了问题与impl Trait这是不可能用dyn Trait

人们可以模糊地想象抽象类型的价值是一种类似的存在主义的想法: exists<'gcx> Baz<'cx, 'gcx, 'tcx>

然而,在我看来,稳定一个保守的子集(排除上面的 fns)并在我们决定如何考虑它之后重新审视它作为可能的扩展是可以的。

更新:为了澄清我对dyn特征的含义:我是说他们可以“隐藏”像'gcx这样的一生,只要边界( 'cx ,这里)确保'gcx的任何地方将仍然是活的dyn Trait使用的。

@nikomatsakis这是一个有趣的例子,但我不认为它改变了这里的基本演算,即我们希望仅从返回类型中就可以清楚地看到所有相关的生命周期。

对于这种情况, Captures trait 似乎是一种很好的轻量级方法。 看起来它目前可能会因为不稳定而进入std::marker

@nikomatsakis你的后续评论让我意识到我没有把所有的部分放在一起来理解为什么你可能希望在这种情况下省略'gcx ,即'gcx不是从客户的角度来看的“相关生命周期”。 无论如何,开始保守似乎很好。

我个人的观点是https://github.com/rust-lang/rust/issues/46541并不是一个真正的错误——这是我所期望的行为,我不明白它怎么会变得不健全,解决这个问题很痛苦。 IMO 应该可以返回一个实现Trait并且生命周期超过'a impl Trait + 'a ,无论它包含什么其他生命周期。 但是,如果这是@rust-lang/lang 喜欢的,我可以稳定一个更保守的开始方法。

(要澄清的另一件事:在 #49041 中的修复程序中唯一会出现错误的情况是隐藏类型相对于缺少的生命周期'gcx是不变的,因此这可能相对很少发生。)

@cramertj

我个人的看法是,#46541 并不是一个真正的错误——这是我所期望的行为,我不明白它是如何变得不健全的,而且解决它很痛苦。

我对那个 POV 表示同情,但我不愿意稳定一些我们不明白如何对其脱糖的东西(例如,因为它似乎依赖于一些模糊的存在生命周期概念)。

@rfcbot关注多个返回站点

我想注册一个关于存在 impl trait 的最后一个问题。 在我想使用 impl trait 的大部分时间里,我实际上想要返回多个类型。 例如:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

当然,这在今天是行不通的,而且它肯定超出了让它工作的范围。 然而,方式IMPL特质的作品,现在,我们正在有效地关上了门为它曾经的工作(与语法)。 这是因为 - 目前 - 您可以从多个返回站点累积约束:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

在这里,推断的类型是(i32, bool) ,其中第一个return约束i32部分,第二个return约束bool部分。

这意味着我们永远无法支持两个return语句不统一的情况(如我的第一个示例)——否则这样做会很烦人。

我想知道我们是否应该设置一个约束,要求每个return (通常,约束的每个源)都必须独立地完全指定? (我们在事后统一它们?)

这将使我的第二个例子非法,并为我们在未来某个时候可能支持第一个案例留下空间。

@rfcbot解决多个返回站点

因此,我在#rust-lang 上与@cramertj进行了相当多的impl Trait “提前返回”变得不稳定的想法,以便我们最终改变它。

他们认为最好明确选择加入这种语法,特别是因为在其他情况下(例如let x: impl Trait = if { ... } else { ... } )人们会想要它,我们不能指望隐含地处理它们(绝对不是)。

我觉得这很有说服力。 在此之前,我假设无论如何我们都会有一些选择加入的语法,但我只是想确保我们不会过早关闭任何门。 毕竟,解释何时需要插入“动态垫片”有点棘手。

@nikomatsakis只是我可能不太了解的观点:虽然使函数能够在运行时返回多种类型中的一种可能很有用,但我不愿意对静态返回类型推断使用相同的语法,并且允许在内部需要一些运行时决策的情况(您刚刚称之为“动态填充程序”)。

第一个foo示例,据我了解问题,可以解决(1)盒装+类型擦除Iterator<Item = u32>或(2)总和类型std::option::Iterstd::slice::Iter ,这反过来会派生一个Iterator实现。 尽量保持简短,因为讨论中有一些更新(即,我现在阅读了 IRC 日志)并且越来越难以理解:我当然同意动态 shim 的类似 dyn 的语法,尽管我也理解称它为dyn可能并不理想。

无耻的插件和一个小注释只是为了记录:您可以通过以下方式轻松获得“匿名”总和类型和产品:

  • Coprod: https ://docs.rs/frunk_core/0.0.23/frunk_core/coproduct/trait.CoprodInjector.html
  • HList: https ://docs.rs/frunk_core/0.0.23/frunk_core/hlist/index.html

@Centril是的,来自 frunk 的东西非常酷。 但是,请注意,为了使CoprodInjector::inject工作,结果类型必须是可推断的,如果不命名结果类型(例如-> Coprod!(A, B, C) ),这通常是不可能的。 通常情况下,您正在使用不可命名的类型,因此您需要-> Coprod!(impl Trait, impl Trait, impl Trait) ,这将导致推理失败,因为它不知道哪个变体应该包含哪个impl Trait类型。

@cramertj非常正确(旁注:每个“变体”可能并非完全不可命名,但只是部分,例如: Map<Namable, Unnameable> )。

enum impl Trait想法之前已经在https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695 中讨论过

@Centril是的,这是真的。 我特别想的是期货,我经常写类似的东西

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj我不会说匿名枚举类似于enum impl Trait ,因为我们无法得出结论X: Tr && Y: Tr(X|Y): Tr (反例: Default特征)。 所以图书馆作者需要手动impl Future for (X|Y|Z|...)

@kennytm大概我们想为匿名枚举自动生成一些特征 impl,所以它看起来基本上是相同的功能。

@cramertj由于可以命名匿名枚举(呵呵),如果为(i32|String)生成Default impl,我们将能够编写<(i32|String)>::default() 。 OTOH <enum impl Default>::default()根本不会编译,所以无论我们自动生成什么,它仍然是安全的,因为它根本不能被调用。

尽管如此,在某些情况下,自动生成仍然会导致enum impl Trait 。 考虑

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

这是完全正常的,如果我们有一个mut rng: (XorShiftRng|IsaacRng)我们可以计算rng.next_u32()rng.gen::<u64>() 。 但是, rng.gen_iter::<u16>()无法构造,因为自动生成只能产生(Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) ,而我们真正想要的是Generator<'a, u16, (XorShiftRng|IsaacRng)>

(也许编译器可以像Sized检查一样自动拒绝委托不安全的调用。)

FWIW 这个特性让我觉得在精神上更接近闭包而不是元组(当然,匿名struct对应于假设的匿名enum s)。 这些东西“匿名”的方式是不同的。

对于匿名struct s 和enum s(元组和“分离”),“匿名”是指“结构”(相对于“名义”)类型——它们重新内置,对其组件类型完全通用,并且不是任何源文件中的命名声明。 但是程序员仍然把它们写出来并像任何其他类型一样使用它们,它们的 trait 实现像往常一样显式地写下来,而且它们并不是特别神奇(除了具有内置语法和“可变参数”,其他类型还不能)。 从某种意义上说,它们一个名称,但它们的“名称”不是字母数字,而是用于编写它们的语法(括号和逗号)。

另一方面,闭包是匿名的,因为它们的名字是秘密的。 每次你写一个新的名字时,编译器都会生成一个新的类型,即使你想知道这个名字是什么,也没有办法引用它。 编译器为这个秘密类型实现了一个或两个特征,你可以与之交互的唯一方法是通过这些特征。

能够从if不同分支返回不同的类型,在impl Trait ,似乎更接近后者——编译器隐式生成一个类型来保存不同的分支,实现所请求的特征将其分派给适当的人,而程序员从不写下或查看该类型是什么,也无法引用它,也没有任何真正的理由想要。

(事实上​​,这个特性感觉有点与假设的“对象文字”有关——这对于其他特征来说就像Fn的现有闭包语法一样。也就是说,你不是单个 lambda 表达式,而是'd 使用作用域中的变量实现给定特征的每个方法(其中self是隐式的),编译器将生成一个匿名类型来保存 upvar,并为它实现给定的特征,它会以相同的方式有一个可选的move模式,依此类推。无论如何,我怀疑表达if foo() { (some future) } else { (other future) }的另一种方式是object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (嗯,你还需要将foo()的结果提升到let所以它只会运行一次)。这相当不符合人体工程学,可能不应该被认为是另一个的实际*替代品特征,但它表明存在关系。也许前者可以脱糖成后者,或其他东西。)

@glaebhoerl这是一个非常有趣的想法! 这里还有一些来自 Java 的现有技术。

我的一些想法(所以不是很成熟):

  1. [bikeshed] 前缀object表明这是一个特征对象,而不仅仅是存在的——但事实并非如此。

一种可能的替代语法:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [macros/sugar] 你可以提供一些简单的语法糖,这样你就可以得到:
future!(if foo() { a.poll() } else { b.poll() })

是的,语法问题是一团糟,因为不清楚你是想从struct文字、闭包还是impl块中汲取灵感:) 我刚刚从头顶挑了一个,例如清酒。 (无论如何,我的主要观点不是我们应该去添加对象文字[尽管我们应该],而是我认为匿名enum在这里是一个红鲱鱼[尽管我们也应该添加它们]。)

能够从 if 的不同分支返回不同类型,在 impl Trait 之后,似乎更接近后者——编译器隐式生成一个类型来保存不同的分支,在其上实现请求的 trait 以分派到适当的分支,并且程序员从不写下或查看该类型是什么,也无法引用它,也没有任何真正的理由想要。

唔。 所以我假设我们不会为枚举类型生成“新名称”,而是利用|类型,对应于这样的 impls:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

显然,这会存在连贯性问题,因为多个函数将生成相同的 impl。 但即使把这些放在一边,我现在意识到这个想法可能由于其他原因而行不通——例如,如果有多个关联类型,在某些情况下它们可能必须相同,但在其他情况下允许不同。 例如,也许我们返回:

-> impl IntoIterator<Item = Y>

但我们做的其他地方

-> impl IntoIterator<IntoIter = X, Item = Y>

我猜这将是两个重叠的实现,不能“合并”; 好吧,也许有专业化。

无论如何,我想“秘密枚举”的概念似乎更清晰。

我想注册一个关于存在 impl trait 的最后一个问题。 在我想使用 impl trait 的大部分时间里,我实际上想要返回多个类型。

@nikomatsakis :公平地说,在这种情况下,返回的内容更接近dyn Trait不是impl Trait ,因为合成/匿名返回值实现了类似于动态调度的东西?

cc https://github.com/rust-lang/rust/issues/49288 ,我最近在使用Future s 和Future -returning trait 方法时遇到了很多问题。

由于这是 FCP 关闭之前的最后一次机会,我想对自动自动特征提出最后一个论点。 我意识到这是最后一刻,所以在我们承诺当前的实施之前,我最多想正式解决这个问题。

为了澄清任何没有关注impl Trait ,这就是我要提出的问题。 当前,由impl X类型表示的类型自动实现自动特征,当且仅当它们背后的具体类型实现所述自动特征时。 具体来说,如果进行以下代码更改,该函数将继续编译,但任何依赖于它返回的类型实现Send的函数的使用都将失败。

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(更简单的例子:工作内部变化导致失败

这个问题不是很明确。 有一个非常慎重的决定让自动特征“泄漏”:如果我们不这样做,我们将不得不在每个返回非发送或非同步的函数上放置+ !Send + !Sync ,我们会与潜在的其他自定义自动特征有一个不清楚的故事,这些自动特征根本无法在函数返回的具体类型上实现。 这是我稍后会谈到的两个问题。

首先,我想简单说明我对这个问题的反对意见:这允许更改函数体以更改面向公众的 API。 这直接降低了代码的可维护性。

在 rust 的整个开发过程中,已经做出了在冗长而不是可用性方面犯错误的决定。 新人看到这些,以为是为了啰嗦而啰嗦,其实不然。 每个决定,无论是不让结构自动实现 Copy,还是在函数签名处明确所有类型,都是为了可维护性。

当我向人们介绍 Rust 时,当然,我可以向他们展示速度、生产力和内存安全性。 但是go有速度。 Ada 具有内存安全性。 Python 有生产力。 Rust 胜过所有这些,它具有可维护性。 当库作者想要更改算法以提高效率时,或者当他们想要重做 crate 的结构时,编译器会向他们保证,当他们出错时,编译器会告诉他们。 在 rust 中,我可以放心,我的代码将继续发挥作用,不仅在内存安全方面,而且在逻辑和接口方面也是如此。 _Rust 中的每个函数接口都可以完全由函数的类型声明来表示_。

稳定impl Trait很有可能违背这一信念。 当然,为了快速编写代码,它非常好,但如果我想制作原型,我会使用 python。 当需要长期可维护性而不是短期只写代码时,Rust 是首选语言。


我说这里只有“很大的机会”会变坏,因为这个问题也不是很明确。 首先,“自动特征”的整个想法是不明确的。 Send 和 Sync 是基于结构的内容实现的,而不是公共声明。 由于这个决定适用于生锈,因此类似的行为impl Trait也可以很好地发挥作用。

但是,函数和结构在代码库中的使用方式不同,而且它们不是同一个问题。

当修改结构的字段,甚至是私有字段时,会立即清楚地改变它的真实内容。 具有非发送或非同步字段的结构做出了该选择,并且库维护人员知道在 PR 更改结构的字段时要仔细检查。

在修改函数的内部时,很明显会影响性能和正确性。 然而,在 Rust 中,我们不需要检查我们是否返回了正确的类型。 函数声明是我们必须坚持的硬性契约, rustc小心翼翼。 这是结构和函数返回的自动特征之间的一条细线,但更改函数的内部结构更加常规。 一旦我们有了完整的生成器驱动的Future ,修改返回-> impl Future函数将更加常规。 如果编译器没有捕捉到修改的发送/同步实现,这些都将是作者需要筛选的更改。

为了解决这个问题,我们可以决定这是一个可接受的维护负担,就像最初的 RFC 讨论所做的那样保守的 impl trait RFC 中的这一部分列出了泄漏自动特征的最大论据(“OIBIT”是自动特征的旧名称)。

我已经列出了我对此的主要回应,但这是最后一点。 改变结构的布局并不常见。 可以防备。 确保函数继续实现相同的自动特征的维护负担比结构更大,这仅仅是因为函数变化更多。


最后一点,我想说自动自动特征不是唯一的选择。 这是我们选择的选项,但选择退出自动特征的替代方案仍然是替代方案。

我们可以要求返回非发送/非同步项目的函数以状态+ !Send + !Sync或返回具有这些界限的特征(可能是别名?)。 这不是一个好的决定,但它可能比我们目前选择的要好。

至于对自定义自动特征的关注,我认为任何新的自动特征都不应仅针对在自动特征之后引入的新类型实现。 这可能会提供比我现在可以解决的更多问题,但这不是我们无法通过更多设计解决的问题。


这已经很晚了,而且很啰嗦,我敢肯定我以前曾提出过这些反对意见。 我很高兴能够最后一次发表评论,并确保我们对我们正在做出的决定完全满意。

感谢您的阅读,我希望最终的决定能让 Rust 朝着最好的方向前进。

扩展@daboross的评论。 特征别名,可以改善非泄漏自动特征的人体工程学,如下所示:

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

这还不错——你必须想出一个好名字( FutureNSS不是)。 主要好处是它减少了因重复边界而导致的剪纸。

是否有可能通过明确声明自动特征的要求来稳定此功能,然后一旦我们找到该维护问题的合适解决方案,或者一旦我们足够确定实际上没有维护负担,以后可能会删除这些要求决定取消要求?

有关需要什么Send ,除非它被标记为!Send ,但不提供Sync除非被标记为同步? 与同步相比,发送不应该更常见吗?

像这样:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

参数位置的impl Trait和返回位置 wrt 之间是否存在不一致。 自动特征?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

这对于参数位置是有意义的,因为如果允许您在此处假设 Send,您将收到后单态化错误。 当然,这里的返回位置和参数位置的规则并不要求重合,但它在可学习性方面存在问题。

至于对自定义自动特征的关注,我认为任何新的自动特征都不应仅针对在自动特征之后引入的新类型实现。

那么,这真正的为即将到来的汽车特质Unpin (只未implmented自我指涉发电机),但是......这似乎是好运气? 这是我们真的可以忍受的限制吗? 我不敢相信将来不会有需要禁用的东西,例如&mutRc ...

我相信这已经被讨论过了,当然这已经很晚了,但我仍然对impl Trait的论点不满意。

a) 按值处理闭包/futures 的能力,以及 b) 将某些类型视为“输出”,因此实现细节是惯用的,并且在 1.0 之前就已经存在,因为它们直接支持 Rust 的核心价值:性能、稳定性、和安全。

-> impl Trait只是履行了 1.0 的承诺,或者删除了一个边缘情况,或者概括了现有的特性:它向函数添加了输出类型,采用了一直用于处理匿名类型的相同机制并应用它在更多情况下。 从abstract type开始可能更有原则,即模块的输出类型,但鉴于 Rust 没有 ML 模块系统,因此顺序不是什么大问题。

相反, fn f(t: impl Trait)感觉就像是“仅仅因为我们可以”而添加的,使语言变得更大、更陌生,却没有给予足够的回报。 我一直在努力并未能找到一些现有的框架来适应它。 我理解围绕fn f(f: impl Fn(...) -> ...)简洁性的论点,以及边界已经可以在<T: Trait>where子句中的理由,但那些感觉是空洞的。 他们并没有否定缺点:

  • 您现在必须学习两种边界语法——至少<> / where共享一个语法。

    • 这也造成了学习悬崖,并模糊了在多个地方使用相同泛型类型的想法。

    • 新的语法使得判断一个函数是通用的变得更加困难——你必须扫描整个参数列表。

  • 现在应该是函数的实现细节(它如何声明其类型参数)成为其接口的一部分,因为您不能编写它的类型!

    • 这也与目前正在讨论的自动特征复杂性有关——进一步混淆了函数的公共接口是什么,什么不是。

  • 老实说,与dyn Trait的类比是错误的:

    • dyn Trait总是意味着同样的事情,并且除了通过现有的自动特征机制之外,不会“感染”其周围的声明。

    • dyn Trait可用于数据结构,这实际上是它的主要用例之一。 如果不查看数据结构的所有用途,数据结构中的impl Trait毫无意义。

    • dyn Trait含义是类型擦除,但impl Trait并不暗示它的实现。

    • 如果我们引入非单态泛型,前一点会更加混乱。 事实上,在这种情况下, fn f(t: impl Trait)可能 a) 不适用于新功能,和/或 b) 需要更多的边缘案例律师,例如自动特征问题。 想象一下fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :尖叫:

所以对我来说归结为参数位置的impl Trait增加了边缘情况,使用了更多的陌生预算,使语言感觉更大等等,而返回位置的impl Trait统一,简化,并使语言更紧密地结合在一起。

除非标记为 !Send,否则要求 Send,但除非标记为 Sync,否则不提供 Sync 怎么样? 与同步相比,发送不应该更常见吗?

这感觉非常……随意和临时。 也许打字更少,但记忆更多,混乱更多。

自行车棚的想法在这里,以免分散我上面的观点:而不是impl ,使用type ? 那是用于关联类型的关键字,它可能是用于abstract type的关键字(之一),它仍然相当自然,并且它更多地暗示了“函数的输出类型”的想法:

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

感谢您的阅读,我希望最终的决定能让 Rust 朝着最好的方向前进。

我很欣赏这个精心编写的反对意见。 正如您所指出的,自动特征一直是“公开”一些人们可能希望保持隐藏的实现细节的深思熟虑的选择。 我认为——到目前为止——这个选择实际上效果很好,但我承认我一直对此感到紧张。

这在我看来,重要的问题是,其功能确实从结构不同的程度:

改变结构的布局并不常见。 可以防备。 确保函数继续实现相同的自动特征的维护负担比结构更大,这仅仅是因为函数变化更多。

真的很难知道这将是多么真实。 似乎一般规则是引入Rc时要谨慎行事——这与存储它的位置无关。 (实际上,我真正工作的案例不是Rc而是引入dyn Trait ,因为这可能不太明显。)

我强烈怀疑在返回期货的代码中,使用非线程安全类型等将很少见。 您将倾向于避免使用这些类型的库。 (当然,在现实场景中进行代码测试总是值得的。)

无论如何,这令人沮丧,因为无论我们给它多长时间的稳定期,这种事情都很难提前知道。

最后一点,我想说自动自动特征不是唯一的选择。 这是我们选择的选项,但选择退出自动特征的替代方案仍然是替代方案。

是的,尽管我对“挑出”特定的汽车特性如Send的想法肯定感到紧张。 还要记住,除了期货之外,还有其他 impl trait 的用例。 例如,返回迭代器或闭包——在这些情况下,默认情况下您希望发送或同步并不明显。 无论如何,您真正想要的,以及我们试图推迟的 =),是一种“条件”绑定(如果T是发送,则发送)。 这正是自动特征给你的。

@rpjohnst

我相信这已经讨论过了

事实上,自从多年前的第一个impl Trait RFC lo 以来,它就已经 :) 了。 (哇,2014 年。我觉得自己老了。)

我一直在努力并未能找到一些现有的框架来适应它。

我感觉完全相反。 对我来说,在参数位置没有impl Trait时,返回位置的impl Trait更加突出。 我看到的统一线程是:

  • impl Trait -- 它出现的地方,它表示将有“一些实现Trait单态类型”。 (谁指定该类型的问题——调用者或被调用者——取决于impl Trait出现的位置。)
  • dyn Trait -- 它出现的地方,它表明会有一些类型实现Trait ,但是类型的选择是动态的。

基于这种直觉,还计划扩展impl Trait可以出现的位置。 例如, https://github.com/rust-lang/rfcs/pull/2071允许

let x: impl Trait = ...;

同样的原则也适用:类型的选择是静态已知的。 类似地,同一个 RFC 引入了abstract typeimpl Trait可以理解为一种语法糖),它可以出现在 trait impls 中,甚至可以作为模块中的成员出现。

自行车棚的想法在这里,以免分散我上面的观点:而不是impl ,使用type

就个人而言,我不倾向于在这里重新建立自行车棚。 我们花了很多时间在https://github.com/rust-lang/rfcs/pull/2071和其他地方讨论语法。 似乎没有“完美的关键字”,但是将impl读作“实现的某种类型”在 imo 中效果很好。

让我补充一点关于自动特征泄漏的信息:

首先,最终我认为自动特征泄漏实际上是正确的做法,正是因为它与语言的其余部分一致。 正如我之前所说,汽车特性一直是一场赌博,但它们似乎已经基本上得到了回报。 我只是不认为impl Trait有那么大的不同。

但是,我也很担心在这里耽搁。 我同意设计空间中还有其他有趣的点,我不是 100% 确信我们已经找到了正确的位置,但我不知道我们是否能确定这一点。 我很担心如果我们现在推迟,我们将很难实现我们今年的路线图。

最后,让我们考虑一下如果我错了的含义:我们在这里基本上谈论的是 semver 变得更加难以判断。 我认为这是一个问题,但可以通过多种方式加以缓解。 例如,我们可以使用 lints 在引入!Send!Sync类型时发出警告。 我们长期以来一直在讨论引入一个 semver 检查器来帮助您防止意外的 semver 违规——这似乎是另一个有用的案例。 简而言之,一个问题,但我认为不是关键问题。

所以 - 至少在这一刻 - 我仍然倾向于继续目前的道路。

就个人而言,我不倾向于在这里重新建立自行车棚。

我也不是很投入。 根据我的印象,这是事后的想法,即参数位置中的impl Trait似乎是由句法上而不是语义的“填补漏洞”所激发的,考虑到您的回答,这似乎是正确的。 :)

对我来说,在参数位置没有impl Trait时,返回位置的impl Trait更加突出。

考虑到关联类型的类比,这很像“在参数位置没有type T ,关联类型更加突出”。 我怀疑没有出现特别的反对意见,因为我们选择的语法让人觉得很荒谬——现有的语法已经足够好了,没有人觉得需要像trait Trait<type SomeAssociatedType>这样的语法糖。

我们已经有了“一些实现Trait单态类型”的语法。 在特征的情况下,我们有“调用者”和“被调用者”指定的变体。 在函数的情况下,我们只有调用者指定的变体,因此我们需要被调用者指定的变体的新语法。

将这种新语法扩展到局部变量可能是合理的,因为这也是一种非常类似于关联类型的情况——它是一种隐藏+命名表达式的输出类型的方法,并且对于转发被调用函数的输出类型很有用。

就像我在之前的评论中提到的那样,我也是abstract type的粉丝。 同样,它只是将“输出类型”概念扩展到模块。 并且将-> impl Traitlet x: impl Traitabstract type对 trait impls 关联类型的推理使用也很棒。

特别是为我不喜欢的函数参数添加这种新语法的概念。 它不会做同样的事情,任何的它与被拉的其他功能。 它确实与我们已经拥有的语法做同样的事情,只是有更多的边缘情况和更少的适用性。 :/

@nikomatsakis

真的很难知道这将是多么真实。

在我看来,我们应该在保守的一边犯错吗? 我们能否通过更多时间获得对设计的更多信心(通过让自动特征泄漏在单独的功能门下,并且只在夜间,而我们稳定其余的impl Trait )? 如果我们现在不泄漏,我们以后可以随时添加对自动特征泄漏的支持..

但是,我也很担心在这里耽搁。 [..] 我很担心如果我们现在推迟,我们将很难实现我们今年的路线图。

可以理解! 但是,我相信您已经考虑过,这里的决定将伴随我们多年。

例如,我们可以使用 lints 在引入!Send!Sync类型时发出警告。 我们长期以来一直在讨论引入一个 semver 检查器来帮助您防止意外的 semver 违规——这似乎是另一个有用的案例。 简而言之,一个问题,但我认为不是关键问题。

这很好听! 🎉 我认为这主要缓解了我的担忧。

是的,尽管我对“挑出”特定的汽车特性如Send的想法肯定感到紧张。

我非常同意这种观点👍。

无论如何,您真正想要的,以及我们试图推迟的 =),是一种“条件”绑定(如果T是发送,则发送)。 这正是自动特征给你的。

如果代码明确说明这一点,我觉得T: Send => Foo<T>: Send会更好地理解。

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

但是,正如我们在 WG-Traits 中讨论的那样,您可能根本无法在此处进行推断,因此如果您想要Send以外的其他内容,则始终必须指定Extra Send ,这将是一个彻头彻尾的麻烦.

@rpjohnst

老实说,与dyn Trait的类比是错误的:

对于参数位置的impl Trait ,它是错误的,但对于-> impl Trait不是这样,因为两者都是存在类型。

  • 现在应该是函数的实现细节(它如何声明其类型参数)成为其接口的一部分,因为您不能编写它的类型!

我想指出,由于 turbofish,类型参数的顺序从来都不是实现细节,在这方面,我认为impl Trait可以提供帮助,因为它允许您在 turbofish 中未指定某些类型参数.

[..] 现有的语法足够好,没有人觉得需要像 trait Trait 这样的语法糖.

永不说永不? https://github.com/rust-lang/rfcs/issues/2274

@nikomatsakis一样,我非常感谢这些最后一分钟的评论所采取的谨慎


@daboross ,我想进一步深入研究选择退出的想法。 乍一看,这似乎很有希望,因为它可以让我们充分说明签名,但默认情况下会使常见情况简洁。

不幸的是,一旦你开始看大局,它就会遇到一些问题:

  • 如果自动特征被视为impl Trait选择退出,它们也应该是dyn Trait
  • 即使在参数位置使用这些结构时,这当然也适用。
  • 但是,泛型的行为不同会很奇怪。 换句话说,对于fn foo<T>(t: T) ,默认情况下您可以合理地期望T: Send
  • 我们当然有一个机制,目前仅适用于Sized ; 这是默认情况下在任何地方都假定的特征,您可以通过编写?Sized选择退出

?Sized机制仍然是 Rust 最晦涩难懂的方面之一,我们通常非常讨厌将其扩展到其他概念。 将它用于像Send这样的核心概念似乎是有风险的——当然,更不用说这将是一个重大的突破性变化。

不过,更重要的是:我们真的不想为泛型添加自动特征假设,因为今天泛型的部分优点在于,您可以有效地类型是否实现自动特征进行泛型,并且只拥有该信息“流过”。 例如,考虑fn f<T>(t: T) -> Option<T> 。 我们可以传入T而不管它是否是Send ,并且输出将是Send iff T是。 这是 Rust 中泛型故事的一个非常重要的部分。

dyn Trait也有问题。 特别是,由于单独编译,我们不得不将这种“退出”性质限制为“众所周知的”自动特征,如SendSync ; 这可能意味着永远不会稳定auto trait以供外部使用。

最后,值得重申的是,当您创建新类型包装器以返回不透明类型时,“泄漏”设计是根据今天发生的情况明确建模的。 从根本上说,我认为“泄漏”首先是汽车特征的固有方面。 它有权衡,但它是该功能的核心,我认为我们应该争取新的功能与之交互。


@rpjohnst

在对上面的 RFC 和@nikomatsakis的摘要评论进行了广泛讨论之后,我没有太多要补充的论点立场问题。

现在应该是函数的实现细节(它如何声明其类型参数)成为其接口的一部分,因为您不能编写它的类型!

我不明白你这是什么意思。 可以扩展吗?

我还想注意以下短语:

fn f(t: impl Trait) 感觉就像是“仅仅因为我们可以”而添加的

破坏善意的讨论(我之所以这么说是因为它是一种重复的模式)。 RFC竭尽全力激发该功能并反驳您在这里提出的一些论点——当然,更不用说关于线程的讨论,以及在 RFC 的先前迭代等中。

权衡是存在的,确实存在不利因素,但这并不能帮助我们对漫画“另一面”的辩论得出一个合理的结论。

感谢大家的详细评论! 我真的超级兴奋终于在稳定版上发布了impl Trait ,所以我非常偏向于当前的实现和导致它的设计决策。 也就是说,我会尽我最大的努力尽可能公正地做出回应,并像从零开始一样考虑事情:

auto Trait泄漏

auto Trait泄漏的想法困扰了我很长时间——在某些方面,它似乎与 Rust 的许多设计目标背道而驰。 与它的祖先(例如 C++ 或 ML 家族)相比,Rust 的不同之处在于它需要在函数声明中明确声明泛型边界。 在我看来,这使得 Rust 的泛型函​​数更易于阅读和理解,并且在进行向后不兼容的更改时相对清晰。 我们在处理const fn方法中延续了这种模式,要求函数明确地将自己指定为const而不是从函数体中推断const 。 与显式 trait bound 非常相似,这可以更容易地判断哪些函数可以以何种方式使用,并让库作者相信小的实现更改不会破坏用户。

也就是说,我在自己的项目中广泛使用了 return-position impl Trait ,包括我在 Fuchsia 操作系统上的工作,我相信自动特征泄漏是正确的默认设置。 实际上,消除泄漏的结果是我必须返回并添加+ Send到基本上每个impl Trait使用过的函数我曾经编写过。 负边界(需要+ !Send )对我来说是一个有趣的想法,但是我会在几乎所有相同的函数上编写+ !Unpin 。 明确性在告知用户决策或使代码更易于理解时很有帮助。 在这种情况下,我认为这两者都不会。

SendSync是用户程序的“上下文”:我编写同时使用Send!Send类型的应用程序或库是非常罕见的(尤其是在编写要在中央执行器上运行的异步代码时,该执行器要么是多线程的,要么不是多线程的)。 选择是否线程安全是编写应用程序时必须做出的首要选择之一,从那时起,选择线程安全意味着我的所有类型都必须是Send 。 对于库,我几乎总是喜欢Send类型,因为不使用它们通常意味着我的库在线程上下文中使用时不可用(或需要创建专用线程)。 在现代 CPU 上使用时,毫无争议的parking_lot::Mutex将具有与RefCell几乎相同的性能,因此我认为没有任何动机推动用户为!Send使用专门的库功能 -案例。 由于这些原因,我认为能够在函数签名级别区分Send!Send类型并不重要,而且我认为这不会是司空见惯的库作者不小心将!Send类型引入impl Trait类型,这些类型以前是Send 。 确实,这种选择伴随着可读性和清晰度的成本,但我相信为了符合人体工程学和可用性的好处,这种权衡是非常值得的。

参数位置impl Trait

在这里我没有太多要说的,除了每次我到达参数位置impl Trait ,我发现它大大提高了我的函数签名的可读性和整体愉悦性。 确实,它并没有添加在今天的 Rust 中不可能实现的新功能,但它对于复杂的函数签名来说是一个很好的生活质量改进,它在概念上与 return-position impl Trait很好地配对,它简化了 OOP 程序员向快乐 Rustaceans 的过渡。 目前,有很多冗余的周围有刚推出一个名为泛型类型提供了约束(如Ffn foo<F>(x: F) where F: FnOnce()fn foo(x: impl FnOnce()) )。 此更改解决了该问题并导致更易于阅读和编写的函数签名,并且 IMO 感觉与-> impl Trait很自然。

TL; DR:我认为我们最初的决定是正确的,尽管它们无疑需要权衡取舍。
我非常感谢每个人的发言并投入大量时间和精力来确保 Rust 是最好的语言。

@Centril

关于参数位置的 impl Trait 它是错误的,但对于 -> impl Trait 则不是这样,因为两者都是存在类型。

是的,这就是我的意思。

@aturon

像......这样的短语破坏了善意的讨论

你是对的,为此道歉。 我相信我在其他地方更清楚地表达了我的观点。

现在应该是函数的实现细节(它如何声明其类型参数)成为其接口的一部分,因为您不能编写它的类型!

我不明白你这是什么意思。 可以扩展吗?

在参数位置支持impl Trait后,您可以通过两种方式编写此函数:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

形式的选择决定了 API 使用者是否甚至可以写下任何特定实例的名称(例如获取其地址)。 impl Trait变体不允许您这样做,并且如果不重写签名以使用<T>语法,则无法始终解决此问题。 此外,迁移到<T>语法是一个重大变化!

冒着进一步讽刺的风险,这样做的动机是更容易教授、学习和使用。 但是,因为两者之间的选择也是函数接口的主要部分,就像类型参数顺序一样,我不觉得这已经得到充分解决——我实际上并不反对它更容易使用或它导致更愉快的功能签名。

我不确定我们的任何其他“简单但有限 -> 复杂但一般”的变化,由可学习性/人体工程学驱动,以这种方式涉及破坏界面的变化。 简单方式的复杂等价物的行为相同,并且您只需要在您已经更改界面或行为时进行切换(例如,终身省略、匹配人体工程学、 -> impl Trait ),或者更改同样通用且旨在被普遍应用(例如模块/路径、带内生命周期、 dyn Trait )。

更具体地说,我担心我们会开始在库中遇到这个问题,这很像“每个人都需要记住导出Copy / Clone ”,但更糟糕的是因为 a)这将是一个突破性的变化,并且 b) 总会有一种压力要向后退,特别是因为这就是该功能的设计目的!

@cramertj就函数签名冗余而言……我们可以通过其他方式摆脱它吗? 带内生命周期能够在没有反向引用的情况下逃脱; 也许我们可以以某种方式做“带内类型参数”的道德等价物。 或者换句话说,“改变同样普遍,旨在普遍适用。”

@rpjohnst

此外,迁移到<T>语法是一个重大变化!

不一定,使用https://github.com/rust-lang/rfcs/pull/2176您可以在末尾添加一个额外的类型参数T: Trait并且 turbofish 仍然可以工作(除非您指的是破损除了turbofish-breakage之外的其他一些方法)。

impl Trait 变体不允许您这样做,并且如果不重写签名以使用<T>语法,就无法始终解决此问题。 此外,迁移到<T>语法是一个重大变化!

另外,我认为您的意思是<T>语法迁移是一项重大更改(因为调用者不能再使用 turbofish 显式指定T值)。

更新:请注意,如果一个函数使用 impl Trait,那么我们目前根本不允许使用 turbofish——即使它有一些普通的泛型参数。

@nikomatsakis移动明确的语法可能为好,如果老标记有明确的类型参数和隐含的那些混合物的重大更改-任何人谁提供的n类型参数现在需要提供n + 1代替。 这是@Centril的 RFC 旨在解决的案例

更新:请注意,如果一个函数使用 impl Trait,那么我们目前根本不允许使用 turbofish——即使它有一些普通的泛型参数。

这在技术上减少了中断案例的数量,但另一方面它增加了您无法命名特定实例化的案例数量。 :(

@nikomatsakis

感谢您真诚地解决这个问题。

我仍然不愿意说自动特征泄漏是_正确的_解决方案,但我同意我们在事后才能真正知道什么是最好的。

我主要考虑了 Futures 用例,但这并不是唯一的用例。 如果不从本地类型泄漏 Send/Sync,那么在许多不同的上下文中使用impl Trait并不是一个好故事。 鉴于此,并考虑到额外的汽车特性,我的建议并不可行。

我不想单独列出SyncSend和 _only_ 假设它们,因为这有点武断,只最适合 _one_ 用例。 但是,假设所有自动特征的替代方法也不是很好。 每种类型的+ !Unpin + !...听起来都不是一个可行的解决方案。

如果我们再有五年的语言设计来提出一个效果系统和其他我现在不知道的想法,我们可能会提出更好的东西。 但就目前而言,对于 Rust 来说,拥有 100% 的“自动”自动特征似乎是最好的前进道路。

@lfairy

如果旧签名混合了显式类型参数和隐式类型参数,那么迁移到显式语法也可能是一个重大变化——任何提供 n 个类型参数的人现在都需要提供n + 1

这是目前不允许的。 如果你使用impl Trait ,你不会得到任何参数的turbofish(正如我所指出的)。 不过,这并不是一个长期的解决方案,而是一个保守的步骤,以在我们有时间提出一个全面的设计之前回避关于如何进行的分歧。 (而且,正如@rpjohnst 所指出的,它有其自身的缺点。)

我希望看到的设计是 (a) 是接受@centril的 RFC 或类似的东西,(b) 说您可以将 turbofish 用于显式参数(但不是impl Trait类型)。 但是,我们没有这样做,部分原因是我们想知道是否有一个故事可以实现显式参数迁移impl trait。

@lfairy

这是@Centril的 RFC 旨在解决的案例

_[琐事]_顺便说一句,实际上是@nikomatsakis让我注意到部分涡轮鱼可以缓解<T: Trait>impl Trait之间的中断;)这不是 RFC 的目标一切从头开始,但这是一个不错的惊喜。 😄

希望一旦我们在推理、默认值、命名参数等方面获得更多信心,我们也可以拥有部分涡轮鱼,最终™。

最后的评论期现已结束。

如果这是在 1.26 中发布的,那么https://github.com/rust-lang/rust/issues/49373对我来说似乎非常重要, FutureIterator是两个主要用途-cases,它们都非常依赖于了解相关的类型。

在问题跟踪器中快速搜索,#47715 是仍然需要修复的 ICE。 我们能在它进入稳定状态之前得到它吗?

我今天遇到的 impl Trait 问题:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

看起来impl Traitunimplemented!()不兼容 - 这是一个已知问题吗?

是的,请参阅 #36375 和 #44923

我刚刚意识到 RFC 1951 的假设 2违背了我计划使用的带有异步块的impl Trait一些用途。 具体来说,如果您采用通用AsRefInto参数来获得更符合人体工程学的 API,然后在返回async块之前将其转换为某种拥有的类型,您仍然会得到返回的impl Trait类型受该参数中的任何生命周期约束,例如

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

这样你会得到一个error[E0597]: `url` does not live long enough因为get在返回的impl Future包含临时引用的生命周期。 这个例子有点做作,你可以将 url 按值传递给get ,但在实际代码中几乎肯定会出现类似的情况。

据我所知,对此的预期修复是抽象类型,特别是

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

通过添加间接层,您必须显式传递抽象类型所需的泛型类型和生命周期参数。

我想知道是否有可能更简洁的方式来编写这个,或者这最终会导致几乎每个函数都使用抽象类型,而不是裸露的impl Trait返回类型?

因此,如果我理解@cramertj对该问题的评论,您会在HttpClient::get的定义上遇到错误,例如`get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` 。 (因为 RFC 明确指定impl Trait捕获 _all_ 泛型类型参数,这是一个错误,您可以捕获可能包含比您明确声明的生命周期更短的生命周期的类型)。

由此看来,唯一的修复似乎仍然是声明一个名义抽象类型,以允许显式声明捕获哪些类型参数。

实际上,这似乎是一个突破性的变化。 因此,如果要添加这种情况下的错误,最好尽快添加。

编辑:重新阅读评论,我不认为这就是它的意思,所以我仍然对是否有可能在不使用抽象类型的情况下解决这个问题感到困惑。

@Nemo157是的,修复 #42940 可以解决您的生命周期问题,因为您可以指定返回类型应该与借用 self 一样长,而不管Url的生命周期如何。 这绝对是我们想要做出的改变,但我相信这样做是向后兼容的——它不允许返回类型有更短的生命周期,它过度限制了返回类型的使用方式。

例如,以下带有“参数Iter可能寿命不够长”的错误:

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

仅在函数的泛型中包含Iter不足以允许它出现在返回类型中,但目前函数的调用者错误地认为它是。 这绝对是一个错误,应该修复,但我相信它可以向后兼容地修复,并且不应该阻碍稳定。

看来#46541 已完成。 有人可以更新OP吗?

是否有理由选择语法abstract type Foo = ...;而不是type Foo = impl ...; ? 为了语法的一致性,我更喜欢后者,我记得前段时间对此进行了一些讨论,但似乎找不到。

我偏爱type Foo = impl ...;type Foo: ...;abstract似乎是不必要的怪事。

如果我没记错的话,主要问题之一是人们已经学会将type X = Y解释X替换Y ”)。 这不适用于type X = impl Y

我自己更喜欢type X = impl Y因为我的直觉是typelet ,但是......

@alexreg关于RFC 2071的主题有很多讨论。 TL;DR: type Foo = impl Trait;打破了将impl Trait脱糖为某种“更明确”的形式的能力,它打破了人们对类型别名作为一种更智能的句法替换的直觉。

我偏爱类型 Foo = impl ...; 或类型 Foo: ...;,abstract 似乎是一个不必要的怪事

你应该加入我的exists type Foo: Trait;阵营 :wink:

@cramertj嗯。 我刚刚对此进行了一些更新,老实说,我不能说我理解@withoutboats的推理。 这对我来说似乎是最直观的(你有反例吗?)和关于脱糖的一点我只是不明白。 我想我的直觉就像@lnicola。 我也觉得这种语法最适合做https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 之类的事情——这甚至可以在当前语法中完成吗?

exists type Foo: Trait;略有改进,但我仍然会删除exists关键字。 type Foo: Trait;不会打扰我抱怨。 😉 abstract只是多余的/奇怪的,正如@eddyb所说。

@alexreg

这甚至可以用当前的语法来完成吗?

是的,但它更尴尬。 这是我更喜欢= impl Trait语法(以abstract关键字

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

编辑:上面的exists type Foo: (Foo1, Foo2);应该是type Foo = (Foo1, Foo2); 。 对困惑感到抱歉。

@cramertj语法看起来不错。 exists应该是正确的关键字吗?

@cramertj是的,我在想你必须做这样的事情......我认为这是一个更喜欢= impl Trait的好理由! 老实说,如果人们认为对于这里的存在类型(与简单类型别名相比)关于替换的直觉已经足够崩溃,那么为什么不进行以下折衷呢?

exists type Foo = (impl Bar, impl Baz);

(但老实说,我宁愿保持对所有内容都使用单个type关键字的一致性。)

我发现:

exists type Foo: (Foo1, Foo2);

深深的陌生。 在 RHS 不受限制的情况下使用Foo: (Foo1, Foo2)与在语言中其他地方使用Ty: Bound方式不一致。

以下表格对我来说似乎很好:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

我也不想在这里使用abstract作为一个词。

我觉得exists type Foo: (Foo1, Foo2);很奇怪

这对我来说肯定是个错误,我认为应该说type Foo = (Foo1, Foo2);

如果我们在这里用abstract typeexists type来打架,我肯定会支持前者。 主要是因为“抽象”用作形容词。 我可以很容易地在对话中将某些东西称为“抽象类型”,而说我们正在制作“存在类型”感觉很奇怪。

我也更喜欢: Foo + Bar: (Foo, Bar)= Foo + Bar= impl Foo + Bar= (impl Foo, impl Bar+的用法在所有其他地方都可以很好地使用 bounds,而缺少=确实意味着我们无法写出完整的类型。 我们不是在这里创建类型别名,我们正在为我们保证有一定界限但我们不能明确命名的东西命名。


我也仍然喜欢来自https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 的语法建议:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

尽管正如那个线程中提到的那样,这有点太小了,而且不是很明确。

我对(impl Foo, impl Bar)解释一定与你们中的一些人非常不同......对我来说,这意味着该类型是一些存在类型的 2 元组,并且与impl Foo + Bar完全不同。

@alexreg如果这是@cramertj的意图,我仍然会发现:语法很奇怪:

exists type Foo: (Foo1, Foo2);

似乎仍然不清楚它在做什么 - 在任何情况下,边界通常不会指定可能类型的元组,并且很容易混淆Foo: Foo1 + Foo2语法的含义。

= (impl Foo, impl Bar)是一个有趣的想法 - 允许创建具有自身不知道的类型的存在元组会很有趣。 我不认为我们_需要_支持这些,因为我们可以为 impl Fooimpl Bar引入两种存在类型,然后为元组引入第三种类型别名。

@daboross好吧,您正在制作“存在类型” ,而不是“存在类型” ; 这就是类型论中所说的。 但我认为“存在一种类型 Foo ......”这句话在心智模型和类型理论的角度都很好地工作。

我认为我们不需要支持这些,因为我们可以只为impl Fooimpl Bar引入两种存在类型,然后为元组引入第三种类型别名。

这似乎不符合人体工程学......临时工不是那么好海事组织。

@alexreg注意:我不是说impl Bar + Baz;(impl Foo, impl Bar) ,后者显然是2元组。

@daboross

如果这是@cramertj的意图,我仍然会觉得 : 语法很奇怪:

exists type Foo: (Foo1, Foo2);

似乎仍然不清楚它在做什么 - 在任何情况下,边界通常不会指定可能类型的元组,并且很容易混淆 Foo: Foo1 + Foo2 语法的含义。

这可能有点不清楚(不像(impl Foo, impl Bar)那样明确,我会立即直观地理解) - 但我个人认为我不会混淆它Foo1 + Foo2

= (impl Foo, impl Bar) 是一个有趣的想法 - 允许创建具有自身不知道的类型的存在元组会很有趣。 我认为我们不需要支持这些,因为我们可以只为 impl Foo 和 impl Bar 引入两种存在类型,然后为元组引入第三种类型别名。

是的,这是一个早期的提议,我仍然非常喜欢它。 值得注意的是,无论如何都可以使用当前语法来完成,但它需要 3 行代码,这不太符合人体工程学。 我还坚持认为,像... = (impl Foo, impl Bar)这样的语法对用户来说是最清楚的,但我知道这里存在争议。

@Centril 一开始我不这么认为,但它有点模棱两可,然后@daboross似乎是这样解释的,哈哈。 无论如何,很高兴我们已经澄清了这一点。

糟糕,请参阅我对https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340 的编辑。 exists type Foo: (Foo1, Foo2);应该是type Foo = (Foo1, Foo2);

@cramertj啊,现在更有意义了。 反正你不觉得能做到以下几点是最符合人体工学的吗? 即使浏览其他线程,我也没有真正看到反对它的好论据。

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg是的,我确实认为这是最符合人体工程学的语法。

使用 RFC https://github.com/rust-lang/rfcs/pull/2289 ,这就是我重写@cramertj的片段的方式:

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

但是,我认为对于类型别名,不引入exists将有助于保留表达能力,同时不会不必要地使语言的语法更加复杂; 因此,从复杂性预算 POV 来看, impl Iterator似乎比exists更好。 然而,最后一个替代方案并没有真正引入新的语法,而且也是最短的,但很清楚。

总而言之,我认为应该允许以下两种形式(因为它可以在impl Trait和我们已经拥有的关联类型语法的界限下工作):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

编辑:应该使用哪种语法? IMO,clippy 应该毫不含糊地更喜欢Type: Bound语法,因为它可以使用,因为它最符合人体工程学和直接。

我更喜欢type Foo: Trait变体而不是type Foo = impl Trait变体。 它匹配关联的类型语法,这很好,因为它也是包含它的模块的“输出类型”。

impl Trait语法已经用于输入和输出类型,这意味着它可能会给人留下多态模块的印象。 :(

如果impl Trait用于输出类型,那么我可能更喜欢type Foo = impl Trait变体,因为关联的类型语法更多地用于特征(与 ML 签名松散对应),而type Foo = ..语法更适用于具体模块。

@rpjohnst

我更喜欢type Foo: Trait变体而不是type Foo = impl Trait变体。

我同意,应该尽可能使用它; 但是(impl T, impl U)不能直接使用绑定语法的情况呢? 在我看来,引入临时类型别名会损害可读性。

impl块中使用时,仅使用裸type Name: Bound似乎会令人困惑:

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

对于该语法和关键字前缀的当前(?)计划,引入要在impl块中使用的临时类型别名的成本也大得多,这些类型别名现在需要在模块级别导出(并给定一个语义上有意义的名称...),它阻止了在私有模块内定义特征实现的相对常见的模式(至少对我而言)。

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

对比

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157为什么不能同时允许:

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

和:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

我不明白为什么同一功能需要两种语法,仅使用type Name = impl Bound;语法为这两个部分显式提供名称仍然是可能的:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157我同意不需要(也不应该)有两种不同的语法。 我必须说,我一点也不觉得type (没有前缀关键字)令人困惑。

@rpjohnst多态模块到底是什么? :-) 无论如何,我不明白为什么我们应该在关联类型定义之后对语法进行建模,这些定义将特征边界放在类型上。 这与bounds无关。

@alexreg多态模块是具有类型参数的模块,与fn foo(x: impl Trait) 。 它不存在,所以我不希望人们认为它存在。

abstract type编辑:命名功能,不建议使用关键字)与边界有关! 边界是您对类型的唯一了解。 它们和关联类型之间的唯一区别是它们是推断出来的,因为它们通常是不可命名的。

@Nemo157 Foo: Bar语法在其他上下文(关联类型和类型参数的边界)中已经更加熟悉,并且在不引入临时变量的情况下可以使用时更加符合人体工程学和(IMO)清晰。

写作:

type IterDisplay: Iterator<Item: Display>;

似乎更直接。 我想说的,相比

type IterDisplay = impl Iterator<Item = impl Display>;

我认为这只是始终如一地应用我们已经拥有的语法; 所以它并不是真正的新事物。

EDIT2:第一个语法也是我希望它在 rustdoc 中呈现的方式。

从需要关联类型的特征到 impl 也变得非常容易:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

impl Bar语法在您不得不引入临时变量时似乎更好,但它也始终如一地应用语法。

能够使用这两种语法与能够在参数位置使用impl Trait以及具有然后由参数使用的显式类型参数T: Trait并没有太大区别。

EDIT1:事实上,只有一种语法是特殊的大小写,而不是相反。

@rpjohnst我不敢苟同,尽管我应该说它与界限没有明确的关系。

无论如何,我并不反对type Foo: Bar;语法,但看在上帝的份上,让我们摆脱abstract关键字。 在任何情况下, type本身都很清楚。

就个人而言,我觉得使用=impl是一个很好的视觉暗示,推理正在发生。 在浏览较大的文件时,它还可以更容易地发现这些地方。

另外,假设我看到type Iter: Iterator<Item = Foo>我必须先找到Foo并弄清楚它是一种类型还是一种特征,然后才能知道发生了什么。

最后,我认为推理点的视觉线索也将有助于调试推理错误和解释推理错误消息。

所以我确实认为= / impl变体可以解决更多剪纸问题。

@phaylon

另外,假设我看到Iter: Iterator<Item = Foo>我必须先找到Foo并弄清楚它是类型还是特征,然后才能知道发生了什么。

这个我不明白; Item = Foo应该始终是一种类型,因为dyn Foo是稳定的(并且裸特征正在逐步淘汰......)?

@Centril

这个我不明白; 鉴于 dyn Foo 是稳定的(并且裸特征正在逐步淘汰......),现在 Item = Foo 应该始终是一种类型?

是的,但是在提议的impl less 变体中,它可能是带有界限的推断类型或具体类型。 例如Iterator<Item = String>Iterator<Item = Display> 。 我必须知道特征才能知道推理是否正在发生。

编辑:啊,没注意到有人用过: 。 有点我的意思,很容易错过:) 但你说得对,它们是不同的。

编辑2:我确实认为这个问题会在关联类型之外存在。 给定type Foo: (Bar, Baz)你需要知道 Bar 和 Baz 才能知道推理发生在哪里。

@Centril

EDIT1:事实上,只有一种语法是特殊的大小写,而不是相反。

目前只有一种声明 _existential_ 类型的方法, -> impl Trait 。 有两种方法可以声明 _universal_ 类型(参数列表中的T: Trait: impl Trait )。

如果我们有接受通用类型的多态模块,我可以看到一些关于它的论点,但我相信type Name = Type;在模块和特征定义中的当前用法是作为输出类型参数,这应该是一个存在类型。


@phaylon

是的,但是在提议的impl less 变体中,它可能是带有界限的推断类型或具体类型。 例如Iterator<Item = String>Iterator<Item = Display> 。 我必须知道特征才能知道推理是否正在发生。

我相信impl less 变体在所有情况下都将: Bound用于存在类型,因此您可以将Iterator<Item = String>Iterator<Item: Display>作为特征边界,但是Iterator<Item = Display>将是无效声明。

@尼莫157
关于相关的类型案例,您是对的,我的错。 但是(如我的编辑中所述)我认为type Foo: (A, B)仍然存在问题。 因为AB都可以是类型或特征。

我相信这也是选择=一个很好的理由。 :只告诉你一些事情是推断出来的,但没有告诉你是哪一个。 type Foo = (A, impl B)对我来说似乎更清楚。

我还假设使用impl更容易阅读和提供代码片段,因为不需要提供关于什么是特征和什么不是特征的附加上下文。

编辑:一些学分:我的观点是基本相同@alexreg在这里,我只是想,为什么我认为扩大impl是preferrable。

目前只有一种声明存在类型的方法, -> impl Trait 。 有两种方法可以声明通用类型(参数列表中的T: Trait: impl Trait )。

这就是我要说的:PI 为什么普遍量化应该有两种方法,但在其他地方只有一种存在(忽略dyn Trait )?

在我看来,一个用户在学习了语言的不同部分后会去写type Foo: Bound;type Foo = impl Bound;似乎同样可能,而且我不能说一种语法在所有情况下都明显更好; 我很清楚,一种语法对某些事物更好,另一种语法对不同事物更好。

@phaylon

我相信这也是选择 = 的一个很好的理由。 : 只告诉你一些事情是推断出来的,但不会告诉你是哪一个。 type Foo = (A, impl B) 对我来说似乎更清楚。

是的,这可能是另一个很好的理由。 确实需要一些解包才能弄清楚什么是存在量化的——从定义跳到定义。

另一件事是:在该语法下,甚至允许在关联类型绑定中使用:吗? 对我来说,这似乎是一个奇怪的特殊情况,因为在这个提议的语法中,存在类型不能以任何其他方式组合/组合。 我想以下将是使用该语法的最一致的方法:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

使用语法,我(这里也有其他人)愿意,我们可以写这一切都在一个单一的线,而且它是立即清除其中的量化正在发生!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

与上述无关:我们什么时候玩在 nightly 中实现let x: impl Trait ? 我已经有一段时间想念这个功能了。

@alexreg

另一件事是:在该语法下,甚至允许在关联类型绑定中使用:吗?

是的,为什么不; 这将是 rust-lang/rfcs#2289 + type Foo: Bound的自然效果。

你也可以这样做:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril我认为允许两种替代语法是个坏主意。 闻起来像“我们无法决定,所以我们只支持两者”综合症。 看到混合和匹配它们的代码将是一个真正的眼睛!

@Centril我有点喜欢@nikomatsakis在你的 RFC 上顺便说一句,对不起。 宁愿写impl Iterator<Item = impl Baz> 。 很好,很明确。

@alexreg这很公平;

但是(不)幸运的是(取决于你的 POV),我们已经开始了“允许两种替代语法”,在参数位置使用impl Trait ,这样我们就有了Foo: Barimpl Bar工作意味着同样的事情;

它用于通用量化,但impl Trait表示法并不真正关心它在对偶的哪一边; 毕竟,我们没有选择any Traitsome Trait

鉴于我们已经做出了“我们无法决定”和“对偶的一方在语法上无关紧要”的选择,在我看来,在所有地方应用“我们无法决定”似乎是一致的,这样用户就不会得到变成“但我可以在那边写成这样,为什么不在这里?” ;)


PS:

回覆。 impl Iterator<Item = impl Baz>它不能作为 where 子句的绑定; 所以你必须像Iter: Iterator<Item = impl Baz>一样混合它。 您必须允许: Iter = impl Iterator<Item = impl Baz>才能统一工作(也许我们应该?)。

使用: Bound也代替= impl Bound也是显式的,只是更短 ^,-
我认为X = TyX: Ty之间的间距差异使语法清晰。

忽略了我自己的建议,让我们在 RFC 继续这个对话;)

但是(不)幸运的是(取决于您的 POV),我们已经开始了“允许两种替代语法”,其中 impl Trait 在参数位置,这样我们就可以让 Foo: Bar 和 impl Bar 工作以表示相同的东西;

我们做到了,但我相信选择更多是从对称/一致性的角度来看的。 泛型参数严格来说比通用类型 ( impl Trait ) 的参数更强大。 但是我们在返回位置引入了impl Trait ,将它引入参数位置是有意义的。

鉴于我们已经做出了“我们无法决定”和“对偶的一方在语法上无关紧要”的选择,在我看来,在任何地方都应用“我们无法决定”是一致的,这样用户就不会得到变成“但我可以在那边写成这样,为什么不在这里?” ;)

我不确定是否到了我们应该举起手臂说“让我们实施一切”的时候。 这里没有关于收益的明确论据。

PS:

回覆。 impl Iterator<Item = impl Baz>它不能作为 where 子句的绑定; 所以你必须像Iter: Iterator<Item = impl Baz>一样混合它。 您必须允许: Iter = impl Iterator<Item = impl Baz>才能使其统一工作(也许我们应该?)。

我想说我们要么只支持where Iter: Iterator<Item = T>, T: Baz (就像我们现在一样),要么完全支持Iter = impl Iterator<Item = impl Baz> (正如你所建议的那样)。 只允许中途的房子似乎有点逃避。

使用: Bound也代替= impl绑定也是显式的,只是更短 ^,-
我认为X = TyX: Ty之间的间距差异使语法清晰。

它清晰易读,但我认为使用存在类型几乎没有那么清楚/明确。 当由于此语法的限制而必须将定义拆分为多行时,这种情况会更加严重。

忽略了我自己的建议,让我们在 RFC 继续这个对话;)

等等,你的意思是你的 RFC? 据我所知,我认为它与那个和这个都相关。 :-)

等等,你的意思是你的 RFC? 据我所知,我认为它与那个和这个都相关。 :-)

行; 那么让我们在这里继续;

我们做到了,但我相信选择更多是从对称/一致性的角度来看的。 泛型参数严格来说比通用类型 ( impl Trait ) 的参数更强大。 但是我们在返回位置引入了impl Trait ,将它引入参数位置是有意义的。

我的全部观点是关于一致性和对称性。 =P
如果你被允许为存在量化和普遍量化都写impl Trait ,对我来说,你也应该被允许对普遍和存在量化使用Type: Trait是有道理的。

关于表现力,你说前者比后者强,但不一定非得如此; 如果我们希望它们成为 AFAIK,它们可能同样强大(但我绝对不是说我们应该这样做......)。

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

我不确定是否到了我们应该举起手臂说“让我们实施一切”的时候。 这里没有关于收益的明确论据。

我的论点是,让用户惊讶的是“这种语法在其他地方也可以使用,它的含义在这里很清楚,但你不能在这个地方写它”比有两种方法来做这件事的成本更高(无论如何,你们都必须熟悉这些方法) )。 我们用https://github.com/rust-lang/rfcs/pull/2300 (合并)、 https ://github.com/rust-lang/rfcs/pull/2302(PFCP)、 https做了类似的事情

它清晰易读,但我认为使用存在类型几乎没有那么清楚/明确。

在我看来,清晰就足够了; 我不认为 Rust 将其归因于“首先是明确的”并且过于冗长(我确实发现语法在使用过多时会出现)也会因不鼓励使用而付出代价。
(如果您想经常使用某些东西,请给它更简洁的语法... cf ?作为对.unwrap()的贿赂)。

当由于此语法的限制而必须将定义拆分为多行时,这种情况会更加严重。

这个我不明白; 在我看来, Assoc = impl Trait应该比Assoc: Trait更容易导致行拆分,因为前者更长。

我想说我们要么只支持where Iter: Iterator<Item = T>, T: Baz (就像我们现在一样),要么一直支持Iter = impl Iterator<Item = impl Baz> (正如你所建议的那样)。
只允许中途的房子似乎有点逃避。

没错!,让我们不要半途而废/逃避并实施where Iter: Iterator<Item: Baz> ;)

@Centril好的,你赢了我,主要是在对称/一致性论点上。 😉 两种形式的人体工程学也有帮助。 不过,不久之后,重度掉毛是此功能所必需的。

明天将使用我的完整回复进行编辑。

编辑

正如@Centril指出的那样,我们已经支持使用: Trait (绑定)语法的通用类型。 例如

fn foo<T: Trait>(x: T) { ... }

与“适当的”或“具体的”通用类型一起,例如

fn foo(x: impl Trait) { ... }

当然,前者比后者更强大,但后者更明确(并且可以说更清晰),当它是所有需要的时候。 事实上,我坚信我们应该有一个编译器 lint,尽可能支持后一种形式。

现在,我们在函数返回位置也已经有了impl Trait ,它代表一个存在类型。 相关的特征类型在形式上是存在的,并且已经使用了: Trait语法。

这给予了我应该在两个通用类型的锈正确结合形式的存在,并且对存在的类型适当结合形式同样存在(后者仅在目前的特征),我坚信我们的存在应该将对存在类型的正确形式和绑定形式的支持扩展到特征之外。 也就是说,我们应该支持以下一般和关联类型

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

我也支持此评论中建议的编译器 linting 行为,这将大大减少野外常见存在类型的表达变化。

我仍然认为将普遍量化和存在量化混为一谈是一个错误,因此一致性论点对我不起作用。 单个关键字在函数签名中起作用的唯一原因是上下文必然会限制您在每个位置仅使用一种量化形式。 我可以看到一些潜在的糖是你没有相同限制的东西

struct Foo {
    pub foo: impl Display,
}

这是存在或普遍量化的简写吗? 从在函数签名中使用impl Trait得出的直觉,我看不出您如何决定。 如果你真的尝试同时使用它,你很快就会意识到这个位置的匿名全称量化是没有用的,所以它一定是存在量化,但这与函数参数中的impl Trait不一致。

这是两个根本不同的操作,是的,它们都使用 trait bound,但我看不出有任何理由认为有两种方法来声明存在类型会减少新手的困惑。 如果尝试使用type Name: Trait实际上是新手可能会做的事情,那么这可以通过 lint 解决:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

我刚刚想出了你的论点的另一种表述,我会更容易接受,它必须等到我在一台真正的计算机上才能重新阅读 RFC 并发布。

我觉得我还没有足够的 Rust 经验来评论 RFC。 但是,我有兴趣看到这个功能合并到夜间稳定的 Rust 中,以便将它与Rust libp2p一起使用,为Drops of Diamond 分片实现的一部分。 我订阅了这个问题,但是我没有时间跟上所有的评论! 我如何才能在此问题上保持最新状态,而无需浏览评论?

我觉得我还没有足够的 Rust 经验来评论 RFC。 但是,我有兴趣看到这个功能合并到夜间稳定的 Rust 中,以便将它与Rust libp2p一起使用,为Drops of Diamond 分片实现的一部分。 我订阅了这个问题,但是我没有时间跟上所有的评论! 我如何才能在此问题上保持最新状态,而无需浏览评论? 目前看来,我可能只需要不时签到,而不是订阅该问题。 如果我可以通过电子邮件订阅以获取有关这方面的高级新闻,那就太好了。

我仍然认为将普遍量化和存在量化混为一谈是一个错误,因此一致性论点对我不起作用。

作为一般原则,独立于此功能,我发现这种推理方式有问题。

我认为,我们应该从语言是如何,而不是我们多么希望它是根据一些替代的展开历史的接近语言设计。 作为参数位置的通用量化的语法impl Trait是稳定的,因此您不能希望它消失。 即使你认为 X、Y 和 Z 是错误的(我可以在 Rust 的设计中找到很多我个人认为是错误的东西,但我接受并假设它们......),我们现在必须忍受它们,我认为考虑到新功能(使事情保持一致),我们如何使所有内容组合在一起。

在讨论中,我认为 RFC 的整个语料库和原样的语言应该被视为不是公理,而是强有力的论据。


你可以证明(但我不会):

struct Foo {
    pub foo: impl Display,
}

在语义上等价于:

struct Foo<T: Display> {
    pub foo: T,
}

在函数论据推理下。

基本上,给定impl Trait ,您必须考虑“这是类似于返回类型还是类似于参数?” ,这可能很困难。


如果尝试使用type Name: Trait实际上是新手可能会做的事情,那么这可以通过 lint 解决:

我也会 lint,但在另一个方向; 我认为以下方式应该是惯用的:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

好的,我认为RFC 2071暗示并可能已在该问题中讨论过的替代表述,但从未明确说明:

只有_一种方式_来声明存在量化类型: existential type Name: Bound; (使用existential因为这是在 RFC 中指定的,我并不完全反对在此公式下删除关键字)。

还有一个糖用于在当前范围内隐式声明一个未命名的存在量化类型: impl Bound (暂时忽略函数参数中的通用量化糖)。

因此,当前的返回类型使用是一个简单的脱糖:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

扩展到conststaticlet同样是微不足道的。

RFC 中没有提到的一个扩展是:以type Alias = Concrete;语法支持这种糖,所以当你写

type Foo = impl Iterator<Item = impl Display>;

这实际上是糖

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

然后它依赖于类型别名的透明特性来允许当前模块查看Foo并看到它引用了存在类型。

事实上,我坚信我们应该有一个编译器 lint,尽可能支持后一种形式。

我基本上同意@alexreg的评论,但我对 linting 向arg: impl Trait有一些担忧,主要是因为impl Trait不起作用,因此鼓励库中的 semver 破坏性更改的风险使用turbofish(现在,您需要部分turbofish才能使其正常工作)。 因此,在 clippy 中的 linting 感觉不如在类型别名的情况下那么简单(在这种情况下,没有 turbofish 会导致任何问题)。

我主要同意@alexreg的评论,但我对 arg: impl Trait 的

@Centril刚刚和我一起在 IRC 上提出了这个问题,我同意这是一个关于向后兼容性(太容易破坏)的公平点。 当/如果部分turbofish着陆时,我认为应该添加编译器lint,但直到那时。

所以……我们现在已经对命名存在类型的语法进行了很多讨论。 我们是否应该尝试得出一个结论并将其写入 RFC / PR 帖子中,以便有人可以开始实际实施? :-)

就个人而言,一旦我们命名了存在主义,我宁愿使用 lint(如果有的话)远离任何impl Trait任何使用

@rpjohnst好吧,您当然同意我和@Centril关于命名存在的观点......至于在函数参数中从它们中

参数位置中impl Trait上的 RFC 是最新的吗? 如果是这样,是否可以安全地说它的语义是_universal_? 如果是这样:我想哭。 深。

@phaazonimpl Trait Rust 1.26 发行说明

给你的类型理论家的旁注:这不是存在的,仍然是普遍的。 换句话说, impl Trait 在输入位置是通用的,但在输出位置是存在的。

只是为了表达我对此的想法:

  • 我们已经有了类型变量的语法,实际上,任意类型变量实际上有一些用途(即,您经常希望在多个地方使用类型变量,而不是仅仅将它放在一个地方)。
  • 协变存在主义将为我们打开 rank-n 函数的大门,这在没有 trait 的情况下现在很难做到(见这个),而且是 Rust 真正缺少的一个特性。
  • impl Trait很容易被称为“被调用者选择的类型”,因为……因为它是目前唯一使我们能够这样做的语言结构! 调用者选择类型已经可以通过多种结构实现。

我真的认为impl Trait在参数位置当前的决定是一个遗憾。 :哭:

我真的认为参数位置当前决定的 impl Trait 是一个遗憾。 😢

虽然我对此有点心烦意乱,但我当然认为现在最好把时间花在实施let x: impl Trait上!

协变存在主义将为我们打开通往 n 阶函数的大门

我们已经有了它的语法( fn foo(f: impl for<T: Trait> Fn(T)) ),(又名“type HRTB”),但它还没有实现。 fn foo(f: impl Fn(impl Trait))产生一个“不允许嵌套impl Trait ”的错误,我希望当我们得到类型 HRTB 时,我们会希望它表示更高级别的版本。

这类似于Fn(&'_ T)含义for<'a> Fn(&'a T) ,所以我不认为它会引起争议。

查看当前的草稿,参数位置中的impl Trait是一个 _universal_,但您是说impl for<_> Trait将它变成一个 _existential_?! 这有多疯狂?

为什么我们认为我们需要引入_另一种方式_来构建_universal_? 我是说:

fn foo(x: impl MyTrait)

之所以有趣,是因为匿名类型变量在 type 中只出现一次。 如果需要返回相同的类型:

fn foo(x: impl Trait) -> impl Trait

显然不会工作。 我们告诉人们从更通用的习语转变为更严格的没有附加值的功能——我在 RFC 中读到的学习假设是一个奇怪的论点,我们认为而不是新手——他们仍然需要学习东西,那么为什么我们要让语法更加模糊大家是为了降低这里的学习曲线吗? 当人们习惯了普遍性与存在性(来一个,原理很简单,用正确的词)时,人们会开始思考为什么我们有相同的关键字/模式来表达两者以及where和就地模板参数。

啊,我想所有这些都已经被接受了,我在咆哮。 我只是觉得这是一个真正的遗憾。 我很确定我不是唯一一个对 RFC 决定感到失望的人。

(在功能稳定后继续讨论这个问题可能没有多大意义,但请参阅此处以获得一个有说服力的论点(我同意),为什么impl Trait在论点位置具有它所做的语义是明智的,并且连贯。Tl;dr 这与fn foo(arg: Box<Trait>)工作方式与fn foo<T: Trait>(arg: Box<T>)大致相同的原因相同,即使dyn Trait是存在的;现在替换dynimpl 。)

查看当前的草案,参数位置中的impl Trait是通用的,但是您是说impl for<_> Trait将其变成存在的?!

不,它们都是通用的。 我是说排名较高的用途看起来像这样:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

在添加它的同时(即没有更改impl Trait )可以写成:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

这是通用的impl Trait ,只是Trait是 HRTB(类似于impl for<'a> Fn(&'a T) )。
如果我们决定(我预计很可能) Fn(...)参数中的impl Trait也是通用的,你可以这样写来达到同样的效果:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

这就是我认为你所说的“更高级别”的意思,如果你没有,请告诉我。

一个更有趣的决定可能是在存在位置应用相同的处理,即允许这样做(这将意味着“返回一些采用任何其他闭包的闭包”):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

写成这样:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

那将是一个包含通用impl Trait的存在impl Trait impl Trait (绑定到存在,而不是封闭函数)。

@eddyb一般来说,为了一致性和不混淆新手,为存在和普遍量化使用两个单独的关键字不是更有意义吗?
存在量化的关键字是否也可用于存在类型?
为什么我们将impl用于存在(通用)量化,但使用existential用于存在类型?

我想说三点:

  • 讨论impl Trait是存在的还是普遍的并没有多大价值。 那里的大多数程序员可能没有阅读足够多的类型理论手册。 问题应该是人们是否喜欢它,或者他们是否觉得它令人困惑。 要回答这个问题,可以在此线程、 reddit论坛上看到某种形式的反馈。 如果某些事情需要进一步解释,那么它就无法通过直观或非令人惊讶的功能的试金石测试。 所以我们应该看看有多少人,他们有多困惑,以及是否比其他功能有更多的问题。 确实很遗憾,这个反馈是稳定应该对这个现象做点什么,但这是一个单独的讨论。
  • 从技术上讲,即使在稳定之后,在这种情况下有一种方法可以摆脱该功能(如果应该不做决定)。 可以对使用它的编写函数进行 lint,并在下一版本中删除该功能(同时保留调用它们的能力,如果它们来自不同版本的 crate)。 这将满足防锈稳定性保证。
  • 不,增加两个关键字指定生存和普遍的类型不会对混乱的提高,只会使事情变得更糟。

这个反馈是在稳定后到达的,确实很遗憾,应该对这种现象采取一些措施

只要impl Trait是一个想法,就在论点位置存在反对意见。 像这样的反馈_不是新的_,即使在相关的 RFC 线程中也引起了激烈的争论。 不仅从类型论的角度讨论了通用/存在类型,而且还讨论了这将如何让新用户感到困惑。

诚然,我们没有得到真正的新用户的观点,但这并不是凭空而来的。

@Boscop anysome被提议作为一对关键字来完成这项工作,但被决定反对(尽管我不知道理由是否曾在任何地方写下)。

诚然,我们无法从 rust 新手和不是打字理论家的人那里得到反馈

对于列入论点总是,这将使它更容易为新人。 所以如果我们现在来自新人的相关的反馈,而不是争论新人应该如何理解它吗?

我想如果有人有时间,通过论坛和其他地方进行某种研究,可以完成包含前后人们有多困惑(我不太擅长统计,但我很确定有人可以想出比盲目预测更好的东西)。

包容的论点始终是它会让新来者更容易。 所以如果我们现在有来自新人的实际反馈,那不应该是一种非常相关的反馈,而不是争论新人应该如何理解它吗?

是的? 我的意思是我不是在争论发生的事情是好主意还是坏主意。 我只是想指出 RFC 线程确实收到了对此的反馈,而且无论如何都决定了。

正如你所说,在其他地方进行关于反馈的元讨论可能会更好,尽管我不确定那会在哪里。

不,再添加两个关键字来指定存在类型和通用类型不会改善混乱,只会让事情变得更糟。

更差? 怎么会这样? 比起模棱两可/混乱,我更喜欢记住更多。

是的? 我的意思是我不是在争论发生的事情是好主意还是坏主意。 我只是想指出 RFC 线程确实收到了对此的反馈,而且无论如何都决定了。

当然。 但是争论的双方都是老的,伤痕累累的和经验丰富的程序员,他们对幕后发生的事情有着深刻的理解,猜测他们不属于的群体(新人)并猜测未来。 从事实的角度来看,就现实中实际发生的事情而言,这并不比掷骰子好多少。 这不是因为专家经验不足,而是因为没有足够的数据来做决定。

现在它被引入了,我们有一种方法可以获取实际的硬数据,或者可以在从 0 到 10 的范围内获得多少人感到困惑的任何硬数据。

正如你所说,在其他地方进行关于反馈的元讨论可能会更好

例如在这里,我已经开始了这样的讨论,并且可以采取一些实际的步骤,即使是小的步骤: https :

更差? 怎么会这样?

因为,除非不推荐使用impl Trait ,否则您将拥有全部 3 个,因此除了混淆impl Trait消失,情况将有所不同,它将权衡两种方法的利弊。

impl Trait就像在被调用者挑选中一样就足够了。 如果您尝试在参数位置使用它,那么您会引入混淆。 HRTB 将消除这种混乱。

@vorner以前我认为我们应该对 Rust 新手进行实际的 A/B 测试,看看他们发现什么实际上更容易和更难学习,因为作为精通 Rust 的人很难猜测。
FWIW,我记得,当我学习 Rust(来自 C++、D、Java 等)时​​,普遍量化的类型泛型(包括它们的语法)很容易理解(泛型的生命周期有点困难)。
我认为, impl Trait arg的类型将导致从新手的路线,还有许多问题很多混乱的这样
在没有任何证据表明更改会使 Rust 更容易学习的情况下,我们应该避免进行此类更改,而是进行使/保持 Rust 更加一致的更改,因为一致性至少使它易于记忆。 Rust 新手无论如何都必须阅读这本书几次,因此为 args 引入impl Trait以允许将书中的泛型推迟到以后并不会真正消除任何复杂性。

@eddyb顺便说一句,除了impl之外,为什么我们还需要另一个类型的existential关键字? (我希望我们都使用some ..)

FWIW,我记得,当我学习 Rust(来自 C++、D、Java 等)时​​,普遍量化的类型泛型(包括它们的语法)很容易理解(泛型的生命周期有点困难)。

我自己也不认为这是一个问题。 在我现在的公司,我正在领导 Rust 课程——现在我们每周见面一次,我尝试教授实际的实现。 这些人都是经验丰富的程序员,主要来自 Java 和 Scala 背景。 虽然存在一些障碍,但论点位置的泛型(至少阅读它们 - 他们在实际编写它们时会有点小心)不是问题。 关于返回位置的泛型(例如,调用者选择函数返回的内容)有点令人惊讶,尤其是它通常可以被省略,但解释需要大约 2 分钟才能点击。 但我什至不敢在论点位置提及 impl Trait 的存在,因为现在我必须回答它为什么存在的问题——我对此没有真正的答案。 这对动机不利,而拥有动机对于学习过程至关重要。

所以,问题是,社区是否有足够的声音来用一些数据来支持这些论点重新展开辩论?

@eddyb顺便说一句,为什么除了 impl 之外,我们还需要另一个类型的存在关键字? (我希望我们都可以使用一些..)

为什么不forall … /me 慢慢溜走

@phaazon我们有forall (即“通用”),它是for ,例如在 HRTB 中( for<'a> Trait<'a> )。

@eddyb 是的,然后也将它用于存在主义,例如 Haskell 对forall所做的那样。

整个讨论非常自以为是,我有点惊讶的是,争论的想法看起来很稳定。 我希望以后有办法推动另一个 RFC 来撤销它(我完全愿意写它,因为我真的真的很不喜欢这将带来的所有混乱)。

我真的不明白。 让他们处于争论的位置有什么意义? 我写的 Rust 不多,但我真的很喜欢能够做到-> impl Trait 。 我什么时候会在论点位置使用它?

我的理解是,这主要是为了保持一致性。 如果我可以在 fn 签名的参数位置写类型impl Trait ,为什么我不能在别处写呢?

也就是说,我个人宁愿告诉人们“只使用类型参数”......

是的,这是为了一致性。 但是当类型参数如此易于使用时,我不确定它是否是一个足够好的参数。 此外,然后出现的问题是针对/针对哪个 lint!

此外,然后出现的问题是针对/针对哪个 lint!

考虑到你根本不能用impl Trait表达几件事,用impl Trait作为参数之一的函数不能做turbofish,因此你不能取它的地址(我忘了一些其他缺点?),我认为对类型参数进行 lint 处理没有什么意义,因为无论如何你都需要使用它们。

因此你不能取它的地址

您可以通过从签名中推断出来。

让他们处于争论的位置有什么意义?

没有,因为它与使用 trait bound 完全相同。

fn foo(x: impl Debug)

是完全一样的东西

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

另外,考虑一下:

fn foo<A>(x: A) -> A where A: Debug

参数位置中的impl Trait不允许您这样做,因为它是匿名的。 这是一个非常无用的功能,因为我们已经拥有处理这种情况所需的一切。 人们不会轻易学习这个新特性,因为几乎每个人都知道类型变量/模板参数,而 Rust 是使用impl Trait语法的单一语言。 这就是为什么很多人抱怨它应该保留返回值/让绑定,因为它引入了一个新的、需要的语义(即被调用者选择的类型)。

简而言之, @iopq :你不需要这个,除了“让我们添加另一个没有人真正需要的语法糖结构,因为它处理一个非常具体的用途——即匿名类型变量”之外没有其他意义

另外,我忘了说:它让你更难看到你的函数是如何参数化/单态化的。

@Verner对于部分turbofish,为了简单、可读性和明确性,对它进行lint 是很有意义的。 不过,我并不真正支持 arg 位置的功能。

-> impl Trait被调用者选择类型,而在x: impl Trait中调用者选择类型时,它如何保持一致?

我知道它没有其他方法可以工作,但这似乎并不“一致”,似乎与一致相反

我真的同意这一切都是一致的,人们会感到困惑,新来者以及高级熟练的 rustaceans。

我们有两个 RFC,从 2 年多前开始,它们之间总共收到了近 600 条评论,以解决在此线程上重新提出的问题:

  • rust-lang/rfcs#1522 ("最小impl Trait ")
  • rust-lang/rfcs#1951(“确定impl Trait语法和参数范围,同时将其扩展为参数”)

(如果您阅读这些讨论,您会发现我最初是两个关键字方法的强烈支持者。我现在认为使用单个关键字是正确的方法。)

经过 2 年和数百条评论后,做出了决定,该功能现已稳定。 这是该功能的跟踪问题,可以跟踪impl Trait的仍然不稳定的用例。 重新提起impl Trait已解决方面与此跟踪问题无关。 欢迎您继续谈论这个,但请不要在问题跟踪器上。

impl Trait甚至没有在特征中 fns 的参数位置获得支持时,它是如何稳定的?

@daboross那么原帖中的复选框需要打勾!

(只是发现 https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable 目前有效)

我认为https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug不起作用(还)很奇怪,还有这个错误消息。 只有我有这种想法吗? 也许它需要在特征的返回位置添加一个复选框impl Trait ,或者它是一个有意识的决定只允许特征函数的参数位置impl Trait ,强制使用existential type返回类型? (这……对我来说看起来不一致,但也许我错过了一点?)

@Ekleog

是否有意识地决定只允许 impl Trait 在 trait 函数的参数位置,强制使用存在类型作为返回类型?

是的——在特征中返回位置impl Trait被推迟,直到我们有更多在特征中使用存在类型的实际经验。

@cramertj我们还没有足够的实践经验来实现它吗?

在我们添加更多功能之前,我希望在一些稳定版本中看到 impl Trait。

@mark-im 我个人看不到关于特征方法的返回位置impl Trait什么争议......也许我错过了一些东西。

我不认为这是有争议的。 我只是觉得我们添加功能太快了。 暂时停止并专注于技术债务并首先获得当前功能集的经验会很好。

我知道了。 我想我只是认为它是现有功能的缺失部分,而不是新功能。

我认为@alexreg是对的,在特征方法上使用存在主义impl Trait非常诱人。 这并不是一个真正的新功能,但我想在尝试实现它之前有几件事需要解决?

@phaazon也许,是的......我真的不知道与我们今天已经拥有的相比,实施细节会有多少不同,但也许有人可以对此发表评论。 我也很想看到 let/const 绑定的存在类型,但我绝对可以接受它作为这个之外的一个特性,因此在开始之前等待另一个周期左右。

我想知道我们是否可以在特征中保留通用的 impl Trait ...

但是,是的,我想我明白你的意思了。

@mark-im 不,我们不能,它们已经是 stable

它们在函数中,但是 Trait 声明呢?

@mark-im 正如代码片段所示,它们在 impls 和 trait 声明中都是稳定的。

只是为了赶上我们正在解决的问题abstract type 。 就个人而言,我非常赞同@Centril最近提出的语法和最佳实践:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

这适用于我的一些代码,我猜它看起来像:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

对我来说, type Bar = (impl Display,);会很好,但type Bar = impl Display;会很糟糕。

如果我们决定使用不同的替代存在类型语法(都与rfc 2071不同?),那么https://users.rust-lang.org/上的论坛主题会是一个好地方吗?

我现在对启动这样一个线程的替代方案没有足够的了解,但是由于存在类型仍未实现,我认为在论坛上讨论然后一个新的 RFC 可能比在跟踪问题中讨论它更好.

type Foo = impl Trait什么问题?

@daboross可能是内部论坛。 我正在考虑编写一个关于它的 RFC 以最终确定语法。

@daboross关于这个线程的语法已经进行了足够多的讨论。 我认为如果@Centril可以在这一点上为它编写一个 RFC,那就太好了。

有什么问题我可以订阅以讨论特征中的存在主义吗?

一种语法或另一种语法是否有任何与宏相关的参数?

@tomaka在第一种情况下, type Foo = (impl Display,)确实是您拥有的唯一语法。 我偏爱type Foo: Trait不是type Foo = impl Trait只是因为我们绑定了一个我们可以命名的类型,例如<TFoo: Trait>where TFoo: Trait ,而使用impl Trait我们无法命名类型。

澄清一下,我并不是说type Foo = impl Bar不好,我是说type Foo: Bar在简单的情况下更好,部分原因是@KodrAus的动机。

后者我读为:“类型 Foo 满足 Bar”,而前者读为:“类型 Foo 等于某种满足 Bar 的类型”。 因此,在我看来,从外延的角度来看,前者更加直接和自然(“我可以用 Foo 做什么”)。 要理解后者,您需要更深入地理解类型的存在量化。

type Foo: Bar也很简洁,因为如果这是用作 trait 中关联类型的绑定的语法,那么您可以简单地将 trait 中的声明复制到 impl 中,它就会简单地工作(如果这是您要公开的所有信息..)。

语法也更简洁,尤其是在涉及关联类型边界和有许多关联类型时。 这可以减少噪音,因此有助于提高可读性。

@KodrAus

以下是我阅读这些类型定义的方式:

  • type Foo: Trait表示“ Foo是实现Trait
  • type Foo = impl Trait表示“ Foo是实现Trait的某种类型的别名”

对我来说, Foo: Trait只是声明了对Foo实施Trait的约束。 在某种程度上, type Foo: Trait感觉不完整。 看起来我们有一个约束,但缺少Foo的实际定义。

另一方面, impl Trait让人联想到“这是一种单一类型,但编译器会找出它的名称”。 因此, type Foo = impl Trait意味着我们已经有了一个具体类型(它实现了Trait ),其中Foo只是一个别名。

我相信type Foo = impl Trait更清楚地传达了正确的含义: Foo是实现Trait的某种类型的别名。

@stjepang

type Foo: Trait表示“Foo 是一种实现 Trait 的类型”
[..]
在某种程度上, type Foo: Trait感觉不完整。

这也是我阅读它的方式(模措辞......),这是一个扩展正确的解释。 这说明了你可以Foo (类型提供的态射)做什么。 因此,它是外延完备的。 从读者,尤其是初学者的角度来看,我认为扩展性更为重要。

另一方面, impl Trait让人联想到“这是单一类型,但编译器填补了空白”。 因此, type Foo = impl Trait意味着我们已经有了一个具体类型(它实现了Trait ),其中Foo是一个别名,但是编译器会弄清楚它到底是哪种类型。

这是一种更详细和更内涵的解释,它与从外延的角度来看是多余的表征有关。 但这在内涵意义上更完整。

@Centril

从读者,尤其是初学者的角度来看,我认为扩展性更为重要。

这是一种更详细、更内涵的解释,涉及到从外延的角度来看是多余的表征

外延与内涵的二分法很有趣——我以前从来没有这样想过impl Trait

尽管如此,我还是希望对结论有所不同。 FWIW,我从来没有设法在 Haskell 和 Scala 中理解存在类型,所以把我算作初学者。 :) 从第一天开始,Rust 中的impl Trait就感觉非常直观,这可能是因为我认为它是一个受限制的别名,而不是可以对类型做什么。 因此,在了解Foo什么以及可以用它

不过,只是我的2c。 其他人可能有不同的impl Trait心理模型。

我完全同意这个评论type Foo: Trait感觉不完整。 type Foo = impl Trait感觉更类似于在其他地方使用impl Trait ,这有助于使语言感觉更加一致和令人难忘。

@joshtriplett请参阅https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 了解一致性讨论; 我相信允许表单形式实际上是一致的事情。 并且只允许其中一种形式(无论哪种...)是不一致的。 允许type Foo: Trait也特别适合https://github.com/rust-lang/rfcs/pull/2289 ,您可以声明: type Foo: Iterator<Item: Display>;使事情整齐统一。

@stjepang type Foo: Bar;的扩展视角不需要您理解类型论中的存在量化。 您真正需要了解的是Foo允许您执行Bar提供的所有操作,仅此而已。 从Foo的用户的角度来看,这也是所有有趣的地方。

@Centril

我相信我现在了解您来自哪里以及将Type: Trait语法推向尽可能多的地方的吸引力。

:用于类型实现特征边界和=用于类型定义和 type-equals-another-type 边界有很强的含义。

我认为这在您的 RFC 中也很明显。 例如,采用以下两种类型边界:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

这两个界限最终具有相同的效果,但(我认为)略有不同。 前者说“ Item必须实现特征Bar ”,而后者说“ Item必须等于某种实现Bar ”。

让我尝试用另一个例子来说明这个想法:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

那么我们应该如何定义一个实现Person的存在类型呢?

  • type Someone: Person ,看起来像类型绑定。
  • type Someone = impl Person ,它看起来像一个类型定义。

@stjepang看起来像类型绑定并不是一件坏事:) 我们可以像这样实现Person for Alice

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

看啊! trait 和 impl 在{ .. }中的内容是相同的,这意味着就Name而言,您可以从未触及的 trait 中复制文本。

由于关联类型是类型级别的函数(其中第一个参数是Self ),我们可以将类型别名视为 0-arity 关联类型,因此没有什么奇怪的事情发生。

这两个界限最终具有相同的效果,但(我认为)略有不同。前者说“Item 必须实现 trait Bar”,而后者说“Item 必须等于某种实现 Bar 的类型”。

是的;我发现第一个措辞更中肯和自然。 :)

@Centril嘿。 这是否意味着仅type Thing;就足以引入抽象类型?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm我认为这在技术上是可行的; 但是您可以根据对隐式/显式的想法来询问是否可取。 在那种特殊情况下,我认为在技术上写下就足够了:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

并且编译器可以为您推断type Output: Sized; (这是一个非常无趣的界限,不会给您任何信息)。 这是为了更有趣的界限需要考虑的事情,但它不会出现在我最初的提议中,因为我认为它可能会鼓励低可供性 API,即使由于程序员的懒惰,具体类型非常简单:) type Output;也不会

我想在阅读完这一切之后,我倾向于更同意@Centril。 当我看到type Foo = impl Bar我倾向于认为Foo是一种特殊类型,就像其他别名一样。 但事实并非如此。 考虑这个例子:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

恕我直言,在 Displayable 的声明中看到 = 有点奇怪,但是 foo 和 bar 的返回类型不相等(即,这 = 不是传递的,与其他任何地方不同)。 问题是 Foo _not_ 是恰好包含某些 Trait 的特定类型的别名。 换句话说,它在任何上下文中都是单一类型,但对于不同的用途,该类型可能会有所不同,如示例中所示。

一些人提到type Foo: Bar感觉“不完整”。 对我来说这是一件好事。 在某种意义上Foo是不完整的; 我们不知道它是什么,但我们知道它满足Bar

@mark-im

问题是 Foo 不是恰好包含某些 Trait 的特定类型的别名。 换句话说,它在任何上下文中都是单一类型,但对于不同的用途,该类型可能会有所不同,如示例中所示。

哇,真的是这样吗? 这肯定会让我很困惑。

Displayableimpl Display的简写,而不是单一的具体类型,有什么原因吗? 考虑到可以以类似方式使用特征别名(跟踪问题:https://github.com/rust-lang/rust/issues/41517),这种行为是否有用? 例子:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

这不是一个有效的例子。 从RFC 2071 中关于存在类型的参考部分

existential type Foo = impl Debug;

Foo可以在整个模块的多个位置用作i32 。 但是,每个使用Foo作为i32函数必须独立地对Foo施加约束,使其必须是i32

每个存在类型声明必须由至少一个函数体或 const/static 初始化程序约束。 主体或初始化程序必须完全约束或不对给定的存在类型施加约束。

没有直接提到,但 RFC 的其余部分需要的是,存在类型范围内的两个函数不能为该存在类型确定不同的具体类型。 这将是某种形式的冲突类型错误。

我猜你的例子会在bar的回报上给出类似expected type `&'static str` but found type `i32`东西,因为foo已经将Displayable的具体类型设置为&'static str

编辑:除非你是从直觉中得出这个结论的

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

相当于

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

而不是我的期望

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

我猜这两种解释中哪一种是正确的可能取决于@Centril可能正在编写的 RFC。

问题是 Foo 不是恰好包含某些 Trait 的特定类型的别名。

我猜这两种解释中哪一种是正确的可能取决于@Centril可能正在编写的 RFC。

type Displayable = impl Display;存在的原因是它是特定类型的别名。
请参阅https://github.com/rust-lang/rfcs/issues/1738 ,这是此功能解决的问题。

@Nemo157你的期望是正确的。 :)

下列:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

将减少为:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

其中_0_1_2名义上都是不同的类型,因此Id<_0, _1>Id<_0, _2>Id<_1, _2> (以及对称实例)都是无人居住的,其中Idrefl 中定义。

免责声明:我(自愿)不阅读 RFC(但知道它是关于什么的),因此我可以评论语法“直观”的地方。

对于type Foo: Trait语法,我完全希望这样的事情是可能的:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

以与where Foo: Display, Foo: Debug相同的方式当前是可能的。

如果它是不允许的语法,我认为这是语法的问题。

哦,我认为 Rust 的语法越多,学习它就越难。 即使一种语法“更容易学习”,只要这两种语法是必需的,初学者最终还是必须同时学习这两种语法,而且如果他们进入一个已经存在的项目,他们可能迟早会学习。

@Ekleog

对于type Foo: Trait语法,我完全希望这样的事情是可能的:

有可能的。 那些“类型别名”声明关联类型(类型别名可以解释为 0 元类型级别函数,而关联类型是 1+ 元类型级别函数)。 当然,你不能在一个特征中拥有多个具有相同名称的关联类型,这就像尝试在一个模块中定义两个具有相同名称的类型别名一样。 在impltype Foo: Bar也对应于存在量化。

哦,我认为 Rust 的语法越多,学习它就越难。

两种语法都已使用。 type Foo: Bar;在特征中已经是合法的,对于通用量化来说也是Foo: Bar其中Foo是一个类型变量。 impl Trait用于返回位置的存在量化和参数位置的全称量化。 允许这两个插件在语言中的一致性差距。 它们对于不同的场景也是最优的,因此两者都可以为您提供全局最优。

此外,初学者不太可能需要type Foo = (impl Bar, impl Baz); 。 大多数用途可能是type Foo: Bar;

RFC 2071 的原始拉取请求提到了一个typeof关键字,该关键字似乎在本次讨论中完全被忽略了。 我发现当前提出的语法相当隐含,让编译器和任何阅读代码的人都在搜索具体类型。

如果这将是明确的,我更愿意。 所以而不是

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

我们会写

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(带有要回收的 return_type_of 的名称),甚至

fn foo_func() -> impl SomeTrait as Foo { ... }

它甚至不需要新的关键字,并且任何知道 impl Trait 语法的人都可以轻松理解。 后一种语法简洁,所有信息都集中在一个地方。 对于特征,它可能如下所示:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

甚至

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

如果这已经被讨论过并被驳回,我很抱歉,但我找不到它。

@Centril

有可能的。 那些“类型别名”声明关联类型(类型别名可以解释为 0 元类型级别函数,而关联类型是 1+ 元类型级别函数)。 当然,你不能在一个特征中拥有多个具有相同名称的关联类型,这就像尝试在一个模块中定义两个具有相同名称的类型别名一样。 在 impl 中,类型 Foo: Bar 也对应于存在量化。

(对不起,我的意思是把它放在impl Trait for Struct ,而不是放在trait Trait

对不起,我不确定我是否理解。 我想说的是,对我来说,代码就像

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(完整版的游乐场链接)
感觉它应该工作。

因为它只是在Type上设置了两个界限,就像where Type: Debug, Type: Display work 一样

如果不允许这样做(我似乎理解为“当然,一个特征中不能有多个具有相同名称的关联类型”?但是考虑到我在编写trait Trait而不是impl Trait for Struct我不确定),那么我认为这是type Type: Trait语法的问题。

然后,在trait声明中,语法已经是type Type: Trait ,并且不允许多个定义。 所以我想也许这艘船很久以前就已经航行了……

然而,正如上面@stjepang@joshtriplett所指出的, type Type: Trait感觉不完整。 虽然它在trait声明中可能有意义(它实际上被设计为不完整的,尽管它不允许多个定义很奇怪),但它在impl Trait块中没有意义,其中类型应该是肯定知道的(目前只能写为type Type = RealType

impl Trait用于返回位置的存在量化和参数位置的全称量化。

是的,我在写这篇文章时也考虑过impl Trait在参数位置,并且想知道如果我知道它正在稳定,我是否应该说我会支持在参数位置impl Trait的相同参数. 也就是说,我认为最好不要重新引发这场辩论:)

允许这两个插件在语言中的一致性差距。 它们对于不同的场景也是最优的,因此两者都可以为您提供全局最优。

优化和简单

好吧,我认为有时为了简单而失去最佳状态是一件好事。 就像,C 和 ML 几乎同时诞生。 C 为简化而对最优做出了巨大让步,ML 更接近最优但更复杂。 即使算上这些语言的衍生物,我也不认为 C 开发人员和 ML 开发人员的数量具有可比性。

impl Trait:

目前,围绕impl Trait:语法,我觉得有一种趋势是为同一功能集制作两种替代语法。 但是,我认为这不是一件好事,因为相同功能的两种语法只会使用户感到困惑,尤其是当他们的确切语义总是存在细微差别时。

想象一个初学者,他总是看到type Type: Trait出现在他们的第一个type Type = impl Trait 。 他们可能会猜到发生了什么,但我很确定会有片刻“那是 WTF 吗? 我已经使用 Rust 多年了,但仍然有我从未见过的语法?”。 这或多或少是 C++ 落入的陷阱。

功能膨胀

我的想法是,基本上,它具有的功能越多,语言就越难学习。 而且我没有看到使用type Type: Trait超过type Type = impl Trait的巨大优势:就像节省了 6 个字符?

当看到type Type: Trait说编写它的人使用type Type = impl Trait时,让rustc输出一个错误对我来说更有意义:至少有一种写东西的方式,它对所有人都有意义( impl Trait已经被清楚地识别为返回位置的存在),并且它涵盖了所有用例。 如果人们尝试使用他们认为直观的东西(尽管我不同意这一点,对我来说= impl Trait比当前的= i32更直观),他们会被正确地重定向到传统上正确的书写方式。

RFC 2071 的原始拉取请求提到了一个 typeof 关键字,该关键字似乎在本次讨论中完全被忽略了。 我发现当前提出的语法相当隐含,让编译器和任何阅读代码的人都在搜索具体类型。

typeof在我 1.5 年前开的 issue 中简要讨论过: https :

作为初学者,我发现type Foo: Bar语法令人困惑。 这是关联的类型语法,但那些应该在特征中,而不是结构中。 如果你看到impl Trait一次,你可以弄清楚那是什么,否则你可以查一下。 使用其他语法更难做到这一点,我不确定有什么好处。

感觉就像语言团队中的一些人真的反对使用impl Trait来命名存在类型,所以他们宁愿使用其他任何东西来代替。 甚至这里的评论对我来说也毫无意义。

但无论如何,我认为这匹马是被打死的。 可能有数百条关于语法的评论,只有少数建议(我意识到我只会让事情变得更糟)。 很明显,没有任何语法不会让每个人都开心,并且有支持和反对所有语法的论据。 也许我们应该选择一个并坚持下去。

哇,这根本不是我理解的。 感谢@Nemo157让我直截了当!

在这种情况下,我确实更喜欢 = 语法。

@Ekleog

那么我认为这是type Type: Trait语法的问题。

可以被允许并且它会被完美地定义,但你通常写where Type: Foo + Bar而不是where Type: Foo, Type: Bar ,所以这似乎不是一个好主意。 对于这种情况,您还可以轻松地触发一个很好的错误消息,建议您在关联类型的情况下编写Foo + Bar

type Foo = impl Bar;也有可理解性问题,因为您看到= impl Bar并得出结论,您可以在每次将其用作-> impl Bar替换它; 但这行不通。 @mark-im 做出了这种解释,这似乎是一个更容易犯的错误。 因此,我得出结论, type Foo: Bar;是可学习性的更好选择。

然而,正如上面@stjepang@joshtriplett所指出的,类型Type: Trait 感觉不完整。

从扩展 POV 来看,它并不是不完整的。 您从type Foo: Bar;获得的信息与从type Foo = impl Bar;获得的信息完全相同。 所以从你可以type Foo: Bar;做什么的角度来看,它是完整的。 事实上,后者被脱糖为type _0: Bar; type Foo = _0;

编辑:我的意思是,虽然对某些人来说可能感觉不完整,但这不是从技术角度来看的。

也就是说,我认为最好不要重新引发这场辩论:)

这是个好主意。 我们应该在设计时考虑语言的本来面目,而不是我们希望的那样。

好吧,我认为有时为了简单而失去最佳状态是一件好事。

如果我们应该为了简单起见,我会改为放弃type Foo = impl Bar;
应该注意的是,C 所谓的简单性(假设是因为 Haskell Core 和类似的东西可能更简单,但仍然听起来不错..)在表现力和可靠性方面付出了高昂的代价。 C 不是我在语言设计中的北极星; 离得很远。

目前,围绕impl Trait:语法,我觉得有一种趋势是为同一功能集制作两种替代语法。 但是,我认为这不是一件好事,因为相同功能的两种语法只会使用户感到困惑,尤其是当他们的确切语义总是存在细微差别时。

但它们在语义上没有任何区别。 一种对另一种脱糖。
我认为尝试编写type Foo: Bar;type Foo = impl Bar只是为了其中一个不起作用,即使两者都具有完美定义的语义,这只会妨碍用户。 如果用户尝试编写type Foo = impl Bar; ,则会触发 lint 并提出type Foo: Bar; 。 lint 正在教用户其他语法。
对我来说,语言统一和一致很重要。 如果我们决定在某处使用这两种语法,我们应该一致地应用该决定。

想象一个初学者,他总是看到type Type: Trait出现在他们的第一个type Type = impl Trait

在这种特定情况下,lint 会触发,并推荐前一种语法。 说到type Foo = (impl Bar, impl Baz); ,初学者无论如何都要学习-> impl Trait ,所以他们应该能够从中推断出含义。

这或多或少是 C++ 落入的陷阱。

C++ 的问题主要是它太老了,有 C 的包袱,还有很多特性支持太多的范式。 这些不是不同的功能,只是不同的语法。

我的想法是,基本上,它具有的功能越多,语言就越难学习。

我认为学习一门新语言主要是学习它的重要库。 那是大部分时间将花费的地方。 正确的功能可以使库更具可组合性并在更多情况下工作。 我更喜欢一种具有良好抽象能力的语言,而不是一种强迫你思考低层次并导致重复的语言。 在这种情况下,我们并没有添加更多的抽象功能,甚至没有添加更多的功能,只是更好的人体工程学。

而且我没有看到使用type Type: Trait不是type Type = impl Trait:的巨大优势,就像节省了 6 个字符?

是的,只保存了 6 个字符。 但是如果我们考虑type Foo: Iterator<Item: Iterator<Item: Display>>; ,那么我们会得到: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; ,它有更多的噪音。 type Foo: Bar;也比后者更直接,更不容易被误解(重新替换..),并且更适合关联类型(从特征复制类型..)。
此外, type Foo: Bar可以自然地扩展到type Foo: Bar = ConcreteType; ,这将公开具体类型,但也确保它满足Bartype Foo = impl Trait;不能做这样的事情。

rustc 在看到type Type: Trait时输出错误,说明编写它的人使用type Type = impl Trait对我来说更有意义:至少有一种写东西的方式,

他们被正确地重定向到传统上正确的写作方式。

我建议用一种传统的方式来写东西。 type Foo: Bar;

@lnicola

作为初学者,我发现type Foo: Bar语法令人困惑。 这是关联的类型语法,但那些应该在特征中,而不是结构中。

我将重申,类型别名确实可以被视为关联类型。 你将能够说:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

我们看到它在关联类型和类型别名中的工作方式完全相同。

是的,只保存了 6 个字符。 但是如果我们考虑type Foo: Iterator<Item: Iterator<Item: Display>>; ,那么我们会得到: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; 这有更多的噪音。

这似乎与声明命名存在的语法正交。 我记得提出的四种语法都可能允许这样做

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

能够使用您建议的简写Trait<AssociatedType: Bound>而不是Trait<AssociatedType = impl Bound>语法来为存在类型(命名或匿名)的关联类型声明匿名存在类型是一项独立功能(但可能与保持整个存在类型特征集一致的术语)。

@Nemo157它们是不同的功能,是的; 但我认为出于一致性考虑将它们放在一起是很自然的。

@Centril

我很抱歉,但他们错了。 从扩展 POV 来看,它并不是不完整的。

我从未建议您提出的语法缺少信息; 我的意思是感觉不完整。 在我和其他人看来,它看起来不对。 我理解你不同意这一点,从你来自的角度来看,但这并没有让人感觉不对。

还观察到,在这个线程中,人们展示了这种精确语法差异的解释问题。 type Foo = impl Trait感觉它更清楚地表明Foo是一个特定但未命名的具体类型,无论您使用多少次,而不是可以采用不同具体类型的特征的别名每次使用它。

我认为告诉人们他们可以把他们所知道的关于-> impl Trait所有东西应用到type Foo = impl Trait ; 有一个通用的概念impl Trait ,他们可以将其视为两个地方的构建块。 像type Foo: Trait这样的语法隐藏了通用的构建块。

@joshtriplett

我的意思是感觉不完整。 在我和其他人看来,它看起来不对。

好吧; 我建议我们在这里使用与incomplete不同的术语,因为对我来说,这表明缺乏信息。

还观察到,在这个线程中,人们展示了这种精确语法差异的解释问题。

我观察到的是一个解释错误,在线程中,关于type Foo = impl Bar;含义。 一个人将Foo不同用途解释为名义上不是相同的类型,而是不同的类型。 也就是说,确切地说: “每次使用时都可以采用不同的具体类型的特征的别名”

有人说type Foo: Bar;令人困惑,但我不确定type Foo: Bar;的替代解释与预期含义不同。 我很想听听其他解释。

@Centril

我将重申,类型别名确实可以被视为关联类型。

他们可以,但现在关联类型与特征相关。 impl Trait适用于任何地方,或者几乎。 如果要将impl Trait为一种关联类型,则必须同时引入两个概念。 也就是说,您将impl Trait视为函数返回类型,猜测或阅读它的含义,然后当您在类型别名中看到impl Trait时,您可以重用该知识。

将其与在特征定义中查看关联类型进行比较。 在这种情况下,您认为这是其他结构必须定义或实现的东西。 但是,如果您在特质之外遇到type Foo: Debug ,您将不知道那是什么。 没有人来实现它,所以它是某种前向声明吗? 它是否与继承有关,就像在 C++ 中一样? 是不是像其他人选择类型的 ML 模块? 如果您以前见过impl Trait ,那么就没有什么可以在它们之间建立联系了。 我们写fn foo() -> impl ToString ,而不是fn foo(): ToString

类型 Foo = impl Bar; 也有可理解性问题,因为您看到 = impl Bar 并得出结论,您可以在每次将其用作 -> impl Bar 时替换它

我之前在这里说过,但这就像认为let x = foo();意味着您可以使用x而不是foo() 。 无论如何,这是一个可以在需要时快速查找的细节,但不会从根本上改变概念。

也就是说,很容易弄清楚这是关于什么的(推导类型,如-> impl Trait ),即使您不确切知道它是如何工作的(当您对它有冲突的定义时会发生什么)。 使用其他语法甚至很难意识到它是什么。

@Centril

好吧; 我建议我们在这里使用一个不同的术语,而不是不完整的,因为对我来说,它表明缺乏信息。

“不完整”并不一定意味着缺乏信息,它可能意味着某些东西看起来应该有其他东西而没有。

type Foo: Trait;看起来不像一个完整的声明。 看起来它缺少了一些东西。 它似乎与type Foo = SomeType<X, Y, Z>;

也许我们已经到了这样的地步,即我们的单行者无法真正弥合type Inferred: Traittype Inferred = impl Trait之间的共识差距。

你认为是否值得用任何语法(甚至是 RFC 中指定的语法)组合这个特性的实验性实现,以便我们可以开始在更大的程序中使用它,看看它是如何适应上下文的?

@lnicola

[..] impl Trait适用于任何地方,或几乎

好吧, Foo: Bound几乎在任何地方都可以使用;)

但是,如果您在特征之外遇到type Foo: Debug ,您将不知道那是什么。

我认为在以下方面使用它的过程: trait -> impl -> type alias 有助于学习。
此外,我认为“Foo 类型实现 Debug”的推断可能来自
在特征和通用边界中看到type Foo: Debug也是正确的。

它是否与继承有关,就像在 C++ 中一样?

我认为 Rust 缺乏继承需要在比学习我们正在讨论的特性时更早的阶段学习,因为这对 Rust 来说是如此基础。

是不是像其他人选择类型的 ML 模块?

由于调用者(用户)选择类型的arg: impl Bar ,也可以对type Foo = impl Bar;进行该推断。 对我来说,对于type Foo: Bar;来说,用户选择类型的推断似乎不太可能。

我之前在这里说过,但这就像认为let x = foo();意味着您可以使用x而不是foo()

如果语言是引用透明的,您可以x替换foo() 。 直到我们将type Foo = impl Foo;到系统中之前,类型别名都是引用透明的。 相反,如果已经有一个绑定x = foo()可用,那么其他foo() in可以替换为x

@joshtriplett

“不完整”并不一定意味着缺乏信息,它可能意味着某些东西看起来应该有其他东西而没有。

很公平; 但它应该有什么它没有?

type Foo: Trait;看起来不像一个完整的声明。

对我来说看起来很完整。 看起来像是判断Foo满足Trait的意思,这正是本意。

@Centril对我来说,“缺少的东西”是它的别名的实际类型。 这和我之前的困惑有点关系。 并不是没有这样的类型,只是那个类型是匿名的......使用 = 巧妙地暗示了有一个类型,它总是相同的类型,但我们不能命名它。

我认为我们有点用尽了这些论点。 最好只是通过实验实现这两种语法并看看什么最有效。

@mark-im

@Centril对我来说,“缺少的东西”是它的别名的实际类型。 这和我之前的困惑有点关系。 并不是没有这样的类型,只是那个类型是匿名的......使用 = 巧妙地暗示了有一个类型,它总是相同的类型,但我们不能命名它。

这正是我的感觉。

有没有机会尽快解决这两个延期项目,以及关于终身省略的问题? 我会自己做,但不知道怎么做!

关于impl Trait确切含义仍然存在很多混淆,而且一点也不明显。 我认为延迟项目绝对应该等到我们清楚地了解impl Trait的确切语义(应该很快就会出现)。

@varkor什么语义不清楚? AFAIK 自 RFC 1951 以来, impl Trait的语义没有任何变化,并在 2071 年进行了扩展。

@alexreg我没有任何计划,但这里有一个粗略的大纲:添加解析后,您需要降低存在 impl 中staticconst的类型特征上下文,就像这里对函数的返回类型所做的那样DefIdImplTraitContext::Existential可选的,因为你不想让你的impl Trait拿起从父函数定义泛型。 这应该让你有一些不错的选择。 如果您基于 @oli-obk 的存在类型 PR ,您可能会更轻松一些。

@cramertj :语言中impl Trait的语义完全限于它在函数签名中的使用,并且将其扩展到其他位置具有明显的含义是不正确的。 我很快就会对此进行更详细的说明,大部分对话似乎都在进行中。

@varkor

该语言中 impl Trait 的语义完全限于其在函数签名中的使用,并且将其扩展到其他位置具有明显的含义是不正确的。

含义在RFC 2071中指定。

@cramertj :RFC 2071 中的含义是模棱两可的,并且允许对短语“存在类型”的含义进行多种解释。

TL;DR — 我试图为impl Trait设定一个精确的含义,我认为这可以澄清那些至少直观地不清楚的细节; 以及对新类型别名语法的提议。

Rust 中的存在类型(帖子)


在过去的几天里,在 Discord rust-lang 聊天中关于impl Trait的精确(即正式的、理论上的)语义进行了很多讨论。 我认为澄清有关该功能的许多细节以及它是什么和不是什么是有帮助的。 它还揭示了哪些语法对于类型别名是合理的。

我写了一些我们的结论的小总结。 这提供了对impl Trait的解释,我认为它相当干净,并且准确地描述了参数位置impl Trait和返回位置impl Trait之间的差异(这不是“普遍-量化”与“存在量化”)。 也有一些实际的结论。

在其中,我提出了一种新的语法来满足“存在类型别名”的普遍要求:
type Foo: Bar = _;

因为这是一个复杂的话题,有很多东西需要先弄清楚,所以我把它写成一个单独的帖子。 非常感谢您的反馈!

Rust 中的存在类型(帖子)

@varkor

RFC 2071 是模棱两可的,允许对“存在类型”一词的含义进行多种解释。

怎么模棱两可? 我读过你的帖子——我仍然只知道静态和常量中非动态存在的一种含义。 它的行为与返回位置impl Trait行为相同,通过为每个项目引入新的存在类型定义。

type Foo: Bar = _;

我们在 RFC 2071 期间讨论了这种语法。正如我在那里所说,我喜欢它清楚地表明Foo是一个单一的推断类型,它为在当前模块之外存在的非推断类型留出了空间(例如type Foo: Bar = u32; )。 我不喜欢它的两个方面:(1)它没有关键字,因此更难搜索;(b)与type Foo = impl Trait相比,它具有与abstract type Foo: Bar;语法相同的冗长问题有: type Foo = impl Iterator<Item = impl Display>;变成type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; 。 我不认为其中任何一个都是破坏交易的,但这不是一个明显的胜利方式或另一种 IMO。

@cramertj这里出现了歧义:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

如果Foo真的是存在类型的类型别名,那么fg将支持不同的具体返回类型。 有几个人本能地以这种方式阅读了该语法,事实上,RFC 2071 语法讨论的一些参与者只是意识到,作为最近 Discord 讨论的一部分,该提案不是这样工作的

问题在于,尤其是面对论点位置impl Trait ,存在量词的去向并不明确。 对于论点,它的范围很窄; 对于返回位置,它的范围似乎很窄,但结果却比这更宽; 对于type Foo = impl Bar这两个位置都是合理的。 基于_的语法推动了一种甚至不涉及“存在”的解释,巧妙地回避了这个问题。

如果Foo真的是一个存在主义类型的类型别名

(强调我的)。 我将“an”读为“特定”,这意味着fg将_不_支持不同的具体返回类型,因为它们引用相同的存在类型。 我一直认为type Foo = impl Bar;使用与let foo: impl Bar;相同的含义,即引入一个新的匿名存在类型; 使您的示例等效于

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

我希望这是相对明确的。


一个问题是“类型别名中的impl Trait ”的含义从未在 RFC 中指定。 它在RFC 2071 的“替代方案”部分中被简要提及,但由于这些固有的教学含糊不清而被明确打折扣。

我也觉得我看到有人提到类型别名已经不是引用透明的。 我认为它在 u.rl.o 上,但经过一番搜索后我无法找到讨论。

@cramertj
@rpjohnst的观点出发,对impl Trait的语义有多种解释,这些解释都与签名中的当前用法一致,但在将impl Trait扩展到其他时会产生不同的结果位置(我知道除了帖子中描述的位置之外的 2 个,但还没有准备好讨论)。 而且我不认为帖子中的解释一定是最明显的(我个人从这个角度没有看到关于 APIT 和 RTIP 的任何类似解释)。

关于type Foo: Bar = _; ,我认为也许应该重新讨论一下——用新的眼光重新审视旧想法并没有什么坏处。 关于你的问题:
(1) 它没有关键字,但它的语法与任何地方的类型推断相同。 搜索“下划线”/“下划线类型”等文档可以轻松提供有关类型推断的页面。
(2) 是的,确实如此。 我们一直在考虑一个解决方案,我认为它与下划线符号非常吻合,希望很快就能提出建议。

@cramertj一样,我并没有真正看到这里的论点。

我只是看不到@varkor的帖子所描述的基本歧义。 我认为我们一直将 Rust 中的“存在类型”解释为“存在一个 _unique_ 类型......”而不是“至少存在一种类型......”因为(正如@varkor的帖子所说)后者等同于“通用类型”,因此如果我们打算允许这种解释,“存在类型”这个短语将完全没有用。 afaik 关于该主题的每个 RFC 始终假定通用类型和存在类型是两个不同的东西。 我在实际的类型论中明白这就是它的意思,同构在数学上是非常真实的,但对我来说,这只是我们一直在滥用类型论术语并且需要为此选择一些其他行话的论点,而不是一个论点impl Trait的预期语义总是不清楚,需要重新考虑。

@rpjohnst描述的范围模糊性是一个严重的问题,但每个提议的语法都可能与类型别名或关联类型混淆。 这些混淆中的哪一个是“更糟”或“更有可能”正是我们在数百条评论后未能解决的永无止境的自行车棚。 我确实喜欢type Foo: Bar = _;似乎解决了type Foo: Bar;需要大量声明来声明任何不重要的存在的问题,但我认为这不足以真正改变“永无止境的自行车棚”情况。

我确信的是,无论我们最终使用什么语法,都需要使用type以外的关键字,因为所有“仅type ”的语法都具有误导性。 事实上,也许不要在语法中使用type _at all_ 所以没有人可以假设他们正在查看“类型别名,但以某种方式更存在”。

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

对我来说,这些完全 _prevent_ 对fg可能返回实现Trait两种不同类型的误解并不明显,但我怀疑这与预防一样接近我们可能会得到。

@Ixrec
短语“存在类型”是有问题的,特别是因为范围模糊。 我还没有看到其他人指出 APIT 和 RPIT 的范围完全不同。 这意味着像type Foo = impl Bar这样的语法,其中impl Bar是“存在类型”,本质上是模棱两可的。

是的,类型论术语被滥用了很多。 但它在 RFC 中被误用(或至少没有解释)——因此 RFC 本身存在歧义。

@rpjohnst描述的范围模糊性是一个严重的问题,但每个提议的语法都可能与类型别名或关联类型混淆。 这些混淆中的哪一个是“更糟”或“更有可能”正是我们在数百条评论后未能解决的永无止境的自行车棚。

不,我不认为这是真的。 有可能提出没有这种混淆的一致语法。 我会冒险放弃自行车是因为目前的两个提议很糟糕,所以它们并不能真正满足任何人。

我确信的是,无论我们最终使用什么语法,都需要有一个关键字,而不是type

我认为这也没有必要。 在您的示例中,您发明了全新的符号,这是您希望在语言设计中尽可能避免的东西——否则您会创建一个充满不一致语法的庞大语言。 只有在没有更好的选择时,您才应该探索全新的语法。 我认为有一个更好的选择。

旁注:在旁注中,我认为完全摆脱“存在类型”是可能的,使整个情况更加清晰,我或其他人将很快跟进。

我发现自己认为type以外的语法也会有所帮助,正是因为许多人将type为简单的可替换别名,这意味着“每次都可能使用不同的类型”解释。

我还没有看到其他人指出 APIT 和 RPIT 的范围完全不同。

我认为范围界定始终是 impl Trait 提案的明确部分,因此不需要“指出”。 你所说的关于范围界定的一切似乎只是在重申我们在过去的 RFC 中已经接受的内容。 我知道从语法上对每个人来说都不是很明显,这是一个问题,但并不是以前没有人理解这一点。 事实上,我认为关于 RFC 2701 的大部分讨论都是关于type Foo = impl Trait;应该是什么,在类型推断是什么以及不允许查看的意义上。

有可能提出没有这种混淆的一致语法。

你是想说type Foo: Bar = _;是那种语法,还是你认为我们还没有找到它?

我认为不可能提出一个没有任何类似混淆的语法,不是因为我们的创造力不足,而是因为大多数程序员不是类型理论家。 我们可能会找到一种将混淆减少到可以容忍的水平的语法,当然有很多语法对于类型理论老手来说是明确的,但我们永远不会完全消除混淆。

你发明了全新的符号

我以为我只是用另一个关键字替换了一个关键字。 您是否看到了一些我不打算的额外更改?

想一想,因为我们一直在滥用“存在”,这意味着existential Foo: Trait / = impl Trait可能不再是合法的语法了。

所以我们需要一个新的关键字放在引用一些未知到外部代码类型的名称前面......我在这上面画了一个空白。 aliassecretinternal等看起来都非常糟糕,而且不太可能有比type更少的“唯一性混淆”。

想一想,因为我们一直在滥用“存在”,这意味着existential Foo: Trait / = impl Trait可能不再是合法的语法了。

是的,我完全同意——我认为我们需要完全摆脱“存在”这个词*(对于如何做到这一点已经有了一些初步的想法,同时仍然很好地解释了impl Trait )。

*(可能只保留dyn Trait的期限)

@joshtriplett@Ixrec :我同意_表示法意味着您不能再像以前那样替换,如果这是优先保留的,我们将需要不同的语法。

请记住,无论如何, _已经是一个关于替换的特例——它影响的不仅仅是类型别名:您当前可以使用的任何地方_ ,您都在阻止完全引用透明性。

请记住,无论如何,_ 已经是一个关于替换的特殊情况——它影响的不仅仅是类型别名:您当前可以使用 _ 的任何地方,您都在阻止完全的引用透明性。

你能告诉我们这到底意味着什么吗? 我不知道受_影响的“参考透明度”概念。

我同意 _ 表示法意味着你不能再像以前那样替换,如果这是优先保留的,我们将需要不同的语法。

我不确定这是一个_priority_。 对我来说,这只是我们发现的唯一客观的论点,似乎更喜欢一种语法而不是另一种。 但这一切都可能会根据我们可以想出的替换type关键字而改变。

你能告诉我们这到底意味着什么吗? 我不知道受_影响的“参考透明度”概念。

是的,对不起,我在没有解释的情况下胡说八道。 让我收集一下我的想法,然后我会制定一个更有凝聚力的解释。 它非常适合查看impl Trait的另一种(并且可能更有帮助)的方式。

引用透明性意味着可以用引用代替其定义,反之亦然,而无需更改语义。 在 Rust 中,这显然不适用于fn的术语级别。 例如:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

如果我们将每次出现的bar替换为foo()bar的定义),那么我们显然会得到不同的输出。

但是,对于类型别名,目前保持引用透明度(AFAIK)。 如果您有别名:

type Foo = Definition;

然后,您可以在不更改程序语义的情况下(避免捕获)将Foo替换为Definition并将Definition替换为Foo ,或其类型正确性。

介绍:

type Foo = impl Bar;

意味着Foo每次出现都是相同的类型,这意味着如果你写:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

您不能将出现的Foo替换为impl Bar ,反之亦然。 也就是说,如果你写:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

返回类型不会与Foo统一。 因此,通过在impl Trait内部引入 RFC 2071 的语义,破坏了类型别名的引用透明性。

关于参照透明度和type Foo = _; ,待续……(作者:@varkor)

我发现自己认为除了 type 之外的语法也会有所帮助,正是因为许多人将 type 解释为一个简单的可替代别名,这意味着“每次都可能使用不同的类型”解释。

好点子。 但是= _赋值位不是暗示它只是一种类型吗?

我以前写过这个,但是...

参考透明度:我认为将type视为绑定(如let )而不是类似 C 预处理器的替换更有用。 一旦你这样看, type Foo = impl Trait就是它看起来的样子。

我想初学者不太可能将impl Trait视为存在与通用类型,而是“ impl sa Trait . If they want to know more, they can read the impl Trait`文档的东西。一旦你更改语法,您将失去它与现有功能之间的联系,而没有太多好处。_您只是用另一种可能误导的语法替换。_

关于type Foo = _ ,它重载了_ ,含义完全不相关。 在文档和/或 Google 中查找似乎也很棘手。

@lnicola您也可以使用const绑定而不是let绑定,前者是引用透明的。 选择let (碰巧在fn内部不是引用透明的)是我认为不是特别直观的任意选择。 我认为类型别名的直观视图是它们是引用透明的(即使没有使用该词),因为它们是aliases

我也没有将type视为 C 预处理器替代,因为它必须避免捕获并尊重泛型(没有 SFINAE)。 相反,我正在考虑type就像我在 Idris 或 Agda 之类的语言中的绑定一样,其中所有绑定都是纯的。

我想初学者不太可能将impl Trait视为存在与通用类型,而是“一种体现特质的东西

这对我来说似乎是一个没有区别的区别。 没有使用术语“existential”,但我相信用户直观地将其链接到与存在类型相同的概念(在 Rust 的上下文中,这只不过是“some type Foo that impls Bar”)。

关于type Foo = _ ,它重载了_ ,含义完全不相关。

怎么会这样? type Foo = _;这里与_在其他需要类型的上下文中的使用一致。
它的意思是“推断真正的类型”,就像你写.collect::<Vec<_>>()

在文档和/或 Google 中查找似乎也很棘手。

不应该那么难吗? “类型别名下划线”应该有望带来想要的结果......?
似乎与搜索“type alias impl trait”没有什么不同。

谷歌不索引特殊字符。 如果我的 StackOverflow 问题中有下划线,Google 不会自动为包含下划线一词的查询编制索引

@Centril

怎么会这样? type Foo = _;与在其他需要类型的上下文中使用 _ 一致。
它的意思是“推断真正的类型”,就像你写 .collect::>()。

但是此功能不会推断类型并为您提供类型别名,它会创建一个存在类型,该类型(在模块或板条箱等有限范围之外)不与“真实类型”统一。

谷歌不索引特殊字符。

这不再是真的(尽管可能依赖于空格..?)。

但是此功能不会推断类型并为您提供类型别名,它会创建一个存在类型,该类型(在模块或板条箱等有限范围之外)不与“真实类型”统一。

type Foo = _;的建议语义是完全基于推理的存在类型别名的替代方案。 如果这还不完全清楚,我将很快跟进一些应该更好地解释意图的东西。

@iopq除了@varkor关于最近更改的注释之外,我还想补充一点,对于其他搜索引擎,官方文档等总是有可能明确使用文字“下划线”和type使其变得可搜索。

无论出于何种原因,在查询中使用 _ 仍然不会得到好的结果。 如果你搜索下划线,你会得到带有下划线这个词的东西。 如果你搜索 _ 你会得到所有有下划线的东西,所以我什至不知道它是否相关

@Centril

选择 let (恰好在 fn 内部不是透明的)是一个我认为不是特别直观的任意选择。 我认为类型别名的直观观点是它们在引用上是透明的(即使没有使用该词),因为它们是别名。

抱歉,我仍然无法理解这一点,因为我的直觉完全倒退了。

例如,如果我们有type Foo = Bar ,我的直觉是:
“我们声明了Foo ,它变成了与Bar相同的类型。”

然后,如果我们写type Foo = impl Bar ,我的直觉会说:
“我们正在声明Foo ,它成为实现Bar 。”

如果Foo只是impl Bar的文本别名,那么这对我来说是非常不直观的。 我喜欢将其视为文本别名与语义别名。

因此,如果Foo可以在它出现的任何地方用impl Bar替换,那是一个文本别名,对我来说最让人联想到宏和元编程。 但是,如果Foo在声明时被赋予了一个含义,并且可以在具有该原始含义的多个地方使用(不是上下文含义!),那就是语义别名。

此外,无论如何,我无法理解上下文存在类型背后的动机。 考虑到特征别名可以达到完全相同的效果,它们会有用吗?

也许由于我的非 Haskell 背景,我发现引用透明度不直观,谁知道... :) 但无论如何,这绝对不是我在 Rust 中所期望的那种行为。

@Nemo157 @stjepang

如果Foo真的是一个存在主义类型的类型别名

(强调我的)。 我将“an”读为“特定”,这意味着fg将不支持不同的具体返回类型,因为它们引用相同的存在类型。

这是对“存在类型”一词的滥用,或者至少是与@varkor的帖子不一致的方式。 type Foo = impl Bar可以出现,使Foo的类型的别名∃ T. T: Trait -如果你替代∃ T. T: Trait无处不在,你使用Foo即使是非-textually ,您可以在每个位置获得不同的具体类型。

这个∃ T量词的范围(在您的示例中表示为existential type _0 )是有问题的。 在 APIT 中它很紧凑——调用者可以传递任何满足∃ T. T: Trait 。 但它不在RPIT 中,也不在 RFC 2071 的existential type声明中,也不在你的去糖示例中 - 在那里,量词更远,在整个功能或整个模块级别,你处理到处都是相同的T

因此,歧义 - 我们已经有impl Trait将其量词放置在不同的位置,这取决于它的位置,那么我们应该期待哪一个type T = impl Trait ? 一些非正式的民意调查,以及 RFC 2071 线程中参与者的一些事后实现,证明它并不清楚一种或另一种方式。

这就是为什么我们想要摆脱将impl Trait解释为与存在主义有关的任何事情,而是根据类型推断来描述其语义。 type T = _没有相同的歧义-仍然存在表面级别“无法复制粘贴_代替T ”,但不再存在“ T是别名的单一类型可以表示多个具体类型。” (不透明/不统一行为是@varkor正在谈论的后续行动。)

参照透明度

仅仅因为当前与引用透明性兼容的类型别名并不意味着人们期望该功能遵循它。

作为一个例子,在const产品参照透明的(在https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768提到的),以及实际造成混乱到新用户 (rust-lang-nursery/rust-clippy#1560)。

所以我认为对于 Rust 程序员来说,引用透明度并不是他们首先想到的。

@stjepang @kennytm我并不是说每个人都会期望带有type Foo = impl Trait;类型别名将以引用透明的方式起作用。 但我认为会有相当多的用户,正如这个线程和其他地方的混淆所证明的那样( @rpjohnst指的是......)。 这是一个问题,但也许不是一个不可克服的问题。 在我们前进的过程中,这是需要牢记的。

我目前对在这件事上应该做什么的想法已经与@varkor和@rpjohnst 保持一致。

回复:参考透明度

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

也就是说,即使在每个实例处生成新类型,在泛型类型别名的上下文中也不是透明的。

@centril当谈到期望Footype Foo = impl Bar;引用透明度时,我举手type Foo: Bar = _; ,我不希望引用透明度。

也有可能我们可以扩展返回位置impl Trait以支持多种类型,而无需任何类似enum impl Trait的机制,通过单态化(部分)调用者。 这加强了“ impl Trait始终存在”的解释,使其更接近dyn Trait ,并建议使用使用impl Traitabstract type语法一点也不。

我在这里写了这篇关于内部的文章: https :

只是当我们稳定新的存在类型时的一个注释 - “存在”总是打算成为一个临时关键字(根据 RFC)并且(IMO)是可怕的。 在稳定之前,我们必须想出更好的东西。

关于“存在”类型的讨论似乎并没有澄清问题。 我会说impl Trait代表实现 Trait 的特定推断类型。 以这种方式描述, type Foo = impl Bar显然是一个特定的、总是相同的类型——这也是唯一真正有用的解释:所以它可以用于除推断它的上下文之外的其他上下文,就像在结构中一样。

从这个意义上说,将impl Trait也写为_ : Trait有意义的。

@rpjohnst

我们也可以扩展 return-position impl Trait以支持多种类型

这将使IMO严格意义上的用处降低impl类型的别名的意义在于,一个函数可以定义为返回impl Foo ,但特定类型仍然通过程序在其他结构和东西中传播。 如果编译器隐式生成合适的enum ,那将起作用,但不适用于单态化。

@jan-hudec 这些想法已经在 Discord 的讨论中提出,并且存在一些问题,主要基于这样一个事实,即当前对返回位置和参数位置impl Trait的解释不一致。

impl Trait代表特定的推断类型是一个不错的选择,但要修复这种不一致性,它必须是与 Rust 今天不同的类型推断——它必须推断多态类型以便它可以保留参数位置impl Trait的当前行为。 这可能是最直接的方法,但它并不像你说的那么简单。

例如,一旦impl Trait意味着“使用这种新的推理类型来找到实现Trait的尽可能多态的类型”, type Foo = impl Bar开始暗示关于模块的事情。 RFC 2071 关于如何推断abstract type说所有使用必须独立推断相同的类型,但这种多态推断至少意味着更多是可能的。 如果我们曾经得到参数化的模块(即使只是在生命周期内,一个更合理的想法),就会有关于这种交互的问题。

还有一个事实是,有些人总是将type Foo = impl Bar语法解释为存在主义的别名,无论他们是否理解“存在主义”这个词,也不管我们如何教授它。 所以选择一种替代语法,即使它碰巧与基于推理的解释一起工作,可能仍然是一个好主意。

此外,虽然_: Trait语法实际上首先激发了围绕基于推理的解释的讨论,但它并没有达到我们想要的效果。 首先, _所隐含的推理不是多态的,所以这与语言的其余部分是一个不好的类比。 其次, _意味着实际类型在其他地方可见,而impl Trait专门设计用于隐藏实际类型。

最后,我之所以写单态化提案,是为了找到另一种方式来统一argument和return-position impl Trait的含义。 虽然是的,但这确实意味着-> impl Trait不再保证单一的具体类型,但我们目前还没有办法利用它。 并且提出的解决方案都是烦人的解决方法 - 额外的样板abstract type技巧, typeof等。迫使每个想要依赖单一类型行为的人也通过abstract type命名该单一类型abstract type语法(无论它可能是什么)可以说是总体上的好处。

这些想法已经在 Discord 的讨论中提出,并且存在一些问题,主要基于当前对返回位置和参数位置impl Trait的解释不一致的事实。

就个人而言,我认为这种不一致在实践中不是问题。 为参数位置、返回位置和类型位置确定具体类型的范围似乎相当直观。

我有一个函数,调用者决定它的返回类型。 当然,我不能在那里使用 impl Trait。 在您了解差异之前,它并不像您暗示的那么直观。

就个人而言,我认为这种不一致在实践中不是问题。

的确。这对我的建议不是我们应该忽略不一致性,而是我们应该重新解释设计以使其保持一致(例如,通过将其解释为多态类型推断)。这样,可以根据新的、一致的解释检查未来的扩展(RFC 2071 等),以防止事情变得混乱。

@rpjohnst

强迫每个想要依赖单一类型行为的人也通过抽象类型语法(无论它可能是什么)来命名该单一类型,总体上可以说是一个好处。

在某些情况下,我同意这种观点,但它不适用于闭包或生成器,并且对于许多你不关心类型是什么而你只关心它实现了某种特性的情况是不符合人体工程学的,例如使用迭代器组合器。

@mikeyhew您误解了我-它适用于闭包或其他abstract type语法发明一个名称。 无论您是否要在其他任何地方使用单一类型,您必须发明一个名称。

@rpjohnst哦,我明白了,感谢您的澄清

焦急地等待let x: impl Trait

作为对let x: impl Trait另一票,它将简化一些futures示例,这是一个示例示例,目前它正在使用一个函数来获得使用impl Trait

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

相反,这可以写成普通的 let 绑定:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

如果需要,我可以通过实施let x: impl Trait来指导某人。 这不是不可能做到的,但也绝对不容易。 一个入口点:

与我们在https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 中访问返回类型 impl Trait 的方式类似,我们需要在https 中访问 locals 的类型

然后,在访问本地人的类型时,一定要将ExistentialContextReturn才能真正启用它。

这应该已经让我们走得很远了。 不确定是否一直如此,它不是 100% 像 return position impl trait,但大多数情况下应该表现得像它。

@rpjohnst

这些想法已经在 Discord 的讨论中提出,并且存在一些问题,主要基于当前对 return-position 和 argument-position impl Trait 的解释不一致的事实。

让我们回到您在文章中谈到的范围。 而且我认为它们实际上对应于封闭的“括号”:对于参数位置,它是参数列表,对于返回位置,它是函数——对于别名,它是定义别名的范围。

根据此线程中的讨论、原始 RFC 和同步讨论,我已经打开了一个 RFC,提出了对existential type具体语法的解决方案: https :

当前存在类型实现不能用于表示所有当前返回位置impl Trait定义,因为impl Trait捕获每个泛型类型参数,即使未使用它也应该可以对existential type执行相同操作游乐场)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

这可能很重要,因为类型参数可以具有限制返回impl Trait的生命周期的内部生命周期,即使该值本身未使用,请在操场中从Bar中删除<T>上面看到对foo的调用失败但bar有效。

当前存在类型实现不能用于表示所有当前返回位置 impl Trait 定义

可以,只是很不方便。 您可以返回带有PhantomData字段 + 实际数据字段的新类型,并将特征实现为转发到实际数据字段

@oli-obk 感谢您的额外建议。 根据您之前的建议和来自@cramertj 的一些建议,我可能很快就会尝试一下。

@fasihrana @Nemo157见上文。 也许几周后! :-)

有人可以澄清existential type隐式捕获类型参数的行为( @Nemo157提到的)是故意的并且会保持原样吗? 我喜欢它,因为它解决了 #42940

我是故意这样实现的

@Arnavion是的,这是故意的,并且与 Rust 中其他项声明(例如嵌套函数)的工作方式相匹配。

existential_typenever_type之间的交互是否已经讨论过?

也许!应该能够填写任何存在类型,而不管涉及的特征如何。

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

或者是否会有某种特殊的不可接触的类型作为类型级别的unimplemented!()能够自动满足任何存在类型?

@vi我认为这属于一般的“从不类型应该在没有任何非默认非自我方法或关联类型的情况下实现所有特征”。 不过,我不知道会在哪里跟踪。

是否有计划尽快扩展对 trait 方法返回类型的支持?

existential type已经适用于特征方法。 Wrt impl Trait ,甚至包含在 RFC 中吗?

@alexreg我相信当你有类似fn foo<T>(..) -> impl Bar<T> (大约变成-> Self::AnonBar0<T> )时,这需要GAT能够脱糖为匿名关联类型。

@Centril你的意思是在impl Bar上做<T>吗? impl Trait的隐式类型捕获行为意味着即使使用fn foo<T>(self, t: T) -> impl Bar;类的东西,您也对 GAT 有相同的需求。

@Nemo157对不起,我没有。 但是你的例子更好地说明了这个问题。 谢谢 :)

@alexreg我相信当你有 fn foo 之类的东西时,这需要(..) -> impl Bar(大致变成 -> Self::AnonBar0)。

啊,我明白了。 老实说,这听起来并不是绝对必要的,但它肯定是实现它的一种方式。 虽然 GAT 缺乏运动让我有点担心......很长一段时间没有听到任何消息。

分类: https : {let,const,static} foo: impl Trait的勾选框。

我能写出:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

可能不是。 但是有正在进行的准备一切的计划,所以我们可能会得到

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

您可以在每晚以以下形式试用此功能

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

获取更多信息的良好开端是https://github.com/rust-lang/rfcs/pull/2071 (以及与之相关的所有内容)

@oli-obk 在rustc 1.32.0-nightly (00e03ee57 2018-11-22) ,我还需要给existential type的特征边界以在impl块中工作。 这是预期的吗?

@jonhoo能够指定特征很有用,因为您可以提供的不仅仅是所需的特征

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

所需的特征可以隐式添加到存在关联的类型中,因此在扩展它们时只需要边界,但我个人更喜欢必须将它们放入实现的本地文档。

@Nemo157啊,对不起,我的意思是目前你_必须_有界限。 也就是说,这不会编译:

impl A for B {
    existential type Assoc;
    // ...
}

而这将:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

哦,所以即使特征不需要关联类型的边界,您仍然必须为存在类型(可能为空)(游乐场)提供边界:

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

这对我来说似乎是一个边缘案例,拥有无边界的存在类型意味着它为用户提供了 _no_ 操作(除了自动特征),那么它可以用来做什么呢?

另外值得注意的是,没有办法用-> impl Trait做同样的事情, -> impl ()是一个语法错误,而-> impl本身就给出了error: at least one trait must be specified ;如果存在类型语法变成type Assoc = impl Debug;或类似的,那么似乎没有至少一个特征绑定就无法指定关联类型。

@Nemo157是的,我只是意识到,因为我确实尝试了您上面建议的代码,但它不起作用:p 我有点假设它会从特征中推断出界限。 例如:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

不必再次指定Future<Output = u32>似乎是合理的,但这不起作用。 我假设existential type Assoc: ; (这也看起来像超级奇怪的语法)也不会做那个推断?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

这对我来说似乎是一个边缘案例,拥有无边界的存在类型意味着它为用户提供了 _no_ 操作(除了自动特征),那么它可以用来做什么呢?

这些不能在相同的特征实现中用于消费吗? 像这样的东西:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

这有点做作,但它可能很有用 - 我可以想象出于生命周期的原因需要在真正的结构之前构建一些初步部分的情况。 或者它可能是这样的:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

在这两种情况下existential type Assoc;都会很有用。

为 impl Trait 定义关联类型的正确方法是什么?

例如,如果我有一个Action特征并且我想确保特征的关联类型的实现是可发送的,我可以这样做:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

这是目前不可能的事情吗?

@acycliczebra我认为它的语法是-> impl Action<Result = impl Send> - 例如,这与-> impl Iterator<Item = u32>使用另一个匿名impl Trait类型的语法相同。

有没有讨论过将impl Trait语法扩展到结构字段之类的东西? 例如,如果我正在为我的公共接口实现一个特定迭代器类型的包装器:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

只要它满足某些特征界限,它在我并不真正关心实际类型的情况下会很有用。 这个例子很简单,但我过去遇到过这样的情况,我用一堆嵌套的类型参数编写很长的类型,这真的没有必要,因为我真的什么都不关心,除了这是一个ExactSizeIterator

但是 IIRC,我认为目前没有办法用impl Trait指定多个边界,所以我会丢失一些有用的东西,比如Clone

@AGausmann关于该主题的最新讨论位于https://github.com/rust-lang/rfcs/pull/2515。 这将允许您说type Foo = impl Bar; struct Baz { field: Foo } ... 。 我认为在稳定type Foo = impl Bar;之后,我们可能希望将field: impl Trait视为糖。 它确实感觉像是一个合理的宏观友好的便利扩展。

@Centril

我想我们可能想将field: impl Trait视为糖

我不认为这是合理的。 结构字段仍然必须具有具体类型,因此您必须告诉编译器它所绑定的函数的返回值。 它可以推断出来,但如果你有多个函数,那么找到它是哪一个就不是那么容易了——而且 Rust 的通常策略是在这种情况下是明确的。

它可以推断出来,但如果你有多个功能,那么找到它是哪一个就不是那么容易了

您会提出对父类型定义用途的要求。 然后将是同一模块中的所有这些函数返回父类型。 对我来说似乎并不难找到。 但我认为,在继续扩展之前,我们希望在type Foo = impl Bar;上解决这个问题。

我想我在当前的existential type实现中发现了一个错误。


代码

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


错误

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

操场

你也可以在 stackoverflow 上找到

我不是 100% 确定我们可以开箱即用地支持这种情况,但是您可以做的是重写函数以具有两个通用参数:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

谢谢!
是的,这就是我在stackoverflow帖子上发布的内容:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

impl Trait是否有计划在 trait 的上下文中可用?
不仅作为关联类型,而且作为方法中的返回值。

impl trait in traits 是与此处跟踪的特性不同的特性,目前没有 RFC。 在这个领域有相当长的设计历史,在 2071(存在类型)的实现稳定之前,进一步的迭代被推迟,因为实现问题以及未解决的语法(有一个单独的 RFC)而被阻止。

@cramertj语法几乎解决了。 我相信现在主要的障碍是 GAT。

@alexreghttps :

@varkor是的,我只是乐观地认为他们很快就会看到该 RFC 的曙光。 ;-)

可能会出现以下情况吗?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

您现在可以执行此操作,尽管只能使用hint函数来指定Interface的具体类型

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

如果回调能够选择它的参数类型,你会怎么写? 实际上,nvm,我想你可以通过普通的泛型来解决这个问题。

@CryZe您要查找的内容与impl Trait无关。 有关我所知道的一切,请参见https://github.com/rust-lang/rfcs/issues/2413

它可能看起来像这样:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi啊,很有趣。 谢谢!

这应该工作吗?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

我们是否必须为existential_type功能保留语言中的existential关键字?

@jethrogb是的。 它目前没有的事实是一个错误。

@cramertj好的。 我应该为此提交一个单独的问题还是我的帖子足够?

提出问题会很棒,谢谢! :)

我们是否必须为existential_type功能保留语言中的existential关键字?

我认为目的是在实现 type-alias-impl-trait 特性时立即弃用它(即放入 lint)并最终将其从语法中删除。

有人也许可以澄清一下。

关闭它以支持更普遍地跟踪impl Trait的元问题: https :

没有一个关于如何使用 impl Trait 的好例子,非常可悲

此页面是否有帮助?
0 / 5 - 0 等级