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. Outline • Grilling in Japan • The plan for Ruby3

    • Steep quick tour • Type checking benefits
  2. 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
  3. 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
  4. 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
  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 ::String ::Conference () { (::Talk) -> void } -> self
  6. 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
  7. Key concepts type-profiler Steep Sorbet RDL RBS (stdlib sigs) Level

    1 type checker Level 2 type checkers Type signature language
  8. 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
  9. 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
  10. 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
  11. Key concepts type-profiler Steep Sorbet RDL RBS (stdlib types) Level

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

    1 type checker Level 2 type checkers Type signature language Use Use
  13. 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
  14. Type checking spectrum No type checking More effort / more

    benefit Less effort / less benefit
  15. Type checking spectrum No type checking Level2: 
 Type check

    your program with signature More effort / more benefit Less effort / less benefit
  16. 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
  17. 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
  18. 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
  19. Outline • Grilling in Japan • The plan for Ruby3

    • Steep quick tour • Type checking benefits
  20. Steep Key ideas • Structural subtyping (for duck typing) •

    Doesn't change runtime behavior at all $ gem install steep https://github.com/soutaro/steep
  21. 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(...)
  22. 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(...)
  23. 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(...)
  24. 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
  25. 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([])
  26. 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([])
  27. 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([])
  28. 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
  29. Outline • Grilling in Japan • The plan for Ruby3

    • Steep quick tour • Type checking benefits
  30. 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
  31. 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?
  32. 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
  33. 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!
  34. Flow sensitive type checking conf = Conference.find_by(name: "GrillRB") if conf

    conf.update!(year: 2019) end ::Conference | nil ::Conference
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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