Talks

Adding Type Signatures into Ruby Docs

RubyKaigi 2022

00:00:01.079 I want to start my talk today by telling a story. I think we are all familiar with how searching for Ruby documentation can be quite difficult. Often, when we search for documentation, we get results that don't meet our expectations. Some results are blog posts that may only sort of relate to what we're looking for, while others are for documentation from Ruby that is three or four years old.
00:00:17.820 The official Ruby documentation is an excellent and reliable source of information. It contains documentation from Ruby version 2.0 to the latest and greatest version of Ruby, including the development branch. All the versions are separated by a path, so you won't have documentation leaks between different versions. The Ruby docs also have a search function that allows you to query the documentation.
00:00:31.300 However, you may have noticed in the past that when you've used it, RDoc orders its search results alphabetically, which can lead to an unoptimized set of results. This can take a long time for users to scroll through to find exactly what they are looking for.
00:00:44.920 Taking a specific example search for the `to_s` method, it presents four options in the top search results: `Info`, `Array`, and `Benchmark`. While some users might find what they're looking for, I believe the majority are looking for something else. There is an opportunity to utilize an alternative method that prioritizes core Ruby objects, which are the objects users most frequently search for.
00:01:26.040 Back in 2019, I started asking myself if I could build something that would consume and understand the Ruby source documentation and what developers consider important. In 2019, I created an app called Ruby API. This app addresses the difficulties people experience when trying to find documentation quickly. Please feel free to check it out during this talk; I think it’s a great resource.
00:01:34.680 Ruby API parses all the Ruby standard library documentation and builds links to the most commonly searched pages. It supports multiple versions of Ruby, allowing you to navigate to the version best suited for your application. Elasticsearch powers the search functionality, optimizing for the most popular Ruby methods and objects. For example, the core Ruby objects like `String`, `Array`, `Integer`, `Float`, and `Symbol` are scored much higher compared to less common objects.
00:01:58.440 Recently, Ruby API has experienced a large uptick in usage. In July of this year, it peaked at over 80,000 visitors and over a hundred thousand page views. I'm glad I could build something that people find useful, and I appreciate all the kind and welcoming feedback.
00:02:21.979 Similar to the normal documentation, Ruby API allows you to see the method source code, including C code. One of my favorite features of Ruby API is that you can execute any documentation example against the version of Ruby running in AWS Lambda. The string object page contains everything you're already familiar with, like method-less object ancestors and detailed descriptions, all pulled directly from the Ruby source code.
00:02:46.799 When I first built Ruby API, I focused on user experience. I wanted to use typography that was easy to read for long sessions, with accessible colors and lots of spacing. After its development, I started to ask myself how I could work on improving the documentation itself.
00:03:09.960 Good afternoon, everyone, and thank you for taking the time to attend my talk today. I will be speaking about type signatures and the ideas I have for improving the Ruby documentation ecosystem by utilizing these signatures to create a more consistent and accessible documentation experience.
00:03:32.880 For those who don't know me, my name is Colby. You can follow me on Twitter; that’s my handle underneath. I often talk about things I'm currently working on and ideas I have. It's the best place to reach me if you have any questions after this talk. I flew in from the wonderful city of Melbourne, Australia. I took this photo last year at Albert Park Lake, where the Melbourne Formula One races are held every year.
00:04:03.540 I also want to express gratitude to the conference organizers for enabling me to attend RubyKaigi in person this year. I wouldn't be here today without their support, and it’s so wonderful to be able to give talks and see everyone in person again after all the lockdowns and border closures. This is my cat Claire. She is a two-year-old Tabby who helped me navigate the very strict lockdowns in Melbourne over the past two years.
00:04:43.540 I currently work as a senior backend engineer at Caliber Analytics, which is a small company focused on building a web performance platform to create a faster and more equitable internet. We believe that anyone can positively impact user experience with the right tools and page speed literacy. Please check out our site at caliberapp.com. You may be familiar with some of my previous work, specifically Bungalow and RubyGems.
00:05:13.500 Unfortunately, during the pandemic, I contributed very little, if anything at all, to both projects due to severe lockdowns and a dip in motivation. During that time, I've been working on Ruby API and in recent months, I began developing a new feature that allows users to switch from the standard RDoc call sequences to RBS type signatures.
00:05:35.940 When you click the toggle button in Ruby API, it switches from showing the RDoc call sequences to displaying the type signatures in RBS. The list of examples on how to use a specific method in Ruby documentation is referred to as call sequences. While RDoc calls these 'calling sequences', I prefer calling them 'call sequences' since it’s easier to say.
00:05:53.880 When looking at the documentation for any method in Ruby source, it’s almost always the method's call sequence you will see. It’s a list of the different ways a method can be used, as documented by the gem authors and Ruby contributors. The type signatures presented on the right is an alternative way of showing those method usages using the signatures from RBS.
00:06:19.260 To better understand why this feature exists, let's take a quick look at the current status quo and some of the challenges present. Most people in this room will be familiar with RDoc already, but I want to give a brief overview of how documentation for Ruby source and its standard library currently works.
00:06:43.200 If you're new to Ruby, welcome to the community; I hope you're enjoying RubyKaigi so far. RDoc is the primary documentation tool we use for writing documentation for our libraries and it generates all Ruby source and standard library documentation. RDoc is part of the set of libraries included with Ruby, commonly referred to as the standard library.
00:07:05.520 You can find the source code on GitHub as part of the Ruby organization, where it's actively maintained by the Ruby core team and contributors. Creating documentation for your code is very straightforward using comments. You can add documentation for your objects, methods, attributes, and constants using RDoc's markup language, which allows you to format your document with styles like bold, italics, links, lists, and more.
00:07:29.740 The great thing about RDoc is that it can even read and format any RDoc-formatted document regardless of whether it includes actual code. In fact, most of the docs inside Ruby source are formatted using RDoc files, as illustrated in the examples on screen.
00:07:53.359 Once you have documentation, you can use the RDoc executable to generate a set of HTML files. You simply specify the directory you want to generate your documentation from. Once RDoc processes your source code, it will run everything through a series of parsers to ultimately generate a series of HTML pages.
00:08:15.579 RDoc also supports different output formats if you wish to generate something besides HTML. I have a fun question for the audience: Does anyone know what the 'pot' format outputs? This format is part of Ruby and is supported by RDoc in recent versions of Ruby 3...
00:08:39.060 If you need a hint, let me know! It's fun to note that RDoc supports taking your code documentation and turning it into PowerPoint slides. Additionally, you can very quickly generate documentation programmatically using RDoc. All you need are two variables: one contains the RDoc environment and the other is a list of options you wish to pass in.
00:09:00.300 This path includes the source code that contains your documentation, essentially being the current directory. The created RDoc file will use a specific template to render the documentation and a flag to minimize console output. Programmatically calling RDoc in this manner is how Ruby API ingests Ruby source documentation for each version.
00:09:20.338 RDoc even supports code that's not written in Ruby. For instance, this code snippet is taken directly from Ruby source, specifically the string implementation. It’s okay if you’re not familiar with C; I won't dwell on this, but I wanted to show you an example of what it looks like.
00:09:46.040 RDoc markup supports more than just basic formatting. A common feature you'll see in the majority of documentation for Ruby source is a section called the 'call sequence'. The call sequence contains examples of a method's inputs and outputs.
00:10:10.420 The example of the empty method is pretty simple, so it only has one entry in the call sequence, which doesn’t take any arguments and returns nil. RDoc gives priority to the call sequence when rendering documentation to HTML. If the method's documentation has a call sequence, RDoc will generate that sequence.
00:10:34.760 However, if the method does not have a call sequence, it will only render its name. A more complex example is the `encode` method for the string object, which shows multiple entries. These entries describe the various inputs and outputs of the `encode` function, including default arguments, keyword arguments, and return types.
00:10:58.160 Something important to know is that the creators and maintainers of Ruby source write the call sequences themselves. When a new method is merged, someone must write its documentation, including updates. This can present several challenges.
00:11:29.698 Here’s an example of the square bracket method for string objects. This method has multiple entries in its call sequence, but you may notice a few inconsistencies. Some arguments merely state the type of input, while others are the actual argument names.
00:11:56.839 Additionally, the label for the returned value doesn't always align consistently. There are also challenges not just regarding the contents of the call sequence but also in how this information is presented to users. A brand-new programmer might not know what a regex is but may want more information.
00:12:20.480 I am sure we can all recall how daunting the Regexp object seemed when we first learned programming. RDoc does not generate links for the different objects, leaving it up to users to find those particular pages.
00:12:43.500 Moreover, there’s currently no syntax highlighting for the call sequence. I believe many of us would appreciate it if RDoc had support for syntax highlighting in this feature.
00:13:05.900 I started to ask myself what I could do to help solve these problems. Given the challenges with maintaining a consistent call sequence, I pondered if there was something else we could utilize instead.
00:13:26.480 Thanks to a new library released as part of Ruby 3, we have the option to generate method sequences automatically. Before I can explain how this works with the RBS, I need to discuss what RBS is. RBS is a new type signature system for Ruby; it allows you to describe your application's objects, methods, attributes, and constants using a set of files called RBS files.
00:13:49.059 The RBS library is capable of validating whether your application conforms to those type signatures. RBS is also part of the Ruby set of standard libraries, is open source, and you can find it under the Ruby project on GitHub.
00:14:10.059 There are two main components of RBS to understand. The first is the RBS file, which contains the signatures. You will notice that it looks similar to Ruby, but there are some key differences. Firstly, these files are not Ruby; they feature a unique ruby-like syntax specifically for RBS.
00:14:34.080 There is no implementation code present, and RBS files strictly serve to define your signatures and the structure of your code, akin to how languages like C use header files. Much like call sequences, methods in RBS can have multiple signatures. An example would be the `gsub` method from the string object.
00:14:54.340 You can also define your own RBS files and have them type-checked by the RBS library. I've created a simple hello world application as an example. It features a single method that takes one argument and prints hello.
00:15:14.179 I've added a `main.rb` file that handles the logic for loading the appropriate file, initializing the application, and calling the hello method. A cool feature of the RBS tool is its ability to generate starting templates for your type signatures based on your existing code.
00:15:36.659 Using the `rbs prototype` command, you can input a file to generate a corresponding RBS file, which the RBS tool will then render to your terminal. For the most part, RBS generates successful templates, with only a few exceptions like missing untyped name arguments.
00:15:52.859 However, we can easily substitute the appropriate types, and for demonstration purposes, I’ll just use 'String' here as the only type. In RBS documentation, it's common to organize your type signatures within an 'sig' directory, maintaining a structure that reflects your application layout.
00:16:12.240 For example, signatures for your library components would be placed in the `sig/lib` folder. Unfortunately, the RBS command lacks the tooling to facilitate easy manipulation of your own RBS files, something I hope will change in the future.
00:16:29.659 The author of RBS has developed another gem called Steep that enables easy type-checking of your own RBS files. Steep features a configuration file that allows you to set up your Ruby environment so that RBS can verify your own files.
00:16:41.459 The Steep file is similar to a Gemfile, allowing you to organize targets and provide settings for type checking. When you execute the Steep check command, it validates your types against both your code and Ruby's standard library.
00:17:03.400 Let's quickly demonstrate an update to our code using an incorrect type. I swapped the string with an integer and we can run the Steep check command again to validate our types and confirm it accurately identifies the type error in our code, providing a helpful error message.
00:17:26.480 It's noteworthy that your code will continue to run even if RBS detects an incorrect type—it does not raise runtime errors for type issues. You may be familiar with another typing system from Ruby called Sorbet, which has been available for a few years. I wish I could discuss Sorbet further in this talk, but I don't have enough time.
00:17:50.020 As of now, Ruby API does not have support for Sorbet's RBI type signatures. The next feature I am working on is importing documentation from RubyGems, and if there’s sufficient support from gems utilizing Sorbet RBI files, I definitely want to add this support in the future.
00:18:13.560 Thank you for bearing with me during this lengthy explanation; take a moment to breathe and relax. Now, returning to Ruby API, you have learned about the origins of call sequences and type signatures. Ruby API serves as a simple interface for importing documentation.
00:18:39.420 It features a rake task that downloads the specified Ruby version, unzips the file, and runs the RDoc tool. Initially, Ruby API prepares the local environment by downloading a copy of Ruby source from the Ruby website for the designated version.
00:19:09.580 Not every version of Ruby supports type signatures, so we perform a quick check to determine if we can proceed. Afterwards, we unpack the RBS gem within the local file system, allowing us to access all the type signatures it contains.
00:19:35.500 If the version of Ruby imported into Ruby API has type signature support, we encapsulate all of the RBS functionalities, using the path name to the folder containing the extracted RBS gem.
00:20:05.420 Our main goal with this class is to establish an RBS environment, akin to how we create an RDoc environment. This environment will let Ruby API look up type signatures from the downloaded Ruby source.
00:20:26.959 A key aspect of this code is that we instruct RBS so that it does not load standard library signatures from the Ruby version currently in use. Ruby API could execute this code on one version of Ruby while importing a different one, so we want to prevent any signature leaks between them.
00:20:53.279 Next, we want to add the path for the standard library signatures that we unpacked from RBS earlier. Lastly, we need to add the path that includes the type signatures for core Ruby classes like `Integer`, `String`, `Hash`, `Symbol`, and `Float`.
00:21:16.140 Once the RBS environment is set up, we can then proceed to look up method type signatures. We need to determine the type of method before querying RBS, as instance methods and class methods are treated differently.
00:21:40.620 By establishing the name, the method type, and the method name, we can ask RBS for the specific type signatures. Now, I am not claiming that type signatures provide a perfect solution. They come with their challenges.
00:22:02.580 One surprising aspect is that RBS signatures are maintained separately from the Ruby source code. Currently, the types used for core and standard library classes are managed outside Ruby source.
00:22:24.600 This can potentially lead to situations where the inputs and outputs of a method, the call sequence documentation, and the RBS type signatures become out of sync, causing confusion for users.
00:22:50.560 Type signatures must first and foremost be understandable by a machine so they can systematically validate your code. However, the syntax is not particularly user-friendly. Here's a more complicated example—the signature for the `gsub` method.
00:23:12.560 In this signature, you may notice several types at play, even one that yields to a block. This signature can be tricky to read and understand at a glance. You may wonder what `T` or `_t` signifies. This is an interface within RBS that checks if an object implements the `to_s` method.
00:23:36.000 Over the past few months, I have been considering how we can enhance type signatures to improve their readability for humans. For example, there are opportunities to remove the double colons that prefix all class objects to simplify the presentation.
00:24:00.060 You might observe that in RBS, some types are abbreviated (like `int`), and others use the full form (like `Integer`). The reason behind this inconsistency often escapes me; perhaps someone here can shed light on it.
00:24:22.120 You'll also notice that the round brackets seem unnecessary most of the time. Therefore, we could remove those as well, although it might depend on the context. Lastly, making the text color-coded would greatly improve readability.
00:24:40.020 Currently, Ruby API does not support syntax highlighting for type signatures, but I would greatly appreciate it if this feature could be added in the future. With these adjustments, the signatures would become much more user-friendly.
00:25:02.940 What are your thoughts on this? Are there additional changes we could implement? One thing I would like to see is the capability to link types and signatures to corresponding documentation.
00:25:26.340 Not all mappings will be one-to-one, but I believe this presents a good use case. For instance, a beginner who is uncertain about what regex is could click a link for further information, making it easier to refresh knowledge about what a range object is.
00:25:48.440 Here’s a before-and-after comparison to illustrate that these suggestions could significantly enhance the readability of type signatures. There’s much more we could do. By reformulating and reorganizing these signatures, we can create a better user experience, possibly including tooltips or text-friendly versions.
00:26:10.960 All the ideas I've shared today have been on my mind for quite a while, but I've yet to propose them to the RBS maintainers or Ruby source contributors. I would love the opportunity to discuss these ideas further with them during RubyKaigi or afterward.
00:26:31.800 I would also like to hear your suggestions for how we can improve Ruby's documentation. Have I overlooked anything? You can find Ruby API on GitHub; we’re a fairly small community but always welcome feedback and ideas on how to enhance Ruby documentation.
00:27:02.560 I would love to experiment with concepts on rubyapi.org that could possibly be merged back into RDoc in the future. If you're interested in further exploring types in Ruby, I recommend checking out other type-related talks happening at RubyKaigi.
00:27:10.560 Lastly, although documentation may seem dull and unexciting, having a robust language and API documentation is crucial for a healthy language ecosystem. I am deeply passionate about this topic and appreciate your time in listening to my talk.