5

不明白如何编写和设置 ruby​​ 3 RBS 类型检查。

假设我有一个类似的文件:

### file name: my_project/book.rb
class Book
  def initialize(pages)
    @pages = pages
  end
  
  def pages
    @pages
  end
end

first_book = Book.new(100)
puts first_book.pages

问题:.rbs在我的文件示例中,文件应该是什么样子?我应该运行什么 CLI 命令来测试它?例如$rbs my_project *或类似的东西?

4

2 回答 2

14

不明白如何编写和设置 ruby​​ 3 RBS 类型检查。

您在这里混淆了三个完全不同的事情:

  1. 类型语言
  2. 类型系统
  3. 类型检查

RBS 是#1,一种类型语言。这正是它听起来的样子:一种用于写下Types的语言

没有说明这些类型的含义(这将是一个Type System),它也没有说明一个程序是否相对于一个类型系统是良好类型的(这将是一个Type Checker)。

RBS 是基于观察到社区中已经存在多种类型检查器而开发的。例如,几乎每个 Ruby IDE 都包含一个类型检查器,以便能够提供更好的代码完成和“红色波浪线”。有冰糕。有陡峭。还有其他几个。

Ruby 社区中已经存在多种不兼容的类型语言。Ruby 核心库和标准库 RDoc 注释有时使用一种非正式的注释。YARD 有类型注释,但没有类型语言。(他们只给出了类型语言的示例)有些人使用示例类型语言编写 YARD 类型注释,因此请使用自制或修改过的。然后是 Sorbet 使用的 RBI。

RBS 背后的想法是提供一种类型语言作为 Ruby 语言的一部分,并提供一个用于查询该语言的 API。这将一方面允许现有类型检查器之间的互操作性,另一方面允许更多竞争。(目前,有一种锁定效应:每个类型检查器都使用不同的类型语言,因此如果不使用不同的类型语言从头开始重写类型签名,则无法轻松交换它们。如果它们使用通用类型语言,您可以轻松地将它们换掉并为您的用例选择最好的。)

RBS 项目的后半部分是为整个 Ruby 核心库和标准库提供一套完整的 RBS 签名。这是当前事务状态的另一个问题:每个类型检查器都必须一遍又一遍地这样做,他们都必须从头开始用自己的类型语言为 Ruby 核心和标准库编写类型签名。使用 RBS,将为 Ruby 核心和标准库提供一组标准的类型签名。

此外,为 Rails 之类的项目编写签名变得更加容易,因为您只需编写组签名,并且您知道它将适用于每个类型检查器、每个 IDE、每个 linter、每个静态分析器等。

问题:.rbs在我的文件示例中,文件应该是什么样子?

这取决于:你想说什么?

class Book[T]
  @pages: T
  def initialize: (pages: T) -> void
  def pages: () -> T
end

是一种可能的类型定义。

class Book[T]
  attr_reader pages: T
  def initialize: (pages: T) -> void
end

可能比第一个更好地捕捉您的意图。(因为我不知道你的意图,但只能看到你的代码,我不知道哪个更好。)

class Book
  attr_reader pages: 100
  def initialize: (pages: 100) -> void
end

是另一种可能的类型定义。这是仍然允许您的示例代码运行的最严格的类型签名。

class Book
  attr_reader pages: Numeric
  def initialize: (pages: Numeric) -> void
end

也是可以的,原样

class Book
  attr_reader pages: Integer
  def initialize: (pages: Integer) -> void
end

当然,这也总是有效的,尽管没有用,因为它没有告诉我们任何我们还不知道的事情:

class Book
  attr_reader pages: untyped
  def initialize: (pages: untyped) -> untyped
end

从技术上讲,您代码中的两种方法都采用可选块,而我们在这里不允许这样做。为了完全匹配代码的语义,签名可能应该是这样的:

class Book[T]
  @pages: T
  def initialize: (pages: T) ?{ (*args: void) -> void } -> void
  def pages: () ?{ (*args: void) -> void } -> T
end

最后一个是与您的代码当前所做和允许的最匹配的一个。这是否也准确地捕获了您希望代码执行和允许的操作,这是一个完全不同的问题,只有您可以回答。

我应该运行什么 CLI 命令来测试它?

目前尚不清楚“测试它”是什么意思。如果您的意思是“类型检查”,那么正如我之前所说:RBS不是类型检查器,它只是一种用于编写类型的语言,它不知道也不关心您对这些类型做了什么。

然而, RBS确实有一个测试模式,但它的作用有所不同:它可以查看您正在运行的代码,并查看您的代码在运行时是否违反了任何类型约束。但是,这不是静态类型检查。

例如,如果您有此签名:

class Foo
  def bar:  () -> Integer
  def quux: () -> Integer
end

这个代码:

class Foo
  def bar;  23           end
  def quux; 'fourty-two' end
end

foo = Foo.new
foo.bar

不会抱怨,因为它从未观察到Foo#quux在运行时违反类型约束。

如果您有以下定义Foo#quux

def quux; if rand < 0.5 then 42 else 'fourty-two' end end

然后它有时会抱怨,有时不会,只要你真的打电话quux到某个地方。

因此,您是否发现任何问题取决于您的测试覆盖率。如果您有一个方法在使用特定的参数组合调用时返回错误的类型,但您在测试中从未使用这种特定的参数组合调用它,那么您将永远不会注意到。

请注意,这与其说是测试代码的正确性,不如说是测试类型签名的正确性。

例如$rbs my_project *或类似的东西?

RBS 附带的工具主要设计为库,因为预计它们将被集成到文档工具(以在文档中显示类型签名)、测试工具(以测试您的类型签名)、IDE(用于代码完成) ),或在类型检查器中。

例如,我上面提到的测试工具打算这样使用:

RBS_TEST_TARGET='Foo::*' bundle exec ruby -r rbs/test/setup test/foo_test.rb

什么rbs/test/setup会做,非常愚蠢地只是要求Foo.constants.grep(Module),然后对于每个模块mod.instance_methods(false),用一个包装器替换每个方法,该包装器检查方法入口和出口的类型并调用原始方法。(使用与 ActiveSupport 相同的技术alias_method_chain。)

如您所见,这对命令行不太友好,它旨在作为库集成到测试工具中。

几个命令行工具可用:

  • rbs ast以 JSON 格式打印出当前环境的 AST
  • rbs list打印出当前环境中的类型列表
  • rbs ancestors打印出模块的祖先
  • rbs methods打印出模块的方法
  • rbs method打印出一个方法
  • rbs validate验证 RBS 文件的语法并进行一些基本的语义完整性检查
  • rbs constant执行持续查找
  • rbs paths打印出 RBS 查找签名文件的路径
  • rbs prototype可以生成骨架类型签名以帮助您入门,它可以从您的代码或已经存在的 RBI 类型签名执行此操作。(RBI 是 Sorbet 类型检查器使用的类型语言。)
  • rbs vendor供应商签名文件到项目目录
  • rbs parse解析 RBS 文件并打印语法错误
  • rbs test使用上述注入的测试钩子运行您的测试

但是,请注意,此 CLI并非旨在作为 RBS 的主要接口。自述文件明确指出:

gem 附带rbs命令行工具以演示它可以做什么并帮助开发 RBS。

CLI 旨在作为如何使用 API的示例,而不是作为 API 的主要入口点。

于 2020-10-11T10:37:19.857 回答
2

我尝试使用 ruby​​ 3.0.0 制作一个简单的示例,以使用 rbs 语法测试静态分析。

1.红宝石文件

# book.rb
class Book
  def initialize(pages)
    @pages = pages
  end

  def pages
    @pages
  end
end

first_book = Book.new(100)
puts first_book.pages

2.生成rbs文件

typeprof book.rb > book.rbs

3. 使用陡峭的 CLI 作为类型检查器。

首先,我们需要这个配置文件:

# Steepfile
target :lib do
  signature "."
  check "book.rb"
end

并运行:

steep check

空消息意味着静态类型检查器中没有错误。要重现静态类型错误,我们可以使用字符串调用初始化程序

# book.rb
Book.new("100")

steep check抛出以下错误

book.rb:11:22: ArgumentTypeMismatch: receiver=singleton(::Book), expected=::Integer, actual=::String ("100")

通过这个例子,我们可以证明我们可以使用 rbs 类型定义来验证我们的 ruby​​ 程序。

于 2020-12-26T00:20:40.600 回答