Slide 1

Slide 1 text

Ruby with types Soutaro Matsumoto

Slide 2

Slide 2 text

• From Tokyo, Japan • Working for type of Ruby since 2004 Soutaro Matsumoto

Slide 3

Slide 3 text

Outline • The overview of type checking for Ruby3 • Type checking benefits • Steep quick tour

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Sub projects type-profiler Steep Sorbet RDL RBS ruby-signature Level 1 type checker Level 2 type checkers Type signature language Use/Generate Use

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

No type checking !

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Outline • The overview of type checking for Ruby3 • Type checking benefits • Steep quick tour

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

NoMethodError (undefined method `update!' for nil:NilClass)

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

conference = Conference.find_by(name: "RubyConf Taiwan") send_notification_to_attendees(conference) Conference | nil (Conference) -> void (Conference | nil) -> void

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Outline • The overview of type checking for Ruby3 • Type checking benefits • Steep quick tour

Slide 41

Slide 41 text

Steep Key ideas • Structural subtyping (for duck typing) • Doesn't change runtime behavior at all $ gem install steep https://github.com/soutaro/steep

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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]

Slide 46

Slide 46 text

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)

Slide 47

Slide 47 text

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"

Slide 48

Slide 48 text

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]

Slide 49

Slide 49 text

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