Talks

The State of Ruby Dev Tooling

RubyKaigi 2024

00:00:03.840 Hello, everybody! Thank you for coming. Welcome to the State of Ruby Tooling. My name is Vinnie, and I'm a Staff Developer in the Ruby Developer Experience team at Shapify. Our team takes a comprehensive look into the developer experience for Ruby, covering everything you might do to interact with the language. We created the Ruby LSP, contribute to gradual typing for Ruby, and also contribute to IRB and Ruby Debug. If you want to know more about my work, you can find me on X and GitHub at 'Vin stock'.
00:00:15.920 Is having options a good thing? It's tempting to think that the answer to this question is obvious and universally true. More options lead to greater flexibility, a sense of freedom, and autonomy. However, it's not that simple. A paper from 2000, titled 'When Choice is Demotivating', highlights a crucial aspect of previous research into the psychology of decision-making. Researchers discovered that much of the work before them focused on evaluating decisions made with a small number of options, not particularly complex ones. This prompted them to wonder what would occur if the complexity of decision-making or the number of choices increased. Would people still feel that having options was positive? It turns out that the answer is not always affirmative. This can lead to what's known as Choice Overload: when the effort to make a decision becomes so burdensome that individuals may resort to cutting corners to reach a conclusion. They might focus on a smaller subset of available information, simplify their decision-making processes, or completely avoid the effort by opting for default choices or making no decision at all.
00:01:30.880 Today, I'd like to discuss options in the context of Developer Experience (DX) and our current status in the Ruby community concerning DX. I want to begin by addressing a common belief, particularly prevalent when discussing tooling online. Many claim that creating tools for Ruby is challenging due to its dynamic nature. While there is some truth to this—without type annotations, the accuracy of static analysis may suffer—this is not the primary challenge we face. Ruby is not the only dynamic language out there; JavaScript also falls into this category. The solution for more accurate static analysis in JavaScript is TypeScript. In the Ruby community, we have similar options like Steep and Sbet, which can provide highly accurate static analysis through gradual typing. Therefore, I believe our most significant challenge today concerning a modern developer experience is the fragmentation in our tooling.
00:01:48.400 Let's follow the story of a new junior developer. Starting out, they want to try Rust and Ruby and desire a complete developer experience that includes everything available for programming languages today. When starting with Rust, the initial step is figuring out how to install the language. The Rust website recommends using the official tool, Rustup, to manage the Rust installation. Moving on to unit testing, the user does not need to configure anything. The test framework is integrated into the language, allowing them to simply start writing and executing their tests. For formatting, there’s an official tool maintained by the Rust team called 'Rust Format'. If they decide to add linting to the project, the Rust team offers 'Clippy', an official tool. Moreover, to integrate all of this into their editor and access rich features such as completion and go-to definition, the Rust team also maintains 'Rust Analyzer'.
00:02:20.679 This cohesive experience where most tools are official and maintained requires almost no configuration. In contrast, in the Ruby ecosystem, when our junior developer turns to Ruby, the experience is markedly different. Their very first step involves the daunting choice of how to install Ruby or manage their Ruby installation. There is no official method, so they will have to evaluate several different tools, many of which might install Ruby, others that allow version switching, and some that do both. Additionally, many may not be cross-platform or only support a limited number of operating systems. Thus, they must already make decisions when faced with multiple options.
00:03:05.279 When we move on to testing, this category illustrates an interesting situation. While we do have two default options, 'Test Unit' and 'Mini Test', included with Ruby installations, we cannot offer users a zero-configuration experience because it's unclear which default they will choose. Similarly, if someone wants to adopt a gradual typing system for Ruby, they have additional decisions to make. Should they opt for complete gradual typing systems like Sorbet or Steep, or choose newer approaches such as Typr? The same goes for formatting and linting. Many tools in the Ruby community serve both purposes, adding to the confusion with multiple choices.
00:03:41.960 In terms of integration into the editor, developers have to navigate through and select from families of different language servers. You have the type checking family that focuses on accurate static analysis features, general language servers providing a decent experience irrespective of whether typing is adopted, and the formatting family of language servers. The complexity and variety persist across other categories. For a junior developer seeking a full experience, they will likely explore at least one tool from every category. However, for the sake of this argument, let’s examine the scenario where they can only pick one from each category, which isn't the reality as multiple tools can be combined.
00:04:23.600 Representing this visually, I would mention that Rust offers a straightforward path where developers can select the next default tool, leading to great ease of use. Conversely, in Ruby, there are approximately 13,000 ways one could configure their environment! Visualizing this provides insight into the overwhelming options available in Ruby, making it very challenging to ensure integrations function across different combinations. Each Ruby project may utilize different sets of tools, which leads to the realization that knowing Ruby alone might not be sufficient for making successful contributions. For example, if you are accustomed to gradual typing and attempt to contribute to a project without typing, your editor experience will likely not meet your expectations.
00:05:13.840 Alternately, moving in the opposite direction may also present challenges where expectations around tooling specifics must be learned in order to contribute effectively to various projects. The main issue we face with Ruby's development experience is the absence of established defaults and guidance. Ruby does not prescribe a default developer experience, resulting in a fragmented tooling ecosystem. This fragmentation makes it difficult to ensure seamless integrations.
00:05:59.400 Integration is cumbersome due to the presence of multiple versions of similar tools. If there’s a breaking change, the integration could fail, making it arduous to stay updated. Developers are often fraught with excessive decisions about the tools and configurations they must choose. For new developers encountering this, it is unrealistic to expect that they will understand the trade-offs involved in their choices across so many categories. The configuration required to make tools function correctly can become overwhelming due to these integrations. Thus, many developers often resort to offering a multitude of configuration options, allowing users to navigate their integrations independently.
00:06:43.680 This leads to a steeper learning curve, as developers must acquire knowledge that is specific to certain tools, not just general Ruby programming skills. Lastly, this dispersion of focus can dilute community efforts. For context, when we consider other ecosystems such as JavaScript, they face similar fragmentation issues, some even more severe. For instance, dependency management and runtime fragmentation are prominent. However, the JavaScript community has the advantage of a significantly larger base of contributors.
00:07:28.240 According to the last Stack Overflow survey, JavaScript was the number one programming language used, with over 63% of respondents, while Ruby was utilized by just over 6%. As such, the JavaScript community may have the breadth of investment and contributors necessary to sustain a multitude of tools. In contrast, the Ruby community may not be able to afford to fragment efforts too heavily across such complex tools.
00:08:12.320 In looking at contributor statistics for Rust Analyzer, it currently boasts over 800 contributors. Comparatively, if we sum up contributors across Steep, Sbet, Solargraph, Ruby LSP, and Typr—all five are actively maintained language servers—we notice that we fall short significantly in contributor numbers even when accounting for repeated contributions from bots like 'Pandabot'. This disparity impacts the quality of our tooling more significantly than the dynamic nature of Ruby itself.
00:08:49.680 I want to stress that there are certainly situations where developing new tools is beneficial. The intent here is not to inhibit the creation of tools. You may very well explore innovative approaches or base implementations on technologies that are emerging. That said, simply creating more tools doesn’t guarantee an enhanced experience. We must consider the overarching view of developer experience and how every tool contributes to the larger picture. We could inadvertently complicate integration or increase the configuration burden on users who are trying to establish their development environments.
00:09:29.320 Developer experience isn't just about an isolated tool; it encompasses how tools interact, creating a unified approach allowing effective engagement with the language. Referring back to the earlier article that highlighted the dangers of decision paralysis—what we aim to prevent is the scenario where evaluating which tools to adopt feels so daunting that users disengage from entire categories of tools or, in extreme cases, abandon Ruby altogether in favor of alternatives.
00:10:06.240 We wish to emulate Rust’s success, where the default options are so compelling that developers can use them without making multiple decisions. Developers should seamlessly enjoy a fantastic experience without encountering hurdles in setting up respective tools.
00:10:46.080 Using tools is never a developer's ultimate goal. No developer aspires to merely use the Ruby LSP, Rails, or Sorbet; those are merely stepping stones. The larger objective focuses on building and creating, indicating that we should aim to make setting up development environments altogether effortless, thereby allowing users to concentrate all of their energy on the task they want to complete.
00:11:09.440 Now, I’d like to shift gears and discuss the role of tools within project dependencies. In Ruby, it is common to see tooling added to the gem file, incorporated into the project’s dependencies. Managing development tools within the gem file allows consistency across projects in terms of versions, which is key when ensuring that developments remain consistent across CI and among various developers engaging with the same project.
00:11:32.360 However, this merely leads to version consistency within the specific project and doesn’t guarantee uniformity across different projects unless updates are synchronized for every tool. Considering the Rust community once more, most tools are generally installed globally as Rustup components. Consequently, if you want to install a linter, formatter, or language server, it becomes a part of your language installation process. This approach yields several benefits, including guaranteeing version consistency across projects. Developers only need to update the global installation to ensure it aligns with the latest version; this significantly simplifies the maintenance burden.
00:12:04.240 In addition, the globally installed tools are already configured as part of the system’s PATH, making launching executables straightforward. This facilitates integration with processes from other applications, such as editors. The overall maintenance of projects becomes much less cumbersome as updates for a particular tool no longer need to be handled across multiple projects—updating one time for the entire system suffices.
00:12:48.480 While Ruby and Rust are fundamentally different languages, which makes certain implementations challenging, there are still areas we could explore. For instance, we could work towards developing a default version manager that enables launching any gem executable using the correct Ruby version for each project. Alternatively, we could enhance the integration of globally installed gems with the project’s bundle, allowing smoother access across dependencies. A plugin calling 'Bundler Compose' has already set the groundwork for these ideas.
00:13:28.480 What does the future for Ruby tooling look like? It’s essential to acknowledge that Rust stands out as the exception. Most popular languages, including Ruby, Python, and JavaScript, do not have official default tooling, inevitably leading to fragmentation. While Go has come close to achieving effective tooling, the older languages on the left side of a list compared to more modern languages like Rust illustrate a clear difference. Rust's more cohesive developer experience stems from the fact that many modern tool-building techniques were conceived just as Rust emerged.
00:14:10.560 Ruby, in contrast, has grown organically over time. This evolutionary paradigm has generated a diverse array of tools with numerous versions, and while Rust benefited from thoughtful planning, our community stands to learn significantly from Rust's success. There’s already a concerted effort towards integration with Ruby’s new parser, Prism, which intends to unify our static analysis efforts into a cohesive framework.
00:14:51.280 For instance, our team aims to upstream the code indexer from the Ruby LSP into Prism, providing a foundation for other static analysis tools. Tools such as Ardo, which generate documentation based on code indexing, could benefits immensely by sharing this index, thereby improving results for both the language server and documentation efforts. The prospect of multiple utilities deriving their functionality from a shared source creates efficiency and reduces maintenance strains. We can create a formatter or a code mod engine based on Prism, allowing us to implement significant features like deprecation frameworks with swift auto-fixes.
00:15:24.000 In view of these shared building blocks, we could substantially ease integration processes, allowing tools to interface seamlessly through a unified language server system. This integrated approach promises a smoother developer experience in Ruby, where various tools can collaborate effortlessly based on shared indexing and functionality.
00:15:59.520 Currently, we are captivated by a project we're referring to as 'Ruby LSP Add-ons'. This initiative aims to allow other gems to embed essential integrations within editors without necessitating independent language server expansions or extension writing. It relieves users of the complexities involved with text synchronizations and encoding management that typically accompany building incremental static analysis tools tailored specifically for Ruby.
00:16:43.920 This project allows add-ons to hook seamlessly into Ruby LSP, gaining access to tools already established within the core language server—such as static analysis systems, code indexing functions, type resolution, and response formatting to the editor. All of these aspects are designed to make the integration straightforward. This initiative would ensure there's only one language server representing the codebase. External tools could then enhance language server functionality without needing to duplicate efforts.
00:17:30.960 For example, features from existing tools like a type checker would enhance the accuracy of the language server responses, while automatic code formatting and violations could originate from a linter integrated into that same framework. Running tests from an editor is another feature we've already implemented with support for numerous testing frameworks.
00:18:09.200 We currently have available several add-ons that bolster functionality related to Rails or RSpec support, enabling users to run tests directly from within RSpec files. Additional integrations include functionalities for maintaining existing tools like RuboCop and MiniTest, all contributing to an enriched development environment.
00:18:38.640 To wrap up, I have two key takeaways from this talk: First, we must consider the holistic perspective, evaluating how tools interact cohesively rather than examining tools in isolation. Secondly, we need coordinated efforts among tool maintainers striving toward our common vision, ultimately assuring an enhanced developer experience for Ruby.
00:19:10.080 Thank you very much for your attention.