【问题标题】:Why can't Rust do more complex type inference? [closed]为什么 Rust 不能做更复杂的类型推断? [关闭]
【发布时间】:2019-01-09 12:42:41
【问题描述】:

Rust 编程语言Chapter 3 中,以下代码用作Rust 无法管理的类型推断的示例:

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {}", number);
}

解释如下:

Rust 需要在编译时明确地知道 number 变量是什么类型,因此它可以在编译时验证它的类型在我们使用 number 的任何地方都有效。如果 number 的类型仅在运行时确定,Rust 将无法做到这一点;如果编译器必须跟踪任何变量的多个假设类型,编译器会更复杂,并且对代码的保证更少。

我不确定我是否理解其中的原理,因为该示例确实看起来像是一个简单的编译器可以推断出类型的东西。

究竟是什么让这种类型推断如此困难?在这种情况下,condition 的值可以在编译时清楚地推断出来(这是真的),因此数字的类型也可以是(i32?)。

如果您尝试跨多个编译单元推断类型,我可以看到事情如何变得更加复杂,但是这个特定示例是否会增加很多编译器的复杂度?

【问题讨论】:

  • @Shepmaster 我认为这与类型推断有关,因为用于学习该语言的规范书籍明确表示它与类型推断有关......类型不需要在运行时更改,因为它可以在给定示例的编译时显式推断。
  • 这不是类型推断问题。即使你明确输入let number: i32 = ...,它仍然不会编译。
  • 处理这种情况似乎很简单——假设这个特性存在;在什么情况下你会觉得它有用?你不能依赖它,因为一旦条件没有被简单地评估,那么你的代码就不能再编译了。
  • 我还觉得有趣的是你决定不引用前面两句话(强调我的): if 块中的表达式计算为整数,而else 块计算为字符串。这是行不通的,因为变量必须只有一个类型。就像 SilvioMayolo 提到的那样,您可以在此处为所有内容指定显式类型,从而消除任何推断。
  • 没有敌意,只是真实的、诚实的困惑。您更愿意使用typeof(if A { X } else { Y })AXY 确定的语言,而不仅仅是X(只要X == Y),这让我很困惑。编译器编写者没有理由花时间研究这些看似没有人想要的用途如此有限的功能。

标签: rust type-inference


【解决方案1】:

我能想到的主要原因有三个:

1。远距离动作效果

让我们假设语言是这样工作的。由于我们正在扩展类型推断,我们不妨让语言更加智能,并让它推断返回类型。这使我可以编写如下内容:

pub fn get_flux_capacitor() {
  let is_prod = true;

  if is_prod { FluxCapacitor::new() } else { MovieProp::new() }
}

在我项目的其他地方,我可以通过调用该函数获得FluxCapacitor。但是,有一天,我将is_prod 更改为false。现在,我将在 每个 调用站点收到错误,而不是我的函数返回错误类型的错误。一个函数中的一个小改动会导致完全未更改的文件出现错误!这很奇怪。

(如果我们不想添加推断返回类型,可以想象它是一个很长的函数。)

2。编译器内部暴露

如果不是那么简单,会发生什么?当然这应该和上面的例子一样:

pub fn get_flux_capacitor() {
  let is_prod = (1 + 1) == 2;

  ...
}

但这能延伸多远?编译器的常量传播主要是一个实现细节。您不希望程序中的类型依赖于这个版本的编译器的智能程度。

3。你到底是什么意思?

作为查看此代码的人类,似乎缺少某些内容。你为什么要在true 上分支?为什么不直接写FluxCapacitor::new()?也许缺少逻辑来检查是否缺少 env=DEV 环境变量。也许实际上应该使用 trait 对象,以便您可以利用运行时多态性。

在这种情况下,您要求计算机做一些看起来不太正确的事情,Rust 经常选择举手并要求您修复代码。

【讨论】:

  • 您能说明一下 Rust 语言选择了哪些原因吗?这些都是基于意见的。
  • 编译器内部暴露——这只能通过const fn来解决
  • 值得注意的是,“编译时常量”的概念并不是 C 或 C++ 的实现细节。
  • @DietrichEpp 即使在那儿,也确实如此。标准保证了其中的一部分,但大多数编译器能够做的比标准要求的要多得多(这很好),并且有时会在用户不知情的情况下公开它(这不太好)。在 C++ constexprs 上的工作使得处理这个更简单、更一致,但我很确定一些编译器会比其他编译器接受更多,即使它们都声称遵循标准。
  • @mcarton:所以你是说一些编译器更宽容,接受其他编译器不接受的代码,因此最好不要在类型检查器中进行任何常量折叠?我不确定我是否遵循这个论点。一些编译器比其他编译器更宽松的事实是普遍存在的并且在许多方面影响程序,编译时常量的标识似乎并不特别。如今,“取决于你的编译器的这个版本有多聪明”的想法太过分了。
【解决方案2】:

你说得对,在这种非常特殊的情况下(condition=true 是静态的),编译器可以检测到 else 分支不可访问,因此 number 必须为 5。

这只是一个人为的例子,不过……在更一般的情况下,condition 的值只能在运行时动态知道。 正如其他人所说,在这种情况下,推理变得难以实施。

关于那个话题,有两件事我还没有看到。

  1. Rust 语言设计倾向于在做事方面犯错 尽可能明确
  2. Rust 类型推断只是局部的

在第 1 点,Rust 处理“这种类型可以是多种类型之一”用例的显式方法是 enums。 你可以这样定义:

#[derive(Debug)]
enum Whatsit {
    Num(i32),
    Text(&'static str),
}

然后做let number = if condition { Num(5) } else { Text("six") };

关于第 2 点,让我们看看枚举(虽然更冗长)是该语言中的首选方法。在书中的示例中,我们只是尝试打印number 的值。 在更真实的情况下,我们会在某一时刻使用 number 进行打印以外的其他事情。

这意味着将它传递给另一个函数或将它包含在另一个类型中。或者(甚至可以使用println!)在其上实现DebugDisplay 特征。本地推断意味着(如果你不能在 Rust 中命名 number 的类型),你将无法做任何这些事情。 假设你想创建一个函数来处理number; 使用您要编写的枚举:

fn do_something(number: Whatsit)

但没有它...

fn do_something(number: /* what type is this? */)

简而言之,您是对的,原则上编译器可以为number 合成一个类型。例如,编译器可能会在编译该代码时创建一个匿名枚举,如上面的Whatsit。 但是你——程序员——不知道那个类型的名字,不能引用它,甚至不知道你能用它做什么(我可以把两个“数字”相乘吗?),这将大大限制它的用处。

采用了类似的方法,例如为语言添加闭包。编译器会知道闭包的具体类型,但程序员却不知道。如果您有兴趣,我可以尝试找出有关该方法在语言设计中引入的困难的讨论。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-05
    • 1970-01-01
    • 1970-01-01
    • 2021-12-30
    • 1970-01-01
    相关资源
    最近更新 更多