Save 37% off PRO during our Black Friday Sale! »

Ruby with types

Ruby with types

RubyConf Taiwan 2019

- https://2019.rubyconf.tw

1fab9d01b25e99522f3dfd01e3d4cb51?s=128

Soutaro Matsumoto

July 26, 2019
Tweet

Transcript

  1. Ruby with types Soutaro Matsumoto

  2. • From Tokyo, Japan • Working for type of Ruby

    since 2004 Soutaro Matsumoto
  3. Outline • The overview of type checking for Ruby3 •

    Type checking benefits • Steep quick tour
  4. None
  5. Ruby3 type checking • Detecting types of each Ruby expression

    statically to help developments • Performance is not our goal (it needs much more precise analysis) taiwan = Conference.find_by!(name: "RubyConf Taiwan", year: 2019) taiwan.talks.each do |talk| puts talk.speaker.email end
  6. Ruby3 type checking • Detecting types of each Ruby expression

    statically to help developments • Performance is not our goal (it needs much more precise analysis) taiwan = Conference.find_by!(name: "RubyConf Taiwan", year: 2019) taiwan.talks.each do |talk| puts talk.speaker.email end ::Conference
  7. Ruby3 type checking • Detecting types of each Ruby expression

    statically to help developments • Performance is not our goal (it needs much more precise analysis) taiwan = Conference.find_by!(name: "RubyConf Taiwan", year: 2019) taiwan.talks.each do |talk| puts talk.speaker.email end ::Conference () { (::Talk) -> void } -> self
  8. Ruby3 type checking • Detecting types of each Ruby expression

    statically to help developments • Performance is not our goal (it needs much more precise analysis) taiwan = Conference.find_by!(name: "RubyConf Taiwan", year: 2019) taiwan.talks.each do |talk| puts talk.speaker.email end ::String ::Conference () { (::Talk) -> void } -> self
  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
  10. Sub projects type-profiler Steep Sorbet RDL RBS ruby-signature Level 1

    type checker Level 2 type checkers Type signature language
  11. Sub projects type-profiler Steep Sorbet RDL RBS ruby-signature Level 1

    type checker Level 2 type checkers Type signature language Use/Generate Use
  12. Level 1 type checker • Type checking without any extra

    efforts • No type annotation, no signature of your program • Reads Ruby code without type annotations and infers the types as much as possible • Many expressions will be left untyped • No bugs can be found around the untyped expressions • Minimizing the cost for type checking sacrificing the precision/coverage
  13. Level 2 type checkers • You write inline type annotations

    as embedded DSL or comments • 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 class Box # @dynamic x attr_accessor :x end # @type var box: Box[Integer] box = Box.new box.x = "hello" class Box[X] attr_accessor x: X end
  14. The Ruby signature language • Ruby-like but different syntax •

    Defines the structure of Ruby programs • Classes, modules, mixin, and interfaces • Methods and instance variables • Generics, unions, tuples, optionals, ... • You can write types for most of the Ruby programs class Array[A] include Enumerable def []=: (Integer, A) -> A def []: (Integer) -> A? def each: { (A) -> void } -> self def partition: { (A) -> bool } -> [Array[A], Array[A]] ... end WIP
  15. Sub projects type-profiler Steep Sorbet RDL Level 1 type checker

    Level 2 type checkers Type signature language RBS ruby-signature
  16. Sub projects type-profiler Steep Sorbet RDL Level 1 type checker

    Level 2 type checkers Type signature language Will be part of Ruby3 RBS ruby-signature
  17. Type checking spectrum No type checking Type check your program

    with signature
  18. Type checking spectrum No type checking Type check your program

    with signature Type check your program with library signature
  19. Type checking spectrum No type checking Type check your program

    with signature Type check your program with library signature Auto-generate type signature
  20. Type checking spectrum No type checking Type check your program

    with signature Type check your program with library signature Write signature but no type checking Auto-generate type signature
  21. No type checking !

  22. Type check with library signature • You don't write signatures

    of your Ruby program • Use library signatures and infer the types of your Ruby code • You can try with level 1 type checker (type-profiler) • Level 2 type checkers detects some trivial problems (Sorbet, Steep) taiwan = Conference.find_by!(name: "RubyConf Taiwan", year: 2019) taiwan.talks.each do |talk| puts talk.email end Missing #speaker call
  23. Auto generate type signature • You don't write signatures of

    your Ruby program • Let type-profiler generate type signature of your Ruby program class Fib def fib(x) if x <= 1 x else fib(x-1) + fib(x-2) end end end puts Fib.new.fib(30) Fib#fib :: (Integer) -> Integer
  24. Write signature but no type checking • Write type signature

    of your program to ship it with the gem • Run type-profiler with your tests to detect mismatches between your signature and tests class Fib def fib: (Integer) -> Integer end class FibTest < Minitest::Test def test_fib assert_equal 2, 3 assert_equal "two", Fib.new.fib("three") assert_equal "ೋ", Fib.new.fib("ࡾ") end end
  25. Write signature but no type checking • Write type signature

    of your program to ship it with the gem • Run type-profiler with your tests to detect mismatches between your signature and tests class Fib def fib: (Integer) -> Integer end class FibTest < Minitest::Test def test_fib assert_equal 2, 3 assert_equal "two", Fib.new.fib("three") assert_equal "ೋ", Fib.new.fib("ࡾ") end end class Fib def fib: (Integer) -> Integer | (String) -> String end
  26. Type check your code • Prepare the signature of your

    Ruby program and type check the implementation • You may write the signature manually • You may generate the signature with type-profiler class Box # @dynamic x attr_accessor :x end # @type var box: Box[Integer] box = Box.new box.x = "hello" Will require some inline type annotations
  27. Type checking spectrum No type checking Type check your program

    with signature Type check your program with library signature Write signature but no type checking Auto-generate type signature
  28. Type checking spectrum No type checking Type check your program

    with signature Type check your program with library signature Write signature but no type checking Auto-generate type signature
  29. Outline • The overview of type checking for Ruby3 •

    Type checking benefits • Steep quick tour
  30. Type checking benefits • Development experience improvements • Uncover bugs

    without running tests • Documentation with validation • Code navigations • Application quality improvements • Uncover bugs missed in tests • To help the development of advanced analyses
  31. NoMethodError (undefined method `update!' for nil:NilClass)

  32. conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019)

  33. conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019) (name: String) ->

    (Conference | nil)
  34. conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019) Conference | nil

    (name: String) -> (Conference | nil)
  35. conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019) Conference | nil

    (name: String) -> (Conference | nil) NoMethodError (undefined method `update!' for nil:NilClass)
  36. conference = Conference.find_by(name: "RubyConf Taiwan") if conference conference.update!(year: 2019) end

    conference = Conference.find_by!(name: "RubyConf Taiwan") conference.update!(year: 2019) Test if it is nil before using the value Use find_by! instead and abort if there is no record
  37. conference = Conference.find_by(name: "RubyConf Taiwan") send_notification_to_attendees(conference) Conference | nil

  38. conference = Conference.find_by(name: "RubyConf Taiwan") send_notification_to_attendees(conference) Conference | nil (Conference)

    -> void (Conference | nil) -> void
  39. Working with nils safely • The only way to avoid

    nil dereference error is by testing if it is nil or not • What if your teammate changes a value to nilable? • Type checkers will tell you if you forget the test • We can generalize to any union types type json = nil | Numeric | String
 | TrueClass | FalseClass 
 | Array[json] | Hash[String, json] • Best fit for case / case-in
  40. Outline • The overview of type checking for Ruby3 •

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

    Doesn't change runtime behavior at all $ gem install steep https://github.com/soutaro/steep
  42. class Conference attr_reader :name attr_reader :talks def initialize(name:) @name =

    name @talks = [] end def speakers talks.each(&:speaker) # Should be #map call end end conference = Conference.new("RubyConf Taiwan") # Should be a keyword argument conference.talks << Talk.new(...)
  43. class Conference attr_reader name: String attr_reader talks: Array[Talk] def initialize:

    (name: String) -> void def speakers: -> Array[Speaker] end class Talk ... end class Speaker ... end class Conference attr_reader :name attr_reader :talks def initialize(name:) @name = name @talks = [] end def speakers talks.each(&:speaker) end end conference = Conference.new("RubyConf Taiwan") conference.talks << Talk.new(...)
  44. class Conference attr_reader :name attr_reader :talks def initialize(name:) @name =

    name @talks = [] end def speakers talks.each(&:speaker) end end conference = Conference.new("RubyConf Taiwan") conference.talks << Talk.new(...) class Conference attr_reader name: String attr_reader talks: Array[Talk] def initialize: (name: String) -> void def speakers: -> Array[Speaker] end class Talk ... end class Speaker ... end ArgumentTypeMismatch: receiver=singleton(::Conference), expected={ :name => ::String }, actual=::String
  45. class Conference attr_reader :name attr_reader :talks def initialize(name:) @name =

    name @talks = [] end def speakers talks.each(&:speaker) end end conference = Conference.new("RubyConf Taiwan") conference.talks << Talk.new(...) class Conference attr_reader name: String attr_reader talks: Array[Talk] def initialize: (name: String) -> void def speakers: -> Array[Speaker] end class Talk ... end class Speaker ... end MethodBodyTypeMismatch: method=speakers, expected=::Array[::Speaker], actual=::Array[::Talk]
  46. Duck typing support interface _DumpTo def <<: (String) -> any

    end class Conference ... def dump_titles: (_DumpTo) -> void end # conference.dump_titles("") # conference.dump_titles([]) # Error conference.dump_titles(3)
  47. No runtime invasion • Inline type annotations of Steep are

    comments • Better for libraries: • Your library users would not want to install Steep • You can keep # of runtime dependencies as small as possible spec.add_development_dependency "steep"
  48. Recap • Plan for Ruby3 type checking • We provide

    several options to adopt type checking • Level 2 type checkers will make Ruby more powerful • Helps to handle nils safely • Best fit for case / case-in • Steep is the best option for Ruby type checking [my personal opinion]
  49. References • Projects • https://github.com/soutaro/steep • https://sorbet.org • https://github.com/plum-umd/rdl •

    https://github.com/mame/ruby-type-profiler • https://github.com/ruby/ruby-signature • Sider • https://sider.review • https://blog.sideci.com/interview-with-bozhidar-batsov-99b049b6fd6a