Upload
others
View
4
Download
0
Embed Size (px)
Citation preview
Ruby with types
Soutaro Matsumoto
• From Tokyo, Japan
• Working for type of Ruby since 2004
Soutaro Matsumoto
Outline
• The overview of type checking for Ruby3
• Type checking benefits
• Steep quick tour
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
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
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
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
Ruby & types projectFrom 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
Sub projects
type-profiler
Steep
Sorbet
RDL
RBSruby-signature
Level 1 type checker Level 2 type checkers
Type signature language
Sub projects
type-profiler
Steep
Sorbet
RDL
RBSruby-signature
Level 1 type checker Level 2 type checkers
Type signature language
Use/Generate
Use
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
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 box.x = "hello"
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
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
Sub projects
type-profiler
Steep
Sorbet
RDL
Level 1 type checker Level 2 type checkers
Type signature language
RBSruby-signature
Sub projects
type-profiler
Steep
Sorbet
RDL
Level 1 type checker Level 2 type checkers
Type signature language
Will be part of Ruby3
RBSruby-signature
Type checking spectrum
No type checking Type check your program with signature
Type checking spectrum
No type checking Type check your program with signature
Type check your program with library signature
Type checking spectrum
No type checking Type check your program with signature
Type check your program with library signature
Auto-generate type signature
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
No type checking
!
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
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
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
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
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
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
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 😀
Outline
• The overview of type checking for Ruby3
• Type checking benefits
• Steep quick tour
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
NoMethodError (undefined method `update!' for nil:NilClass)
conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019)
conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019)
(name: String) -> (Conference | nil)
conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019)
Conference | nil
(name: String) -> (Conference | nil)
conference = Conference.find_by(name: "RubyConf Taiwan") conference.update!(year: 2019)
Conference | nil
(name: String) -> (Conference | nil)
NoMethodError (undefined method `update!' for nil:NilClass)
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
conference = Conference.find_by(name: "RubyConf Taiwan") send_notification_to_attendees(conference)
Conference | nil
conference = Conference.find_by(name: "RubyConf Taiwan") send_notification_to_attendees(conference)
Conference | nil
(Conference) -> void
(Conference | nil) -> void 🤔
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 typestype json = nil | Numeric | String | TrueClass | FalseClass | Array[json] | Hash[String, json]
• Best fit for case / case-in
Outline
• The overview of type checking for Ruby3
• Type checking benefits
• Steep quick tour
Steep
Key ideas • Structural subtyping (for duck typing)• Doesn't change runtime behavior at all
$ gem install steep
https://github.com/soutaro/steep
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(...)
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(...)
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
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]
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)
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"
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]
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