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
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
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
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
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
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
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
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
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
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
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(...)
(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(...)
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
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([])
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([])
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([])
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
some bugs without running tests • Code navigation (jump to definition) • Code quality improvements • Detects some bugs not discovered through tests What kind of bugs?
= 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
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!
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
... 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
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
(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
(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
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
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
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