Slide 1

Slide 1 text

An introduction to typed Ruby programming Soutaro Matsumoto

Slide 2

Slide 2 text

Soutaro Matsumoto https://github.com/soutaro

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ম೑ Yaki Niku (Grilled meat)

Slide 5

Slide 5 text

Outline • Grilling in Japan • The plan for Ruby3 • Steep quick tour • Type checking benefits

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 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 12

Slide 12 text

Key concepts type-profiler Steep Sorbet RDL RBS (stdlib sigs) Level 1 type checker Level 2 type checkers Type signature language

Slide 13

Slide 13 text

Ruby signature language (RBS) • Describes the structure of 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

Slide 14

Slide 14 text

Level 1 type checker (type-profiler) • Type checking without any 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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Key concepts type-profiler Steep Sorbet RDL RBS (stdlib types) Level 1 type checker Level 2 type checkers Type signature language

Slide 17

Slide 17 text

Key concepts type-profiler Steep Sorbet RDL RBS (stdlib types) Level 1 type checker Level 2 type checkers Type signature language Use Use

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Type checking spectrum More effort / more benefit Less effort / less benefit

Slide 20

Slide 20 text

Type checking spectrum No type checking More effort / more benefit Less effort / less benefit

Slide 21

Slide 21 text

Type checking spectrum No type checking Level2: 
 Type check your program with signature More effort / more benefit Less effort / less benefit

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Type checking spectrum No type checking Level2: 
 Type check 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

Slide 24

Slide 24 text

Type checking spectrum No type checking Level2: 
 Type check 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

Slide 25

Slide 25 text

Outline • Grilling in Japan • The plan for Ruby3 • Steep quick tour • Type checking benefits

Slide 26

Slide 26 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 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

ArgumentTypeMismatch: receiver=singleton(::Proposal), expected={ :title => ::String }, actual=::String 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(...)

Slide 30

Slide 30 text

MethodBodyTypeMismatch: method=contacts, expected=::Array[::String], actual=::Array[::Speaker] 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(...) class Proposal attr_reader title: String attr_reader speakers: Array[Speaker] def initialize: (title: String) -> void def contacts: () -> Array[String] end class Speaker ... end

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

No runtime invasion • Inline type annotations of Steep are 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

Slide 35

Slide 35 text

Outline • Grilling in Japan • The plan for Ruby3 • Steep quick tour • Type checking benefits

Slide 36

Slide 36 text

Benefits of type checking • Development experience improvements • Uncovers some bugs without running tests • Code navigation (jump to definition) • Code quality improvements • Detects some bugs not discovered through tests

Slide 37

Slide 37 text

Benefits of type checking • Development experience improvements • Uncovers some bugs without running tests • Code navigation (jump to definition) • Code quality improvements • Detects some bugs not discovered through tests What kind of bugs?

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

conf = Conference.find_by(name: "GrillRB") conf.update!(year: 2019)

Slide 40

Slide 40 text

conf = Conference.find_by(name: "GrillRB") conf.update!(year: 2019) NoMethodError (undefined method `update!' for nil:NilClass)

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Use types to avoid nil error • Assign different types 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!

Slide 43

Slide 43 text

Flow sensitive type checking conf = Conference.find_by(name: "GrillRB") if conf conf.update!(year: 2019) end ::Conference | nil

Slide 44

Slide 44 text

Flow sensitive type checking conf = Conference.find_by(name: "GrillRB") if conf conf.update!(year: 2019) end ::Conference | nil ::Conference

Slide 45

Slide 45 text

Working with nils safely • You need to test to 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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Summary • Ruby3 will provide an option for static type 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