Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Embedding it into Ruby code

Embedding it into Ruby code

RubyKaigi 2024, Naha

Soutaro Matsumoto

May 16, 2024
Tweet

More Decks by Soutaro Matsumoto

Other Decks in Programming

Transcript

  1. Soutaro Matsumoto • Ruby committer • Develops Steep and RBS

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

    • Working at Timee from this April @soutaro
  3. 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
  4. 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
  5. 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?
  6. 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
  7. 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
  8. Dif fi culties • Switching fi les during development •

    Keeping two sets of fi les updated • Reviewing on browser
  9. 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
  10. 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 🤔
  11. 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 😣
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. Annotated Ruby code Type de fi nition Validates implementation Dev

    writes Ruby code and types Extract embedded type de fi nition
  18. # @rbs name: String # @rbs size: Integer # @rbs

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

    returns void def initialize(name, size) end # @rbs yields (String) -> void def each_name end
  20. # @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
  21. # @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
  22. # @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]
  23. # @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]
  24. # @rbs! # type t = Integer # # @rbs!

    # interface _WithComments # def comment: () -> String? # end # # @rbs! # module Foo = Kernel
  25. 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
  26. How is it? • It's promising 🤩 • The edit

    㱻 type-check cycle becomes much smoother
  27. 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
  28. 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
  29. 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]
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. Some rejected ideas def tag(string, attributes = {}) #: (String,

    ?Hash[Symbol, untyped]) -> String end Too wide
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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 🤔
  43. 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] <span> 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
  44. 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
  45. 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
  46. 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