Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby 3 RBS type checking

Tags:

ruby

rbs

Don't understand how to write and setup ruby 3 RBS type checking.

Let's say I have a similar file:

### 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

The question: What should a .rbs file look like to my file example? And what CLI command should I run to test it ? For example $rbs my_project * or something like this ?

like image 375
Daishiro Avatar asked Oct 10 '20 16:10

Daishiro


1 Answers

Don't understand how to write and setup ruby 3 RBS type checking.

You are confusing three completely different things here:

  1. A Type Language
  2. A Type System
  3. A Type Checker

RBS is #1, a Type Language. It is exactly what it sounds like: a Language for writing down Types.

It says nothing about what those types mean (that would be a Type System), and it says nothing about whether or not a program is well-typed with respect to a type system (that would be a Type Checker).

RBS was developed based on the observation that there are multiple type checkers in the community already. E.g. almost every Ruby IDE contains a type checker to be able to provide better code completion and "red squigglies". There is Sorbet. There is Steep. There are a couple of others.

There are multiple incompatible type languages already in the Ruby community. The Ruby core library and standard library RDoc comments sometimes use an informal one. YARD has type annotations, but no type language. (They only give an example of what a type language could look like.) Some people write YARD type annotation using the example type language, so use a homegrown or modified one. Then there is RBI, used by Sorbet.

The idea behind RBS is to provide one Type Language as part of the Ruby Language with one API for querying that language. This will allow on the one hand interoperability between existing type checkers and on the other hand more competition. (At the moment, there is a lock-in effect: every type checker uses a different type language, so you can't easily exchange them without rewriting your type signatures from scratch in a different type language. If they use a common type language, you can easily swap them out and choose the best one for your use case.)

The second half of the RBS project is to ship a complete set of RBS signatures for the whole Ruby core library and standard library. That is another problem with the current state of affairs: every type checker has to do this over and over and over again, they all have to write type signatures for the Ruby core and standard libraries in their own type language from scratch. With RBS, there is going to be one standard set of type signatures for the Ruby core and standard libraries.

Also, writing signatures for projects like Rails becomes much easier because you only have to write one set of signatures and you know that it will work with every type checker, every IDE, every linter, every static analyzer, etc.

The question: What should a .rbs file look like to my file example?

That depends: what is it that you want to say?

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

Is one possible type definition.

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

might be a slightly better one that captures your intent better than the first one. (Since I don't know your intent but can only see your code, I can't tell which is better.)

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

Is another possible type definition. This is the strictest possible type signature that will still allow your sample code to run.

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

Is also possible, as is

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

And of course, this is also always valid, albeit useless since it doesn't tell us anything that we don't already know:

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

Technically speaking, both of the methods in your code take an optional block, whereas we have disallowed that here. To match the semantics of your code exactly, the signature should probably be this:

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

This last one is the one that most closely matches what your code currently does and allows. Whether this also accurately captures what you want your code to do and allow, that is a completely different question which only you can answer.

And what CLI command should I run to test it ?

It is not quite clear what mean by "test it". If you mean "type check it", then as I said before: RBS is not a type checker, it is simply a language for writing types, it doesn't know and doesn't care what you do with those types.

RBS does have a test mode, however, but that does something different: it can look at your running code and see whether your code violates any of the type constraints at runtime. However, this is not static type checking.

For example, if you have this signature:

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

and this code:

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

foo = Foo.new
foo.bar

It will not complain, because it never observed Foo#quux violating the type constraint at runtime.

And if you have this definition of Foo#quux instead:

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

Then it will sometimes complain and sometimes not, provided that you actually call quux somewhere.

So, whether or not you catch any problems depends on your test coverage. If you have a method that returns the wrong type when called with a particular combination of arguments, but you never call it with this particular combination of arguments in your tests, then you will never notice.

Note that this is not so much testing the correctness of your code, it is more for testing the correctness of your type signatures.

For example $rbs my_project * or something like this ?

The tools that ship with RBS are primarily designed as libraries, because it is expected that they are going to be integrated in documentation tools (to show type signatures in documentation), test harnesses (to test your type signatures), IDEs (for code completion), or in type checkers.

For example, the testing tool I mentioned above is intended to be used like this:

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

What rbs/test/setup will do, is very stupidly just ask for Foo.constants.grep(Module), then for each of those modules do mod.instance_methods(false) and literally replace each method with a wrapper that checks the types at method entry and exit and calls the original method. (Using the same technique that ActiveSupport's alias_method_chain uses.)

As you can see, this is not very command line friendly, it is intended to be integrated into the test harness as a library.

There are a couple of command line tools available:

  • rbs ast prints out an AST of the current environment in JSON format
  • rbs list prints out a list of the types in the current environment
  • rbs ancestors prints out the ancestors of a module
  • rbs methods prints out the methods of a module
  • rbs method prints out a method
  • rbs validate validates the syntax of RBS files and does some basic semantic sanity checks
  • rbs constant performs constant lookup
  • rbs paths prints out the paths where RBS looks for signature files
  • rbs prototype can generate a skeleton type signature to get you started, it can do this either from your code or from an already existing RBI type signature. (RBI is the type language used by the Sorbet type checker.)
  • rbs vendor vendors signature files into the project directory
  • rbs parse parses an RBS file and prints syntax errors
  • rbs test runs your tests with the above-mentioned test hooks injected

However, note that this CLI is not intended as the primary interface to RBS. The README explicitly says:

The gem ships with the rbs command line tool to demonstrate what it can do and help develop RBS.

The CLI is meant as an example for how to use the API, not as the primary entry point into the API.

like image 129
Jörg W Mittag Avatar answered Nov 15 '22 06:11

Jörg W Mittag