Upgrade to Pro — share decks privately, control downloads, hide ads and more …

An Introduction to Static Typing in Ruby 3

An Introduction to Static Typing in Ruby 3

A talk at a NYC.rb meetup.

Soutaro Matsumoto

February 11, 2021
Tweet

More Decks by Soutaro Matsumoto

Other Decks in Programming

Transcript

  1. Soutaro Matsumoto • One of the Ruby core committers •

    Develops RBS, Steep, and rbs_protobuf • Works for Square • @soutaro on GitHub/Twitter
  2. Ruby 3.0 • Released Dec 25, 2020 🎉🎄🎅 • Key

    updates • Performance with MJIT • Concurrency with Ractor and Fiber Scheduler • Static typing with RBS and TypeProf ← 👀 https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/
  3. Static Typing in Ruby 3 • Ruby 3 ships with

    RBS • RBS gem and type de fi nitions of standard libraries • RBS is not a type checker, but a language, fi les, or a library • Several RBS-based tools are available • You choose the best tools for your projects
  4. # The conference object represents a conference. # # conf

    = Conference.new("RubyConf 2020") # conf.talks << talk1 # conf.talks << talk2 # class Conference # The name of the conference. attr_reader name: String # Talks of the conference. attr_reader talks: Array[Talk] def initialize: (String name) -> void # Yields all speakers of the conference. # Deduped, and no order guaranteed. def each_speaker: { (Speaker) -> void } -> void | () -> Enumerator[Speaker, void] end
  5. Static Type Checking conference = Conference.new("RubyConf 2020") talks = [...]

    conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end
  6. Static Type Checking conference = Conference.new("RubyConf 2020") talks = [...]

    conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end Does new method accept a String argument?
  7. Static Type Checking conference = Conference.new("RubyConf 2020") talks = [...]

    conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end Does new method accept a String argument? What is the type of the value of #talks?
  8. Static Type Checking conference = Conference.new("RubyConf 2020") talks = [...]

    conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end Does new method accept a String argument? What is the type of the value of #talks? What is the type of the speaker?
  9. # The conference object represents a conference. # # conf

    = Conference.new("RubyConf 2020") # conf.talks << talk1 # conf.talks << talk2 # class Conference # The name of the conference. attr_reader name: String # Talks of the conference. attr_reader talks: Array[Talk] def initialize: (String name) -> void # Yields all speakers of the conference. # Deduped, and no order guaranteed. def each_speaker: { (Speaker) -> void } -> void | () -> Enumerator[Speaker, void] end conference = Conference.new("RubyConf 2020") talks = [...] conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end
  10. # The conference object represents a conference. # # conf

    = Conference.new("RubyConf 2020") # conf.talks << talk1 # conf.talks << talk2 # class Conference # The name of the conference. attr_reader name: String # Talks of the conference. attr_reader talks: Array[Talk] def initialize: (String name) -> void # Yields all speakers of the conference. # Deduped, and no order guaranteed. def each_speaker: { (Speaker) -> void } -> void | () -> Enumerator[Speaker, void] end conference = Conference.new("RubyConf 2020") talks = [...] conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end
  11. # The conference object represents a conference. # # conf

    = Conference.new("RubyConf 2020") # conf.talks << talk1 # conf.talks << talk2 # class Conference # The name of the conference. attr_reader name: String # Talks of the conference. attr_reader talks: Array[Talk] def initialize: (String name) -> void # Yields all speakers of the conference. # Deduped, and no order guaranteed. def each_speaker: { (Speaker) -> void } -> void | () -> Enumerator[Speaker, void] end conference = Conference.new("RubyConf 2020") talks = [...] conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end
  12. # The conference object represents a conference. # # conf

    = Conference.new("RubyConf 2020") # conf.talks << talk1 # conf.talks << talk2 # class Conference # The name of the conference. attr_reader name: String # Talks of the conference. attr_reader talks: Array[Talk] def initialize: (String name) -> void # Yields all speakers of the conference. # Deduped, and no order guaranteed. def each_speaker: { (Speaker) -> void } -> void | () -> Enumerator[Speaker, void] end conference = Conference.new("RubyConf 2020") talks = [...] conference.talks.push(*talks) conference.each_speaker do |speaker| puts "%s (%s)" % [speaker.name, speaker.email] end
  13. RBS Features Union types Generics Interface types untyped type String

    | Symbol Array[String | Integer] Integer? #== Integer | nil Array[Integer] Hash[String, Array[Integer]] def eval: (String) -> untyped interface _IntegerConvertible def to_int: () -> Integer end
  14. RBS Based Tools • Type checkers • Steep • rbs-test

    • RBS generators • rbs-prototype • TypeProf, • rbs_rails • Sord • rbs_protobuf • Others • gem_rbs_collection • rbs2ts • (Sorbet)
  15. Steep • Static type checker for Ruby written in Ruby

    based on RBS • You write types of your Ruby program in RBS, and Steep checks the consistency between the code and RBS • https://github.com/soutaro/steep $ steep check
  16. Type Checking with Steep • VSCode integration • On the

    fl y error reporting • Completion • Hover to show meta-info
  17. rbs-prototype $ rbs prototype rb lib/goodcheck/import_loader.rb $ rbs prototype runtime

    -rgoodcheck 'Goodcheck::ImportLoader' • Detect classes and methods and generate RBS for them • All types are left untyped module Goodcheck class Analyzer attr_reader rule: untyped attr_reader trigger: untyped attr_reader buffer: untyped def initialize: (rule: untyped rule, trigger: untyped trigger, buffer: untyped buffer) -> untyped def scan: () { (untyped) -> untyped } -> untyped def scan_simple: (untyped regexp) { (untyped) -> untyped } -> untyped def scan_var: (untyped pat) { (untyped) -> untyped } -> untyped end
  18. rbs_rails • Generates RBS fi les for Active Record models

    and URL helpers • It knows how associations and attributes are represented as Ruby objects in Rails app • https://github.com/pocke/rbs_rails $ rake rbs_rails:generate_rbs_for_models \ 
 rbs_rails:generate_rbs_for_path_helpers
  19. class Logfile < ApplicationRecord extend _ActiveRecord_Relation_ClassMethods[Logfile, Logfile::ActiveRecord_Relation] attr_accessor id ():

    Integer def id_changed?: () -> bool def id_change: () -> [Integer?, Integer?] def id_will_change!: () -> void def id_was: () -> Integer? def id_previously_changed?: () -> bool def id_previous_change: () -> Array[Integer?]? def id_previously_was: () -> Integer? def restore_id!: () -> void class Logfile < ApplicationRecord belongs_to :report validates :report_id, :presence => true validates :ext, :uniqueness => { :scope => :report_id }, :inclusion => { :in => %w[log.txt diff.txt log.html diff.html], :message => "%{value} is not a valid ext" } validates :data, :presence => true def uri r = report t = r.datetime.strftime('%Y%m%dT%H%M%SZ') "#{r.server.uri}ruby-#{r.branch}/log/#{t}.#{ext}.gz" end end
  20. gem_rbs_collection • RBS fi les for Ruby gems • RBS

    supports loading RBS fi les from a gem, but this is for gems without RBS fi les • == De fi nitelyTyped in TypeScript, typeshed in Python https://github.com/ruby/gem_rbs_collection
  21. Work fl ow 1. Generate RBS with rbs_rails 2. Choose

    a class from your project 3. Write RBS for the class (or generate with rbs-prototype) 4. Run Steep to con fi rm your RBS and Ruby code are consistent 5. Goto 2
  22. Start Small • I don't recommend generating RBS and running

    the tool for everything • Many type errors will be reported and you will give up • You can type check gradually # This won't work well ⚠ $ rbs prototype rb lib/**/*.rb $ steep check
  23. RBS Based Tools • Type checkers • Steep • rbs-test

    • RBS generators • rbs-prototype • TypeProf, • rbs_rails • Sord • rbs_protobuf • Others • gem_rbs_collection • rbs2ts • (Sorbet)
  24. rbs-test • Runtime type checking (not static) • Hooks method

    calls and type checks if arguments/return values have expected types • Pros: Easier to use compared to Steep
 Cons: Slow, unsupported features, incompatibilities $ rbs test --target='Goodcheck::*' \ ruby -Ilib:test test/config_loader_test.rb ... ERROR["test_load_config"] test_load_config#ConfigLoaderTest (0.03s) Minitest::UnexpectedError: TypeError: [Goodcheck::Pattern::Regexp#initialize] ArgumentTypeError: expected `bool` but given `nil` ...
  25. rbs-test • Runtime type checking (not static) • Hooks method

    calls and type checks if arguments/return values have expected types • Pros: Easier to use compared to Steep
 Cons: Slow, unsupported features, incompatibilities $ rbs test --target='Goodcheck::*' \ ruby -Ilib:test test/config_loader_test.rb ... ERROR["test_load_config"] test_load_config#ConfigLoaderTest (0.03s) Minitest::UnexpectedError: TypeError: [Goodcheck::Pattern::Regexp#initialize] ArgumentTypeError: expected `bool` but given `nil` ... Class patterns to type check
  26. rbs-test • Runtime type checking (not static) • Hooks method

    calls and type checks if arguments/return values have expected types • Pros: Easier to use compared to Steep
 Cons: Slow, unsupported features, incompatibilities $ rbs test --target='Goodcheck::*' \ ruby -Ilib:test test/config_loader_test.rb ... ERROR["test_load_config"] test_load_config#ConfigLoaderTest (0.03s) Minitest::UnexpectedError: TypeError: [Goodcheck::Pattern::Regexp#initialize] ArgumentTypeError: expected `bool` but given `nil` ... Class patterns to type check Command line to run Ruby program
  27. TypeProf • Generates RBS fi les from Ruby code. •

    Much smarter than rbs-prototype. • Execute the Ruby script at type level, and print the result. Ruby script to start execution $ typeprof exe/goodcheck
  28. module Goodcheck class Analyzer attr_reader rule: untyped attr_reader trigger: untyped

    attr_reader buffer: untyped def initialize: (rule: untyped rule, trigger: untyped trigger, buffer: untyped buffer) -> untyped def scan: () { (untyped) -> untyped } -> untyped def scan_simple: (untyped regexp) { (untyped) -> untyped } -> untyped def scan_var: (untyped pat) { (untyped) -> untyped } -> untyped end end $ rbs prototype rb module Goodcheck class Analyzer attr_reader rule: untyped attr_reader trigger: untyped attr_reader buffer: Buffer def initialize: (rule: untyped, trigger: untyped, buffer: Buffer) -> Buffer def scan: ?{ (Issue) -> Array[bot]? } -> ((Array[Issue] | Enumerator[Issue, untyped] | Enumerator def scan_simple: (Regexp regexp) ?{ (Issue) -> Array[bot]? } -> Array[Issue]? def scan_var: (untyped pat) ?{ (Issue) -> untyped } -> nil end end $ typeprof
  29. Sord and rbs_protobuf • Sord reads YARD docs and generate

    RBS (and RBI) fi les using the types included in the comments • https://github.com/AaronC81/sord $ sord defs.rbs • rbs_protobuf runs as a plugin of protoc and translates .proto to RBS type de fi nition • https://github.com/square/rbs_protobuf $ protoc --rbs_out=sig/protos \ protos/a.proto Refer types written by human
  30. rbs2ts • Converts RBS type de fi nitions to TypeScript

    types • https://github.com/mugi-uno/rbs2ts $ rbs2ts convert type.rbs
  31. Sorbet • The most widely used static type checker for

    Ruby • They plan to support reading (a subset of) RBS • RBI will continue to be the primary type de fi nition format in Sorbet • Supporting RBS will help using gems even without RBI fi les
  32. Recap • Ruby 3 ships with RBS and there are

    tools based on RBS • Waiting for your contribution • Writing RBS of gems and push it to gem_rbs_collection • Generating API docs from RBS • I want more Steep users • Let me help you to start type checking your Ruby code using Steep