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

1fab9d01b25e99522f3dfd01e3d4cb51?s=128

Soutaro Matsumoto

August 31, 2019
Tweet

Transcript

  1. An introduction to typed Ruby programming Soutaro Matsumoto

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

  3. None
  4. ম೑ Yaki Niku (Grilled meat)

  5. Outline • Grilling in Japan • The plan for Ruby3

    • Steep quick tour • Type checking benefits
  6. None
  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
  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
  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
  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
  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
  12. Key concepts type-profiler Steep Sorbet RDL RBS (stdlib sigs) Level

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

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

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

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

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

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

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

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

    • Steep quick tour • Type checking benefits
  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
  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?
  38. NoMethodError (undefined method `update!' for nil:NilClass)

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

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

    for nil:NilClass)
  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
  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!
  43. Flow sensitive type checking conf = Conference.find_by(name: "GrillRB") if conf

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

    conf.update!(year: 2019) end ::Conference | nil ::Conference
  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
  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
  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
  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
  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
  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
  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
  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
  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