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

An introduction to typed Ruby programming

An introduction to typed Ruby programming

GrillRB 2019, https://grillrb.com

Soutaro Matsumoto

August 31, 2019
Tweet

More Decks by Soutaro Matsumoto

Other Decks in Programming

Transcript

  1. An introduction to
    typed Ruby programming
    Soutaro Matsumoto

    View full-size slide

  2. Soutaro Matsumoto
    https://github.com/soutaro

    View full-size slide

  3. ম೑
    Yaki Niku (Grilled meat)

    View full-size slide

  4. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View full-size slide

  5. Ruby3 type checking
    • Detecting types of each Ruby expression statically to help developments

    • Performance improvement is not the goal (it needs much more precise
    analysis)
    conf = Conference.find_by!(name: "GrillRB", year: 2019)
    conf.talks.each do |talk|
    puts talk.speaker.email
    end

    View full-size slide

  6. Ruby3 type checking
    • Detecting types of each Ruby expression statically to help developments

    • Performance improvement is not the goal (it needs much more precise
    analysis)
    conf = Conference.find_by!(name: "GrillRB", year: 2019)
    conf.talks.each do |talk|
    puts talk.speaker.email
    end
    ::Conference

    View full-size slide

  7. Ruby3 type checking
    • Detecting types of each Ruby expression statically to help developments

    • Performance improvement is not the goal (it needs much more precise
    analysis)
    conf = Conference.find_by!(name: "GrillRB", year: 2019)
    conf.talks.each do |talk|
    puts talk.speaker.email
    end
    ::Conference
    () { (::Talk) -> void } -> self

    View full-size slide

  8. Ruby3 type checking
    • Detecting types of each Ruby expression statically to help developments

    • Performance improvement is not the goal (it needs much more precise
    analysis)
    conf = Conference.find_by!(name: "GrillRB", year: 2019)
    conf.talks.each do |talk|
    puts talk.speaker.email
    end
    ::String
    ::Conference
    () { (::Talk) -> void } -> self

    View full-size slide

  9. Ruby & types project
    From Ruby team Type checker developers
    Matz
    Koichi Sasada
    Yusuke Endoh
    Dmitry Petrashko and Sorbet team

    (Sorbet)
    Jeff Foster (RDL)
    Yusuke Endoh (type-profiler)
    Soutaro Matsumoto (Steep)
    Have meetings to discuss for collaboration, share the progress,
    and develop the foundation of type checking for Ruby

    View full-size slide

  10. Key concepts
    type-profiler
    Steep
    Sorbet
    RDL
    RBS (stdlib sigs)
    Level 1 type checker
    Level 2 type checkers
    Type signature language

    View full-size slide

  11. Ruby signature language (RBS)
    • Describes the structure of Ruby programs

    • Type of standard libraries

    • Type of gems

    • Type of your program

    • Covers most of the Ruby programs
    class Array[A]
    include Enumerable[A, self]
    def []=: (Integer, A) -> A
    def []: (Integer) -> A?
    def each: { (A) -> void } -> self
    def partition: { (A) -> bool } ->
    [Array[A], Array[A]]
    ...
    end

    View full-size slide

  12. Level 1 type checker (type-profiler)
    • Type checking without any extra efforts

    • No type annotation, no signature of your program

    • Reads Ruby code and infers the types as much as possible

    • Can generate RBS of your program

    • Many expressions will be left untyped

    • No bugs can be found around the untyped expressions

    View full-size slide

  13. Level 2 type checkers
    • You write inline type annotations and type signatures

    • Detects types of most of Ruby expressions
    Sorbet Steep
    class Box
    extend T::Sig
    extend T::Generic
    Elem = type_member
    sig {returns(Elem)}
    attr_reader :x
    sig {params(x: Elem).returns(Elem)}
    attr_writer :x
    end
    box = Box[Integer].new
    # Ruby code
    class Box
    # @dynamic x
    attr_accessor :x
    end
    # @type var box: Box[Integer]
    box = Box.new
    box.x = "hello"
    # In RBS file
    class Box[X]
    attr_accessor x: X
    end

    View full-size slide

  14. Key concepts
    type-profiler
    Steep
    Sorbet
    RDL
    RBS (stdlib types)
    Level 1 type checker
    Level 2 type checkers
    Type signature language

    View full-size slide

  15. Key concepts
    type-profiler
    Steep
    Sorbet
    RDL
    RBS (stdlib types)
    Level 1 type checker
    Level 2 type checkers
    Type signature language
    Use
    Use

    View full-size slide

  16. Key concepts
    type-profiler
    Steep
    Sorbet
    RDL
    RBS (stdlib types)
    Level 1 type checker
    Level 2 type checkers
    Type signature language
    Use
    Use
    Will be part of Ruby3 Install yourself

    View full-size slide

  17. Type checking spectrum
    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  18. Type checking spectrum
    No type checking
    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  19. Type checking spectrum
    No type checking
    Level2: 

    Type check your program with signature
    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  20. Type checking spectrum
    No type checking
    Level2: 

    Type check your program with signature
    Level1:

    Type check your program with library signature
    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  21. Type checking spectrum
    No type checking
    Level2: 

    Type check your program with signature
    Level1:

    Type check your program with library signature
    Level1:

    Auto-generate type signature
    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  22. Type checking spectrum
    No type checking
    Level2: 

    Type check your program with signature
    Level1:

    Type check your program with library signature
    Level1:

    Auto-generate type signature

    More effort / more benefit
    Less effort / less benefit

    View full-size slide

  23. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View full-size slide

  24. Steep
    Key ideas
    • Structural subtyping (for duck typing)

    • Doesn't change runtime behavior at all
    $ gem install steep
    https://github.com/soutaro/steep

    View full-size slide

  25. class Proposal
    attr_reader :title
    attr_reader :speakers
    def initialize(title:)
    @title = title
    @speakers = []
    end
    def contacts
    speakers.each(&:email) # Should be a #map call
    end
    end
    proposal = Proposal.new("The Year of Concurrency") # Should be a keyword argument
    proposal.speakers << Speaker.new(...)

    View full-size slide

  26. class Proposal
    attr_reader title: String
    attr_reader speakers: Array[Speaker]
    def initialize: (title: String) -> void
    def contacts: () -> Array[String]
    end
    class Speaker ... end
    class Proposal
    attr_reader :title
    attr_reader :speakers
    def initialize(title:)
    @title = title
    @speakers = []
    end
    def contacts
    speakers.each(&:email)
    end
    end
    proposal = Proposal.new("The Year of Concurrency")
    proposal.speakers << Speaker.new(...)

    View full-size slide

  27. ArgumentTypeMismatch: receiver=singleton(::Proposal),
    expected={ :title => ::String },
    actual=::String
    class Proposal
    attr_reader title: String
    attr_reader speakers: Array[Speaker]
    def initialize: (title: String) -> void
    def contacts: () -> Array[String]
    end
    class Speaker ... end
    class Proposal
    attr_reader :title
    attr_reader :speakers
    def initialize(title:)
    @title = title
    @speakers = []
    end
    def contacts
    speakers.each(&:email)
    end
    end
    proposal = Proposal.new("The Year of Concurrency")
    proposal.speakers << Speaker.new(...)

    View full-size slide

  28. MethodBodyTypeMismatch: method=contacts,
    expected=::Array[::String],
    actual=::Array[::Speaker]
    class Proposal
    attr_reader :title
    attr_reader :speakers
    def initialize(title:)
    @title = title
    @speakers = []
    end
    def contacts
    speakers.each(&:email)
    end
    end
    proposal = Proposal.new("The Year of Concurrency")
    proposal.speakers << Speaker.new(...)
    class Proposal
    attr_reader title: String
    attr_reader speakers: Array[Speaker]
    def initialize: (title: String) -> void
    def contacts: () -> Array[String]
    end
    class Speaker ... end

    View full-size slide

  29. Duck typing support
    class Proposal
    def dump_speakers(out)
    speakers.each do |speaker|
    out << "Speaker: #{speaker.name}\n"
    end
    end
    end
    # String and IO have #<<
    proposal.dump_speakers("")
    proposal.dump_speakers(STDOUT)
    # Even Array has!
    proposal.dump_speakers([])

    View full-size slide

  30. Duck typing support
    class Proposal
    def dump_speakers(out)
    speakers.each do |speaker|
    out << "Speaker: #{speaker.
    end
    end
    end
    class Proposal
    def dump_speakers: (_DumpTo) -> void
    end
    interface _DumpTo
    def <<: (String) -> any
    end
    # String, Array, and IO have #<<
    proposal.dump_speakers("")
    proposal.dump_speakers(STDOUT)
    proposal.dump_speakers([])

    View full-size slide

  31. Duck typing support
    class Proposal
    def dump_speakers(out)
    speakers.each do |speaker|
    out << "Speaker: #{speaker.
    end
    end
    end
    class Proposal
    def dump_speakers: (_DumpTo) -> void
    end
    interface _DumpTo
    def <<: (String) -> any
    end
    # Hash doesn't have #<<
    proposal.dump_speakers({})
    # Integer#<< doesn't accept String
    proposal.dump_speakers(3)
    # String, Array, and IO have #<<
    proposal.dump_speakers("")
    proposal.dump_speakers(STDOUT)
    proposal.dump_speakers([])

    View full-size slide

  32. No runtime invasion
    • Inline type annotations of Steep are comments

    • Better for libraries:

    • Your library users do not have to install Steep

    • You can keep # of runtime dependencies as small as possible
    # @type var box: Box[Integer]
    box = Box.new

    View full-size slide

  33. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View full-size slide

  34. Benefits of type checking
    • Development experience improvements

    • Uncovers some bugs without running tests

    • Code navigation (jump to definition)

    • Code quality improvements

    • Detects some bugs not discovered through tests

    View full-size slide

  35. Benefits of type checking
    • Development experience improvements

    • Uncovers some bugs without running tests

    • Code navigation (jump to definition)

    • Code quality improvements

    • Detects some bugs not discovered through tests
    What kind of bugs?

    View full-size slide

  36. NoMethodError (undefined method `update!' for nil:NilClass)

    View full-size slide

  37. conf = Conference.find_by(name: "GrillRB")
    conf.update!(year: 2019)

    View full-size slide

  38. conf = Conference.find_by(name: "GrillRB")
    conf.update!(year: 2019)
    NoMethodError (undefined method `update!' for nil:NilClass)

    View full-size slide

  39. conf = Conference.find_by(name: "GrillRB")
    if conf
    conf.update!(year: 2019)
    end
    conf = Conference.find_by!(name: "GrillRB")
    conf.update!(year: 2019)
    Test if it is nil before using the value
    Use find_by! instead and abort if there is no record

    View full-size slide

  40. Use types to avoid nil error
    • Assign different types for nil-able expression and non-nil expression

    • A type ::Conference doesn't allow to be nil

    • A type ::Conference | nil allows to be nil
    conf = Conference.find_by(name: "GrillRB")
    conf.update!(year: 2019)
    ::Conference | nil
    NoMethodError: type=(::Conference | nil), method=update!

    View full-size slide

  41. Flow sensitive type checking
    conf = Conference.find_by(name: "GrillRB")
    if conf
    conf.update!(year: 2019)
    end
    ::Conference | nil

    View full-size slide

  42. Flow sensitive type checking
    conf = Conference.find_by(name: "GrillRB")
    if conf
    conf.update!(year: 2019)
    end
    ::Conference | nil
    ::Conference

    View full-size slide

  43. Working with nils safely
    • You need to test to ensure the value is not nil before method calls

    • Type checkers tell you if you forget the test

    • We can generalize to any kind of disjunction of types...

    • Conference or nil

    • String or Integer or Array of Integer

    View full-size slide

  44. Case analyses on types
    class DivNode ... end
    class AnchorNode ... end
    def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    end
    end
    class DivNode
    def tag_name
    :div
    end
    end
    class AnchorNode
    def tag_name
    :a
    end
    end
    Using case analysis Using duck-typing

    View full-size slide

  45. Is it a bad style?
    • Using inheritance/mixin/duck-typing have been the
    recommended style

    • We easily forget updating all of the case expressions when
    we add a new class

    • Sometimes we feel case analysis is better

    • Because of the solution in our mind

    • Because we don't want to monkey-patch existing classes
    class ImgNode
    ...
    end
    def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    when ImgNode
    :img
    end
    end

    View full-size slide

  46. What if we have a type checker?
    • Type checkers (Steep and Sorbet) support union types and provide case
    exhaustiveness checking

    • They detects if you forget supporting a case
    def tag_name: (DivNode | AnchorNode) -> Symbol
    def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    end
    end
    RBS
    Ruby

    View full-size slide

  47. What if we have a type checker?
    • Type checkers (Steep and Sorbet) support union types and provide case
    exhaustiveness checking

    • They detects if you forget supporting a case
    def tag_name: (DivNode | AnchorNode) -> Symbol
    Union Type def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    end
    end
    RBS
    Ruby

    View full-size slide

  48. def tag_name: (DivNode | AnchorNode | ImgNode) -> Symbol
    def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    # Missing `when` for ImgNode
    end
    end

    View full-size slide

  49. def tag_name: (DivNode | AnchorNode | ImgNode) -> Symbol
    MethodBodyTypeMismatch: method=tag_name,
    expected=::Symbol,
    actual=(::Symbol | nil)
    def tag_name(node)
    case node
    when DivNode
    :div
    when AnchorNode
    :a
    # Missing `when` for ImgNode
    end
    end

    View full-size slide

  50. No longer a bad style
    • There is no reason to avoid case analyses if we use type checkers

    • Sorbet also supports!

    • If you feel case analysis is better for your problem, you can write case/
    case-in
    • It is another option how you design your code
    • You can continue using inheritance/duck-typing if you like it

    View full-size slide

  51. Summary
    • Ruby3 will provide an option for static type checking

    • Type checking gives more flexibility on how you design your Ruby code

    • You can work with nils safely using optional types

    • You can use case analyses when you want with union types

    View full-size slide