$30 off During Our Annual Pro Sale. View Details »

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. An Introduction to Static Typing
    in Ruby 3
    Soutaro Matsumoto

    @soutaro

    View Slide

  2. Soutaro Matsumoto
    • One of the Ruby core committers

    • Develops RBS, Steep, and rbs_protobuf

    • Works for Square

    • @soutaro on GitHub/Twitter

    View Slide

  3. 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/

    View Slide

  4. 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

    View Slide

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

    View Slide

  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

    View Slide

  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?

    View Slide

  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?

    View Slide

  9. 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?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  14. 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

    View Slide

  15. RBS Based Tools
    • Type checkers

    • Steep

    • rbs-test
    • RBS generators

    • rbs-prototype

    • TypeProf,

    • rbs_rails

    • Sord

    • rbs_protobuf
    • Others

    • gem_rbs_collection

    • rbs2ts

    • (Sorbet)

    View Slide

  16. 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

    View Slide

  17. Type Checking with Steep
    • VSCode integration

    • On the
    fl
    y error reporting

    • Completion

    • Hover to show meta-info

    View Slide

  18. 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




    View Slide

  19. 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

    View Slide

  20. 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


    View Slide

  21. 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

    View Slide

  22. Only 14 gems... 㱺 Chance for contribution! 🤩

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. RBS Based Tools
    • Type checkers

    • Steep

    • rbs-test
    • RBS generators

    • rbs-prototype

    • TypeProf,

    • rbs_rails

    • Sord

    • rbs_protobuf
    • Others

    • gem_rbs_collection

    • rbs2ts

    • (Sorbet)

    View Slide

  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`


    ...

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. rbs2ts
    • Converts RBS type de
    fi
    nitions to TypeScript types

    • https://github.com/mugi-uno/rbs2ts
    $ rbs2ts convert type.rbs

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. View Slide