【问题标题】:Why does Rust have a "Never" primitive type?为什么 Rust 有一个“从不”的原始类型?
【发布时间】:2018-08-14 01:03:38
【问题描述】:

Rust 的 std::process::exit 有这种类型

pub fn exit(code: i32) -> !

其中!"Never" primitive type

为什么 Rust 需要一个特殊的类型呢?

将此与 System.Exit.exitWith 类型的 Haskell 进行比较

exitWith :: forall a. Int -> a

对应的 Rust 签名是

pub fn exit<T>(code: i32) -> T

没有必要为不同的T 单态这个函数,因为T 永远不会实现,所以编译应该仍然可以工作。

【问题讨论】:

  • 重点是什么?到处写exit::&lt;!&gt; 而不仅仅是exit
  • "a T is never materialized" 但是类型签名实际上并没有这么说,它说 any T 可以实现。
  • @loganfsmyth 确实如此。底部的类型恰好是forall a. a,这正是! 的统一规则现在正在做的事情。来自! 的文档:“类型为 ! 的表达式将强制转换为任何其他类型”。 forall a. a 不是“将强制转换为任何类型”的适当类型吗?
  • 你不能仅仅不单态化一个泛型函数。对于它“返回”的任何类型,它至少必须遵守一些调用约定,因此exit::&lt;&amp;str&gt;() 将编译为不同于exit::&lt;f64&gt;() 的东西(至少在x86_64 上)。除非你建议 std::process::exit 应该是特例... not,这基本上是 ! 首先是
  • @ljedrz:我不会说它是重复的。此处的 OP 不质疑不同功能的使用,或者它们为什么出现,这就是您链接的问题及其答案的意义所在。相反,OP 正在质疑类型系统中分歧的编码,以及为什么在其他语言没有的情况下选择 Never。

标签: types rust


【解决方案1】:

TL;DR:因为它支持局部推理和可组合性。

您将exit() -&gt; ! 替换为exit&lt;T&gt;() -&gt; T 的想法仅考虑了类型系统和类型推断。你是对的,从类型推断的角度来看,两者都是等价的。然而,语言不仅仅是类型系统。

无意义代码的局部推理

! 的存在允许本地推理来检测无意义的代码。例如,考虑:

use std::process::exit;

fn main() {
    exit(3);
    println!("Hello, World");
}

编译器立即标记println! 语句:

warning: unreachable statement
 --> src/main.rs:5:5
  |
5 |     println!("Hello, World");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unreachable_code)] on by default
  = note: this error originates in a macro outside of the current crate
          (in Nightly builds, run with -Z external-macro-backtrace for more info)

怎么样?好吧,exit 的签名清楚地表明它永远不会返回,因为永远不会创建 ! 的实例,因此不可能执行它之后的任何内容。

优化的局部推理

同样,rustc 将有关 exit 签名的信息传递给 LLVM 优化器。

首先在exit的声明中:

; std::process::exit
; Function Attrs: noreturn
declare void @_ZN3std7process4exit17hcc1d690c14e39344E(i32) unnamed_addr #5

然后在使用现场,以防万一:

; playground::main
; Function Attrs: uwtable
define internal void @_ZN10playground4main17h9905b07d863859afE() unnamed_addr #0 !dbg !106 {
start:
; call std::process::exit
  call void @_ZN3std7process4exit17hcc1d690c14e39344E(i32 3), !dbg !108
  unreachable, !dbg !108
}

可组合性

在 C++ 中,[[noreturn]] 是一个属性。这真的很不幸,因为它没有与通用代码集成:对于有条件的noreturn 函数,您需要通过箍,并且选择noreturn 类型的方法与使用的库一样多。

在 Rust 中,! 是一流的构造,在所有库中都是统一的,最重要的是......即使在没有 ! 的情况下创建的库也可以正常工作。

最好的例子是Result 类型(Haskell 的Either)。它的完整签名是Result&lt;T, E&gt;,其中T 是预期类型,E 是错误类型。 !Result 中没有什么特别之处,但可以用! 实例化:

#![feature(never_type)]

fn doit() -> Result<i32, !> { Ok(3) }

fn main() {
    doit().err().unwrap();
    println!("Hello, World");
}

编译器可以看穿它:

warning: unreachable statement
 --> src/main.rs:7:5
  |
7 |     println!("Hello, World");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unreachable_code)] on by default
  = note: this error originates in a macro outside of the current crate
          (in Nightly builds, run with -Z external-macro-backtrace for more info)

可组合性(二)

推理无法实例化的类型的能力还扩展到推理无法实例化的枚举变体。

例如下面的程序编译:

#![feature(never_type, exhaustive_patterns)]

fn doit() -> Result<i32, !> {
    Ok(3)
}

fn main() {
    match doit() {
        Ok(v) => println!("{}", v),
        // No Err needed
    }

    // `Ok` is the only possible variant
    let Ok(v) = doit();
    println!("{}", v);
}

通常,Result&lt;T, E&gt; 有两个变体:Ok(T)Err(E),因此匹配必须考虑到这两个变体。

然而,由于! 不能被实例化,Err(!) 不能被实例化,因此Result&lt;T, !&gt; 有一个变体:Ok(T)。因此编译器只允许考虑Ok 的情况。

结论

编程语言比它的类型系统更多。

编程语言是关于开发人员将其意图传达给其他开发人员和机器。 Never 类型让开发者的意图更加清晰,让其他方可以清楚地理解开发者的意思,而不必从偶然的线索中重构意思。

【讨论】:

  • “意图”参数对我来说很有意义。我不确定您为什么要与 C++ 进行比较(“可组合性”点),因为我在问题中没有真正提到 C++,而 Haskell 解决方案已经完全可组合。
  • @theindigamer:正是因为 Haskell 解决方案是可组合的,我想展示一个不是的替代方案。
  • C++ 很容易成为替罪羊 :P.
  • @theindigamer:当然;它也是我最擅长的语言,所以我很容易找到我想要的例子。
  • @Matthieu 我不得不提一下,Haskell 编译器实际上可以在 prove 使用参数化的 forall a. t -&gt; a 类型的函数 ta 中不免费返回⊥在a该函数无法检查它已被实例化的a,仅使用对任何a有效的操作)。同样适用于forall a. t -&gt; Either b a 等。所以在这种情况下,类型系统实际上准确地表示正在发生的事情。不过,我不确定 Rust——Rust 的多态性是参数化的吗? (C++ 不是。)
【解决方案2】:

我认为 Rust 需要特殊类型 ! 的原因包括:

  1. 表面语言不提供任何编写 type Never = for&lt;T&gt;(T) 的方法,类似于 Haskell 中的 type Never = forall a. a

    更一般地说,在类型别名中,如果不将它们引入 LHS,就不能在 RHS 上使用类型变量(也称为泛型参数),这正是我们在这里想要做的。使用空的 struct/enum 没有意义,因为我们在这里需要一个类型别名,以便 Never 可以与任何类型统一,而不是新构造的数据类型。

    由于此类型无法由用户定义,因此它提供了将其作为基元添加可能有意义的一个原因。

  2. 如果在语法上允许将非单态类型分配给 RHS(例如 forall a. a),编译器将需要做出任意选择。调用约定(正如 cmets 中的 trentcl 所指出的那样),即使选择并不重要。 Haskell 和 OCaml 可以回避这个问题,因为它们使用统一的内存表示。

【讨论】:

    【解决方案3】:

    只是做一些补充:

    Never在Rust中是底层类型,Haskell也有它的底层类型,只是换了个名字而已。

    在类型论中,数学逻辑中的一种理论,底部类型是没有值的类型。它也被称为零或空类型,有时用向上(⊥)符号表示。

    https://en.wikipedia.org/wiki/Bottom_type

    大多数常用语言没有明确表示empty type 的方法,但它们隐含了它,因为代码可能会出现恐慌或无限循环。

    和网站更多内容:

    大多数常用语言无法明确表示 空类型。有一些值得注意的例外。

    自 Haskell2010 起,Haskell 支持空数据类型。因此,它允许 定义数据为空(没有构造函数)。 Empty 类型是 不是很空,因为它包含非终止程序和 未定义的常数。当你想要的时候,经常使用未定义的常量 有空类型的东西,因为 undefined 匹配任何类型 (所有类型的“子类型”也是如此),并试图评估 undefined 将导致程序中止,因此它永远不会返回 一个答案。

    在 Scala 中,底部类型表示为 Nothing。除了用于 只抛出异常或不返回的函数 通常,它也用于协变参数化类型。为了 例如,Scala 的 List 是一个协变类型构造函数,所以 List[Nothing] 是所有类型 A 的 List[A] 的子类型。所以 Scala 的 Nil, 用于标记任何类型列表结尾的对象,属于 键入 List[Nothing]。

    在 Rust 中,底部类型称为 never 类型,用 ! 表示。 它存在于函数的类型签名中,保证永远不会 返回,例如通过调用 panic!() 或永远循环。也是 某些控制流关键字的类型,例如 break 和 return, 它不会产生价值,但仍然可以用作 [4]

    在 TypeScript 中,最底层的类型是 never。[7][8]

    在带有闭包编译器注解的 JavaScript 中,底部类型是 !Null(字面意思是 Null 单元类型的非 null 成员)。

    在 PHP 中,最底层的类型是 never。

    在 Python 中,底部类型是 typing.NoReturn.[9]

    【讨论】:

      猜你喜欢
      • 2019-01-30
      • 2021-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-30
      • 2014-01-26
      相关资源
      最近更新 更多