Slide 1

Slide 1 text

Embedding it into Ruby code Soutaro Matsumoto
 Timee, Inc.

Slide 2

Slide 2 text

Embedding it into Ruby code Soutaro Matsumoto
 Timee, Inc.

Slide 3

Slide 3 text

Embedding it into Ruby code Soutaro Matsumoto
 Timee, Inc. RBS Type Declaration

Slide 4

Slide 4 text

Soutaro Matsumoto • Ruby committer • Develops Steep and RBS • Working at Timee from this April @soutaro

Slide 5

Slide 5 text

Soutaro Matsumoto • Ruby committer • Develops Steep and RBS • Working at Timee from this April @soutaro

Slide 6

Slide 6 text

Type checking with RBS & Steep • Two fi les to maintain -- Ruby code and RBS type de fi nition • Steep validates the consistency between them Ruby code RBS type de fi nition

Slide 7

Slide 7 text

Why RBS fi les? • To avoid extending Ruby syntax for types • Matz didn't want it • Unclear if we can implement something without breaking compatibility • Didn't want to write types in string literals • sig("() -> void") • Meta programming

Slide 8

Slide 8 text

Meta programming • Meta programming allows de fi ning Ruby programs without speci fi c syntax nor speci fi c methods • How can we declare types of them in Ruby code?

Slide 9

Slide 9 text

Types in another fi le • We can de fi ne natural and compact syntax without considering con fl icts • Keep everything static -- no meta-programming

Slide 10

Slide 10 text

More bene fi ts • The type declaration helps understanding the outline of the Ruby program without reading the detailed implementation • We anyway need something to declare types for extensions

Slide 11

Slide 11 text

Dif fi culties • Switching fi les during development • Keeping two sets of fi les updated • Reviewing on browser

Slide 12

Slide 12 text

Updating two fi les class Talk attr_reader title: String attr_reader speakers: Array[Speaker] def initialize: (String title) -> void end class Talk attr_reader :title, :speakers def initialize(title) @title = title @speakers = [] end

Slide 13

Slide 13 text

Updating two fi les class Talk attr_reader title: String attr_reader speakers: Array[Speaker] def initialize: (String title) -> void def first_speaker: () -> Speaker end class Talk attr_reader :title, :speakers def initialize(title) @title = title @speakers = [] end def first_speaker = speakers.first || raise end + + You have to edit two fi les to do one thing 🤔

Slide 14

Slide 14 text

Updating two fi les class Talk attr_reader title: String attr_reader speakers: Array[Speaker] def initialize: (String title) -> void def first_speaker: () -> Speaker end class Talk attr_reader :title, :speakers def initialize(title) @title = title @speakers = [] end def first_speaker = speakers.first || raise end - - Deleting implementation or type de fi nition easily slips our minds 😣

Slide 15

Slide 15 text

Reviewing on browser • We need types for reviewing changes, but PR may open without them

Slide 16

Slide 16 text

Reviewing on browser • We need types for reviewing changes, but PR may open without them

Slide 17

Slide 17 text

It's time to be more integrated • It started as an optional feature • Stronger connection between the code and types would make more sense today

Slide 18

Slide 18 text

RBS::Inline https://github.com/soutaro/rbs-inline $ bundle add rbs-inline --require=false

Slide 19

Slide 19 text

RBS::Inline • Embed the RBS type declarations into Ruby code as comments # rbs_inline: enabled class Person attr_reader :name #:: String attr_reader :addresses #:: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize(name:, addresses:) @name = name @addresses = addresses end def to_s #:: String "Person(name = #{name}, addresses = #{addresses.j end # @rbs yields (String) -> void def each_address(&block) #:: void addresses.each(&block) end

Slide 20

Slide 20 text

RBS::Inline • Generates RBS fi les from annotated Ruby code • Type check your project with the generated RBS type de fi nitions # rbs_inline: enabled class Person attr_reader :name #:: String attr_reader :addresses #:: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize(name:, addresses:) @name = name @addresses = addresses end def to_s #:: String # Generated from foo.rb with RBS::Inline class Person attr_reader name: String attr_reader addresses: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize: (name: String, addresses: Array[String] def to_s: () -> String # @rbs yields (String) -> void def each_address: () { (String) -> void } -> void

Slide 21

Slide 21 text

RBS::Inline • Generates RBS fi les from annotated Ruby code • Type check your project with the generated RBS type de fi nitions # rbs_inline: enabled class Person attr_reader :name #:: String attr_reader :addresses #:: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize(name:, addresses:) @name = name @addresses = addresses end def to_s #:: String # Generated from foo.rb with RBS::Inline class Person attr_reader name: String attr_reader addresses: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize: (name: String, addresses: Array[String] def to_s: () -> String # @rbs yields (String) -> void def each_address: () { (String) -> void } -> void

Slide 22

Slide 22 text

RBS::Inline • Generates RBS fi les from annotated Ruby code • Type check your project with the generated RBS type de fi nitions # rbs_inline: enabled class Person attr_reader :name #:: String attr_reader :addresses #:: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize(name:, addresses:) @name = name @addresses = addresses end def to_s #:: String # Generated from foo.rb with RBS::Inline class Person attr_reader name: String attr_reader addresses: Array[String] # @rbs name: String # @rbs addresses: Array[String] # @rbs returns void def initialize: (name: String, addresses: Array[String] def to_s: () -> String # @rbs yields (String) -> void def each_address: () { (String) -> void } -> void

Slide 23

Slide 23 text

Annotated Ruby code Type de fi nition Validates implementation Dev writes Ruby code and types Extract embedded type de fi nition

Slide 24

Slide 24 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end

Slide 25

Slide 25 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end # @rbs yields (String) -> void def each_name end

Slide 26

Slide 26 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end def to_s #:: String end # @rbs yields (String) -> void def each_name end

Slide 27

Slide 27 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end def to_s #:: String end # @rbs yields (String) -> void def each_name end attr_reader :name #:: String attr_reader :size #:: Integer

Slide 28

Slide 28 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end def to_s #:: String end # @rbs yields (String) -> void def each_name end attr_reader :name #:: String attr_reader :size #:: Integer ALL_SIZES = [...] #:: Array[Integer]

Slide 29

Slide 29 text

# @rbs name: String # @rbs size: Integer # @rbs returns void def initialize(name, size) end def to_s #:: String end # @rbs yields (String) -> void def each_name end attr_reader :name #:: String attr_reader :size #:: Integer class StringArray < Array #[String] end ALL_SIZES = [...] #:: Array[Integer]

Slide 30

Slide 30 text

# @rbs! # type t = Integer # # @rbs! # interface _WithComments # def comment: () -> String? # end # # @rbs! # module Foo = Kernel

Slide 31

Slide 31 text

Using RBS::Inline • Generates RBS fi les from Ruby code when Ruby scripts under lib dir is changed • Steep detects the fi le changes and type checks automatically $ fswatch lib | xargs -n1 rbs-inline --output

Slide 32

Slide 32 text

How is it? • It's promising 🤩 • The edit 㱻 type-check cycle becomes much smoother

Slide 33

Slide 33 text

Limitations • RBS::Inline is a prototype to test the experience and fi nd the best syntax • No IDE/editor integration yet • No completion • No error detection • No navigation features

Slide 34

Slide 34 text

Designing RBS::Inline syntax • Focus on the most frequently used RBS constructs for practical Ruby programs • Limited overloading and generic method, no interface, no type alias • Write RBS fi les if you need advanced features (or @rbs! annotation) • Concrete syntax matters • Review the method type syntax

Slide 35

Slide 35 text

Compact syntax • Write the RBS method types directly • Supports generics and overloads #:: (String, ?Hash[Symbol, untyped]) -> String def tag(string, attributes = {}) end #:: [A] { (String) -> A } -> Array[A] #:: () -> Enumerator[String, void] def map(&block) end def map: [A] { (String) -> A } -> Array[A] | () -> Enumerator[String, void]

Slide 36

Slide 36 text

Doc style syntax • Annotate parameters and return types separately • The texts after -- are comment for the parameter # @rbs string: String -- The content of the tag # @rbs attributes: Hash[Symbol, untyped] -- Attributes # @rbs returns String -- Returns the tag def tag(string, attributes) end

Slide 37

Slide 37 text

Doc style syntax • Annotate parameters and return types separately • The texts after -- are comment for the parameter # @rbs string: String -- The content of the tag # @rbs attributes: Hash[Symbol, untyped] -- Attributes # @rbs returns String -- Returns the tag def tag(string, attributes) end

Slide 38

Slide 38 text

Doc style syntax • Annotate parameters and return types separately • The texts after -- are comment for the parameter # @rbs string: String -- The content of the tag # @rbs attributes: Hash[Symbol, untyped] -- Attributes # @rbs returns String -- Returns the tag def tag(string, attributes) end

Slide 39

Slide 39 text

Doc style syntax • Annotate parameters and return types separately • The texts after -- are comment for the parameter # @rbs string: String -- The content of the tag # @rbs attributes: Hash[Symbol, untyped] -- Attributes # @rbs returns String -- Returns the tag def tag(string, attributes) end

Slide 40

Slide 40 text

Doc style syntax • Annotate parameters and return types separately • The texts after -- are comment for the parameter # @rbs string: String -- The content of the tag # @rbs attributes: Hash[Symbol, untyped] -- Attributes # @rbs returns String -- Returns the tag def tag(string, attributes) end

Slide 41

Slide 41 text

Return type annotation • Shortcut for return type of a method • You can use anything based on the feature requirement, number of annotations you need, method type length, or size of the implementation def to_s #:: String end # @rbs returns String def to_s end

Slide 42

Slide 42 text

Some rejected ideas

Slide 43

Slide 43 text

Some rejected ideas def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end

Slide 44

Slide 44 text

Some rejected ideas def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end Too wide

Slide 45

Slide 45 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end Too wide

Slide 46

Slide 46 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end Too wide Cryptic but not compact

Slide 47

Slide 47 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end # @rbs method: (String, ?Hash[Symbol, untyped]) -> String def tag(string, attributes = {}) end Too wide Cryptic but not compact

Slide 48

Slide 48 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end # @rbs method: (String, ?Hash[Symbol, untyped]) -> String def tag(string, attributes = {}) end Too wide @rbs method is redundant Cryptic but not compact

Slide 49

Slide 49 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end # @rbs method: (String, ?Hash[Symbol, untyped]) -> String def tag(string, attributes = {}) end # @param string [String] # @param attributes (Hash[Symbol, untyped]) # @returns [String] def tag(string, attributes) end Too wide @rbs method is redundant Cryptic but not compact

Slide 50

Slide 50 text

Some rejected ideas def tag( string, #: String attributes #: Hash[Symbol, untyped] ) #: String end def tag(string, attributes = {}) #: (String, ?Hash[Symbol, untyped]) -> String end # @rbs method: (String, ?Hash[Symbol, untyped]) -> String def tag(string, attributes = {}) end # @param string [String] # @param attributes (Hash[Symbol, untyped]) # @returns [String] def tag(string, attributes) end Too wide @rbs method is redundant Cryptic but not compact 🤔

Slide 51

Slide 51 text

Why not a YARD tag? • We made the annotation syntax similar to YARD tags • The structure is same, but the actual syntax is di ff erent • We need more annotations other than @param and @returns • The type syntax is di ff erent from RBS # @param text [String, nil] # @param size [Integer] Size of it # @returns [String] tag with style def font_size(text, size:) end # @rbs text: String? # @rbs size: Integer -- Si # @rbs returns String -- < def font_size(text, size:) end

Slide 52

Slide 52 text

Integration with YARD ecosystem • We made the @rbs syntax compatible with YARD syntax • Passing --tag option to yardoc will ignore all RBS annotations • We can develop a YARD plugin that handles @rbs annotations [warn]: Unknown tag @rbs in file `lib/foo.rb` near line 3

Slide 53

Slide 53 text

Feedback is welcome • We are still seeking a better syntax, before implementing it in rbs-gem • I already heard some requests: • @sig instead of @rbs • Writing method types immediately after # @rbs instead of #:: • @rbs return: T instead of @rbs returns T Approved

Slide 54

Slide 54 text

Summary • We are working for embedded RBS type declarations in Ruby code • It allows coding without switching between fi les • It gives more context when you are reviewing changes on your browser • Install rbs-inline gem to test it today • Catch me if you have any feedback/ideas/concerns • Small meeting with RBS folks at hack space during afternoon break