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
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
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
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
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
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
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
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
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(...)
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(...)
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([])
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([])
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([])
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
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
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
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
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
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
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
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