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

Adding Type Signatures into Ruby Docs

Colby Swandale
September 08, 2022

Adding Type Signatures into Ruby Docs

Since Ruby's beginnings, its documentation has been maintained by people who help and support the language. Before the core team releases a new version of Ruby, contributors must update the documentation to reflect the current set of functionality, which presents many challenges to remaining consistent over Ruby's long history. One method may describe a set of arguments and the types one way, but another may tell them differently. Ruby 3 gained a highly requested feature, Type Annotations! A way to describe the structure of your Ruby Programs. In this talk, we'll look at improving Ruby's documentation by leveraging Ruby's Type Signatures to provide users with more accurate and consistent documentation.

Colby Swandale

September 08, 2022
Tweet

More Decks by Colby Swandale

Other Decks in Technology

Transcript

  1. View Slide

  2. View Slide

  3. View Slide

  4. RDoc orders search results alphabetically

    View Slide

  5. to_s

    View Slide

  6. to_s
    • ARGF


    • Addrinfo


    • Array


    • Benchmark::Tms
    Alphabetical Order

    View Slide

  7. to_s
    • Integer


    • Float


    • BigDecimal


    • Object
    Core Class Order
    • ARGF


    • Addrinfo


    • Array


    • Benchmark::Tms
    Alphabetical Order

    View Slide

  8. I wanted to build something
    that understands Ruby-src docs

    View Slide

  9. rubyapi.org

    View Slide

  10. Supports Ruby 2.3 to 3.1

    View Slide

  11. Quick Search & Autocomplete

    View Slide

  12. 80K visitors, 100K+ page views/month

    View Slide

  13. View source code inline or on Github

    View Slide

  14. Execute code blocks with Ruby REPL

    View Slide

  15. View Slide

  16. How can I help improve Ruby
    documentation?

    View Slide

  17. Adding Type Signatures into
    Ruby Docs

    View Slide

  18. Colby Swandale
    @oceanicpanda

    View Slide

  19. View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. Call Sequences Type Signatures

    View Slide

  26. RDoc
    Ruby standard documentation library

    View Slide

  27. github.com/ruby/rdoc

    View Slide

  28. module Rake
    ##
    # A Task is the basic unit of work in a Rakefile. Tasks have associated
    # actions (possibly more than one) and a list of prerequisites. When
    # invoked, a task will first ensure that all of its prerequisites have an
    # opportunity to run and then it will execute its own actions.
    #
    # Tasks are not usually created directly using the new method, but rather
    # use the +file+ and +task+ convenience methods.
    #
    class Task
    # Create a task named +task_name+ with no actions or prerequisites. Use
    # +enhance+ to add actions and prerequisites.
    def initialize(task_name, app)
    ...
    end
    end
    end

    View Slide

  29. = Bug Triaging Guide
    This guide discusses recommendations for triaging bugs in Ruby's bug
    tracker.
    == Bugs with Reproducible Examples
    These are the best bug reports. First, consider whether the bug
    reported is
    actually an issue or if it is expected Ruby behavior. If it is expected
    Ruby
    behavior, update the issue with why the behavior is expected, and set
    the
    status to Rejected.
    bug_triaging.rdoc

    View Slide

  30. $ rdoc .
    Parsing sources...
    100% [22/22] test/stringio/test_stringio.rb
    Generating Darkfish format into /Users/colby/Github/stringio/doc...
    Files: 22
    Total: 236 (163 undocumented)
    30.93% documented
    Elapsed: 0.3s

    View Slide

  31. View Slide

  32. $ rdoc . —format pot
    Parsing sources...
    100% [897/897] yjit.c
    Generating POT format into /Users/colby/Github/ruby/docs...
    Files: 897
    Total: 12129 (2858 undocumented)
    76.44% documented
    Elapsed: 29.7s

    View Slide

  33. $ rdoc . —format pot
    Do you know what this format is?

    View Slide

  34. View Slide

  35. View Slide

  36. require 'rdoc'
    @rdoc = RDoc::RDoc.new
    @rdoc_options = RDoc::Options.load_options.tap |r|
    r.files = Dir[Dir.pwd]
    r.template = "rdoc"
    r.quiet = true
    end
    @rdoc.document @rdoc_options

    View Slide

  37. /*
    * call-seq:
    * empty? -> true or false
    *
    * Returns +true+ if the length of +self+ is zero, +false+ otherwise:
    *
    * "hello".empty? # => false
    * " ".empty? # => false
    * "".empty? # => true
    *
    */
    static VALUE
    rb_str_empty(VALUE str)
    {
    return RBOOL(RSTRING_LEN(str) == 0);
    }

    View Slide

  38. /*
    * call-seq:
    * empty? -> true or false
    *
    * Returns +true+ if the length of +self+ is zero, +false+ otherwise:
    *
    * "hello".empty? # => false
    * " ".empty? # => false
    * "".empty? # => true
    *
    */
    static VALUE
    rb_str_empty(VALUE str)
    {
    return RBOOL(RSTRING_LEN(str) == 0);
    }
    Description
    Call Sequence

    View Slide

  39. View Slide

  40. View Slide

  41. Challenge to keep call sequences consistent

    View Slide

  42. str[integer] = new_str
    str[integer, integer] = new_str
    str[range] = aString
    str[regexp] = new_str
    str[regexp, integer] = new_str
    str[regexp, name] = new_str
    str[other_str] = new_str
    String#[]

    View Slide

  43. str[integer] = new_str
    str[integer, integer] = new_str
    str[range] = aString
    str[regexp] = new_str
    str[regexp, integer] = new_str
    str[regexp, name] = new_str
    str[other_str] = new_str
    String#[]

    View Slide

  44. str[integer] = new_str
    str[integer, integer] = new_str
    str[range] = aString
    str[regexp] = new_str
    str[regexp, integer] = new_str
    str[regexp, name] = new_str
    str[other_str] = new_str
    String#[]

    View Slide

  45. str[integer] = new_str
    str[integer, integer] = new_str
    str[range] = aString
    str[regexp] = new_str
    str[regexp, integer] = new_str
    str[regexp, name] = new_str
    str[other_str] = new_str
    Call sequences are plain text

    View Slide

  46. Can we automatically
    generate method sequences?

    View Slide

  47. Yes! 🎉

    View Slide

  48. RBS
    Type Signatures for Ruby

    View Slide

  49. github.com/ruby/rbs

    View Slide

  50. # string.rbs
    class String
    include Comparable
    # https://github.com/ruby/rbs/blob/master/core/string.rbs
    # Returns a new String containing `other_string`
    # concatenated to `self`:
    #
    # "Hello from " + self.to_s # => "Hello from main”
    def +: (string other_str) -> String
    end

    View Slide

  51. class String
    include Comparable
    def gsub: (Regexp | string pattern, string replacement) -> String
    | (Regexp | string pattern, Hash[String, String] hash) -> String
    | (Regexp | string pattern) { (String match) -> _ToS } -> String
    | (Regexp | string pattern) -> ::Enumerator[String, self]
    end

    View Slide

  52. Create your own RBS
    f
    iles

    View Slide

  53. # lib/application.rb
    class Application
    def hello(name)
    "Hello, #{name}!"
    end
    end

    View Slide

  54. # main.rb
    require_relative ‘./lib/application
    app = Application.new
    app.hello(“Colby”)

    View Slide

  55. $ rbs prototype rb lib/application.rb
    class Application
    def hello: (untyped name) -> ::String
    end

    View Slide

  56. $ rbs prototype rb lib/application.rb
    class Application
    def hello: (untyped name) -> ::String
    end

    View Slide

  57. $ rbs prototype rb lib/application.rb
    class Application
    def hello: (String name) -> ::String
    end

    View Slide

  58. # sig/lib/application.rbs
    class Application
    def hello: (String name) -> ::String
    end

    View Slide

  59. # Gemfile
    source "https://rubygems.org"
    gem "steep"

    View Slide

  60. # Steepfile
    target :lib do
    check "lib"
    check "main.rb"
    signature "sig"
    end

    View Slide

  61. $ bundle exec steep check
    .....................................................................

    View Slide

  62. # main.rb
    require_relative ‘./lib/application
    app = Application.new
    app.hello(1)

    View Slide

  63. $ bundle exec steep check
    .....................................................................F
    main.rb:4:15: [error] Cannot pass a value of type `::Integer`
    as an argument of type `::String`
    ! ::Integer <: ::String
    ! ::Numeric <: ::String
    ! ::Object <: ::String
    ! ::BasicObject <: ::String
    !
    ! Diagnostic ID: Ruby::ArgumentTypeMismatch
    !
    " puts app.hello(1)

    View Slide

  64. $ ruby main.rb
    Hello, 1!

    View Slide

  65. View Slide

  66. Ruby API doesn’t support RBI at the moment

    View Slide

  67. 😮💨

    View Slide

  68. Call Sequences Type Signatures

    View Slide

  69. $ ./bin/rake import:ruby[3.1.2]

    View Slide

  70. def prepare_environment
    system "unzip #{ruby_src_download_path}”
    if release.has_type_signatures?
    system "gem unpack --target #{download_path.join("gems")} “ \
    “#{download_path.join(“gems/rbs-*.gem”)}"
    end
    end

    View Slide

  71. def prepare_environment
    system "unzip #{ruby_src_download_path}”
    if release.has_type_signatures?
    system "gem unpack --target #{download_path.join("gems")} “ \
    “#{download_path.join(“gems/rbs-*.gem”)}"
    end
    end

    View Slide

  72. def prepare_environment
    system "unzip #{ruby_src_download_path}”
    if release.has_type_signatures?
    system "gem unpack --target #{download_path.join("gems")} “ \
    “#{download_path.join(“gems/rbs-*.gem”)}"
    end
    end

    View Slide

  73. def prepare_environment
    system "unzip #{ruby_src_download_path}”
    if release.has_type_signatures?
    system "gem unpack --target #{download_path.join("gems")} “ \
    “#{download_path.join(“gems/rbs-*.gem”)}"
    end
    end

    View Slide

  74. if @release.has_type_signatures?
    require_relative "./ruby_type_signature_repository"
    @type_repository = RubyTypeSignatureRepository
    .new()
    end

    View Slide

  75. if @release.has_type_signatures?
    require_relative "./ruby_type_signature_repository"
    @type_repository = RubyTypeSignatureRepository
    .new()
    end

    View Slide

  76. @repository ||= RBS::Repository.new(no_stdlib: true).tap do |r|
    r.add(rbs_gem_path.join("stdlib"))
    end
    @loader = RBS::EnvironmentLoader
    .new(core_root: rbs_gem_path.join(CORE_PATH))
    @environment = RBS::Environment.from_loader(@loader).resolve_type_names
    @builder = RBS::DefinitionBuilder.new(env: @environment)
    Setup RBS environment

    View Slide

  77. @repository ||= RBS::Repository.new(no_stdlib: true).tap do |r|
    r.add(rbs_gem_path.join("stdlib"))
    end
    @loader = RBS::EnvironmentLoader
    .new(core_root: rbs_gem_path.join(CORE_PATH))
    @environment = RBS::Environment.from_loader(@loader).resolve_type_names
    @builder = RBS::DefinitionBuilder.new(env: @environment)
    Setup RBS environment

    View Slide

  78. @repository ||= RBS::Repository.new(no_stdlib: true).tap do |r|
    r.add(rbs_gem_path.join("stdlib"))
    end
    @loader = RBS::EnvironmentLoader
    .new(core_root: rbs_gem_path.join(CORE_PATH))
    @environment = RBS::Environment.from_loader(@loader).resolve_type_names
    @builder = RBS::DefinitionBuilder.new(env: @environment)
    Setup RBS environment

    View Slide

  79. @repository ||= RBS::Repository.new(no_stdlib: true).tap do |r|
    r.add(rbs_gem_path.join("stdlib"))
    end
    @loader = RBS::EnvironmentLoader
    .new(core_root: rbs_gem_path.join(CORE_PATH))
    @environment = RBS::Environment.from_loader(@loader).resolve_type_names
    @builder = RBS::DefinitionBuilder.new(env: @environment)
    Setup RBS environment

    View Slide

  80. if method_rdoc.type == “instance"
    @type_repository.signature_for_object_instance_method(
    object: object_rdoc.name,
    method: method_doc.name
    )&.map(&:to_s)
    elsif method_rdoc.type == “class"
    @type_repository.signature_for_object_class_method(
    object: object_rdoc.name,
    method: method_rdoc.name
    )&.map(&:to_s)
    end
    Fetching a method’s signatures

    View Slide

  81. method_type = RBS::TypeName.new(
    name: “String”,
    namespace: RBS::Namespace.root
    )
    // instance methods
    @builder.build_instance(method_type).methods[:to_i]
    &.method_types
    // class methods
    @builder.build_singleton(method_type).methods[:to_i]
    &.method_types
    Query RBS for type signature

    View Slide

  82. Type Signatures also have their
    own challenges

    View Slide

  83. Managed separately from


    ruby-src

    View Slide

  84. Not optimised for humans

    View Slide

  85. String#gsub
    (::Regexp | ::string pattern, ::string replacement) -> ::String
    (::Regexp | ::string pattern, ::Hash[::String, ::String] hash) -> ::String
    (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String
    (::Regexp | ::string pattern) -> ::Enumerator[::String, self]
    Example

    View Slide

  86. String#gsub
    (::Regexp | ::string pattern, ::string replacement) -> ::String
    (::Regexp | ::string pattern, ::Hash[::String, ::String] hash) -> ::String
    (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String
    (::Regexp | ::string pattern) -> ::Enumerator[::String, self]
    Example

    View Slide

  87. Optimised for computers humans

    View Slide

  88. (::int index) -> ::String?
    (::int start, ::int length) -> ::String?
    (::Range[::Integer] | ::Range[::Integer?] range) -> ::String?
    (::Regexp regexp) -> ::String?
    (::Regexp regexp, ::int | ::String capture) -> ::String?
    (::String match_str) -> ::String?

    View Slide

  89. (int index) -> String?
    (int start, int length) -> String?
    (Range[Integer] | Range[Integer?] range) -> String?
    (Regexp regexp) -> String?
    (Regexp regexp, int | String capture) -> String?
    (String match_str) -> String?

    View Slide

  90. (int index) -> String?
    (int start, int length) -> String?
    (Range[Integer] | Range[Integer?] range) -> String?
    (Regexp regexp) -> String?
    (Regexp regexp, int | String capture) -> String?
    (String match_str) -> String?

    View Slide

  91. (Integer index) -> String?
    (Integer start, Integer length) -> String?
    (Range[Integer] | Range[Integer?] range) -> String?
    (Regexp regexp) -> String?
    (Regexp regexp, Integer | String capture) -> String?
    (String match_str) -> String?

    View Slide

  92. Integer index -> String?
    Integer start, Integer length -> String?
    Range[Integer] | Range[Integer?] range -> String?
    Regexp regexp -> String?
    Regexp regexp, Integer | String capture -> String?
    String match_str -> String?

    View Slide

  93. Integer index -> String?
    Integer start, Integer length -> String?
    Range[Integer] | Range[Integer?] range -> String?
    Regexp regexp -> String?
    Regexp regexp, Integer | String capture -> String?
    String match_str -> String?

    View Slide

  94. Range[Integer] | Range[Integer?] range -> String?

    View Slide

  95. (::int index) -> ::String?
    (::int start, ::int length) -> ::String?
    (::Range[::Integer] | ::Range[::Integer?] range) -> ::String?
    (::Regexp regexp) -> ::String?
    (::Regexp regexp, ::int | ::String capture) -> ::String?
    (::String match_str) -> ::String?
    Integer index -> String?
    Integer start, Integer length -> String?
    Range[Integer] | Range[Integer?] range -> String?
    Regexp regexp -> String?
    Regexp regexp, Integer | String capture) -> String?
    String match_str -> String?

    View Slide

  96. Next steps

    View Slide

  97. github.com/rubyapi/rubyapi

    View Slide

  98. Checkout the other talks about
    Ruby Types!
    Types teaches success, what will we do? - @fugakkbn


    Let's collect type info during Ruby running and automaticall - @pink_bangbi


    Ruby programming with types in action - @soutaro

    View Slide

  99. ありがとう

    View Slide