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 Slide

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

    View Slide

  3. View Slide

  4. ম೑
    Yaki Niku (Grilled meat)

    View Slide

  5. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View Slide

  6. View 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

    View 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
    ::Conference

    View Slide

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

  10. 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 Slide

  11. 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 Slide

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

    View Slide

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

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

  15. 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 Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

  21. Type checking spectrum
    No type checking
    Level2: 

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

    View Slide

  22. 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 Slide

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

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

  25. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View Slide

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

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

    View Slide

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

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

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

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

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

  32. 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 Slide

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

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

  35. Outline
    • Grilling in Japan

    • The plan for Ruby3

    • Steep quick tour

    • Type checking benefits

    View Slide

  36. 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 Slide

  37. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. 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 Slide

  42. 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 Slide

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

    View Slide

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

    View Slide

  45. 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 Slide

  46. 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 Slide

  47. 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 Slide

  48. 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 Slide

  49. 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 Slide

  50. 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 Slide

  51. 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 Slide

  52. 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 Slide

  53. 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 Slide