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
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
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
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
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
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
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
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
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
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
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
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
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
without running tests • Documentation with validation • Code navigations • Application quality improvements • Uncover bugs missed in tests • To help the development of advanced analyses
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
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
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(...)
(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(...)
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]
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"
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]