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

The State of Ruby 3 Typing

The State of Ruby 3 Typing

RubyConf 2020.

Soutaro Matsumoto

November 17, 2020
Tweet

More Decks by Soutaro Matsumoto

Other Decks in Programming

Transcript

  1. The State of Ruby 3 Typing
    Soutaro Matsumoto

    @soutaro

    View full-size slide

  2. The State of Ruby 3 Typing
    • Ruby 3 will ship with a feature to support type checkers.

    • A type de
    f
    i
    nition language RBS and rbs gem.

    • RBS type de
    f
    i
    nitions of standard libraries.

    • Several type checkers are available for Ruby.

    • You choose the best type checker for your projects.

    View full-size slide

  3. Type Checkers for Ruby
    Steep (@soutaro)
    TypeProf (@mame)
    Sorbet (Stripe)
    The most widely used type checker for Ruby.
    A static type checker for Ruby written in Ruby.
    Infers types in Ruby programs.

    (Bundled with Ruby 3.)
    RDL (Je
    f
    f
    Foster)
    Research project from Tufts University.

    View full-size slide

  4. 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 full-size slide

  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
    Does new method accept a String argument?

    View full-size 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
    Does new method accept a String argument?
    What is the type of the value of #talks?

    View full-size 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?
    What is the type of the value of #talks?
    What is the type of the speaker?

    View full-size slide

  8. What is RBS?
    • A language to de
    f
    i
    ne types for Ruby programs.

    • Classes, modules, methods, instance variables, mixins, ...

    • A library and assets to help type checker developments.

    View full-size slide

  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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  15. https://github.com/ruby/rbs

    View full-size slide

  16. RBS for Gems
    • The main targets of RBS are Stdlib types and Gem types.
    Written by Used by
    Stdlib types Ruby committers (community) All Ruby developers
    Gem types Gem authors, community Gem users
    App types The developer of the app The developer of the app

    View full-size slide

  17. Ruby
    Gem types in RBS


    Stdlib types in RBS
    App types App types App types
    Type checker A Type checker B Type checker C

    View full-size slide

  18. Ruby
    Gem types in RBS


    Stdlib types in RBS
    App types App types App types in RBS
    Type checker A Type checker B Type checker C
    Lib types

    View full-size slide

  19. Type Checkers and RBS
    Steep
    TypeProf
    Sorbet
    Uses RBI as it's native type declaration language.

    Will support RBS too. (Maybe only a subset.)
    Uses RBS as the primary type language.
    Reads library types from RBS and generates RBS.

    View full-size slide

  20. FAQ
    • Why it requires writing types in other
    f
    i
    les?

    View full-size slide

  21. Why in Different Files?
    • To avoid writing types in Ruby code.

    • Matz believes type annotations will be outdated in future and wants no
    types written in Ruby code.

    • To keep type checking optional in Ruby.

    • To support libraries written in C.

    View full-size slide

  22. Inline Type Annotations
    • Types in Ruby code is out of scope of RBS.

    • We know type checkers need inline type annotations.
    class Conference


    extend T::Sig


    sig do


    params(name: String).void


    end


    def initialize(name:)


    @name = name


    @speakers = T.let([], T::Array[Speaker])



    class Conference


    # Method types are in RBS in Steep.


    def each_speaker(&block)


    # @type var speakers: Array[Speaker]


    speakers = []


    talks.each do |talk|


    speakers << talk.speaker


    end


    Sorbet Steep

    View full-size slide

  23. RBS of Gems
    • Type checkers need type de
    f
    i
    nitions of libraries == We need RBS of gems!

    • Distributing RBS

    • Releasing gems with RBS

    • RBS of existing gems

    • Writing RBS

    View full-size slide

  24. Releasing Gems with RBS
    • Add sig directory and put RBS
    f
    i
    les there.

    View full-size slide

  25. RBS of Existing Gems
    • Write RBS for gems and publish it in https://github.com/ruby/gem_rbs

    • Save RBS
    f
    i
    les in gems/gem_name/version directory

    View full-size slide

  26. RBS of Existing Gems
    • Multiple versions supported

    • Copy the version directories to make new version

    • Optimistic version resolution

    • Incompatible with semantic versioning

    • Required version 3.2.1 will resolve to 4.2 if there is no better version
    available

    View full-size slide

  27. Writing RBS of Gems
    • You can generate RBS from Ruby code

    • Sord will support generating RBS from YARD docs
    $ rbs prototype rb lib/**/*.rb


    $ rbs prototype runtime -r goodcheck


    $ typeprof exe/goodcheck

    View full-size slide

  28. module Goodcheck


    class ImportLoader


    attr_reader cache_path: untyped


    attr_reader expires_in: untyped


    def initialize: (cache_path: untyped cache_path,


    force_download: untyped force_download,


    config_path: untyped config_path,


    ?expires_in: untyped expires_in) -> untyped


    def load: (untyped name) { () -> untyped } -> untyped


    def load_file: (untyped path) { (untyped) -> untyped } -> untyped


    end


    end
    $ rbs prototype rb lib/**/*.rb

    View full-size slide

  29. module Goodcheck


    class ImportLoader


    attr_reader cache_path : Pathname


    attr_reader expires_in : Integer


    def initialize : (cache_path: Pathname,


    force_download: bool,


    config_path: Pathname,


    ?expires_in: Integer) -> Pathname


    def load : (untyped) ?{ (String) -> Array[untyped] } -> (Array[String] | Integer)


    def load_file : (untyped) ?{ (String) -> Array[untyped] } -> Array[String]


    end


    end
    $ typeprof exe/goodcheck

    View full-size slide

  30. The State of Ruby 3 Typing
    • Ruby 3 will ship with a feature to support type checkers.

    • A type de
    f
    i
    nition language RBS and rbs gem.

    • RBS type de
    f
    i
    nitions of standard libraries.

    • Several type checkers are available for Ruby.

    • You choose the best type checker for your projects.

    • https://github.com/ruby/gem_rbs

    View full-size slide