Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Embedding it into Ruby code

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Embedding it into Ruby code

RubyKaigi 2024, Naha

Avatar for Soutaro Matsumoto

Soutaro Matsumoto

May 16, 2024

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