Code Quality

Summarized using AI

Gradual Typing in Ruby - A Three Year Retrospective

Ufuk Kayserilioglu and Alexandre Terrasa • November 08, 2022 • Denver, CO

In their presentation "Gradual Typing in Ruby" at RubyConf 2021, Ufuk Kayserilioglu and Alexandre Terrasa from Shopify discussed their three-year journey of adopting gradual typing in their codebase utilizing Sorbet. They emphasized that Ruby is inherently a dynamically typed language and that the introduction of gradual typing aims to enhance type safety without sacrificing the dynamic nature that developers love. They outlined several critical points regarding this adoption journey:

  • Human and Technical Challenges: The transition to gradual typing posed both human and technical challenges. It was crucial to address the developers' understanding and perception of the benefits of adopting this methodology.

  • Importance of Gradual Adoption: Emphasizing a gradual approach allowed teams to incrementally adopt the typing process, as opposed to a forceful complete overhaul which could lead to resistance. This flexibility meant developers could adopt typing at varying levels of rigor at their own pace.

  • Building Support Tools: The team created a suite of tools, including Robocop Sorbet, tapioca (for generating RBI files), and an internal metrics dashboard called Sorbet Metrics. These tools facilitated quicker adaptation by automating error checks and generating documentation for gem dependencies, which helped developers communicate better about their code and expectations.

  • Positive Metrics and Feedback: After implementing these strategies, the teams observed a significant increase in type coverage in their monolith, with only 8% of their files remaining untyped. Feedback from developers improved over time, demonstrating growing satisfaction and understanding of the benefits of typing. Surveys showed increased recognition of Sorbet catching errors before code reached production.

  • Continuous Improvement: The speakers highlighted the importance of continuous adaptation and tool improvement while also contributing back to the community to enhance Sorbet's capabilities, especially regarding Ruby on Rails functionalities.

  • Culture of Collaboration: By treating developers like customers, the team ensured that their needs were constantly assessed through feedback, which directed future enhancements and support for using Sorbet effectively.

In conclusion, the journey towards gradual typing in Ruby at Shopify illustrates that with the right tools, methodology, and collaboration among teams while respecting the dynamic nature of Ruby, significant improvements in code quality and developer satisfaction can be achieved.

Gradual Typing in Ruby - A Three Year Retrospective
Ufuk Kayserilioglu and Alexandre Terrasa • November 08, 2022 • Denver, CO

We began adopting gradual typing on Shopify's giant monolith almost 3 years ago. Today, we are running the monolith with great type coverage and even see some internal teams commit to stricter typing in their components.

The road to get here was not easy, though. We had to work with our developers to solve the right problems at the right levels of abstraction to ensure the adoption was healthy. This talk will go over some of the challenges and some of our wins along the way. It will also help you decide if gradual typing might work for your codebase and your team, as well.

RubyConf 2021

00:00:14.960 I was on stage in Nashville at RubyConf 2019, where I was talking about how we adopted Sorbet at Shopify's scale. Since then, it's been about three years since we started adopting Sorbet on our codebase. So, it sounded like a good opportunity to take a step back and share some of our learnings and conduct a small retrospective. Before we go into that, I want to talk a little bit about gradual typing.
00:00:42.160 A lot of the conversation in Ruby today is centered around static typing, but we prefer to call it gradual typing for a few reasons. Ruby, as we all know, is a dynamically typed language, and we love Ruby because of its dynamic nature, which allows us to build frameworks like Rails. However, static analysis is a fast and powerful tool. For instance, in our codebase at Shopify, our core monitors using Sorbet can perform full parsing, understanding, and type checking over 40,000 files in seconds. In fact, it takes about 25 seconds to run on a developer's machine. This is impressive because Sorbet can do all of this statically without having to run or load our code.
00:01:19.200 We want to avoid turning Ruby into another language, such as a CoffeeScript-like language. Our codebases at Shopify are immense; we have over 40,000 Ruby files in our monolith. If we were to enforce static typing, it would mean adding types to all our files, or even worse, rewriting our codebase in a different language. We love Ruby, so gradual typing is the best middle ground. The reason we refer to it as gradual typing is that you don’t have to fully commit to types right away; you can adjust the level of adoption to suit your needs, whether that’s a little or a lot. That’s why we will be focusing on gradual typing rather than just static typing.
00:02:26.080 Before embarking on this journey, there were a few challenges that we needed to address as a technical team. It's easy to concentrate on the technical challenges that need solving, but the more important issues are the human challenges. How do we make it easier for teams to adopt this new gradual typing in our codebase? This is an important question, and if we can’t solve that, then adoption will falter. What benefits do the teams gain from this adoption? If we don’t communicate those benefits clearly, people will be less motivated to adopt it. Furthermore, how do we effectively advertise those benefits? It is not enough for the benefits to exist; they need to be understandable and known by the teams.
00:03:33.040 There are also technical challenges to consider. Ruby is a very dynamic language; we often create classes and methods at runtime. For instance, a new module can be created at runtime, defining methods conditionally. This makes it nearly impossible to perform static type checking or fully understand what the code is doing without executing it. Moreover, the dependencies we use—gems— function like black boxes. When you rely on a gem, how can you know how it works? You either check the documentation or look at the source code, as there are typically no artifacts that describe what classes or methods it exports or the parameters those methods take.
00:04:34.240 So, for static typing tools, dependencies present a significant challenge. How do we address these issues? First, we need to ensure that we are making the right decision about introducing gradual typing. We built a series of experiments to test whether using gradual typing with Sorbet was beneficial for us.
00:05:01.199 We started by gradually migrating to typed files. This meant checking whether the constants used in our files were defined properly. As we began migrating some of these files to typed forms, we noticed that we were seeing fewer syntax errors arising from naming issues in production. When we checked the methods called on the constants we were using, there was a notable reduction in production errors. It’s important to mention that while we saw improvements, we are still utilizing metaprogramming.
00:05:36.080 We realized that Sorbet was introducing more rigor into our coding practices within the monolith. The signatures we defined were creating evergreen documentation, making it easier for teams to communicate expectations. Sorbet is helping us form contracts between teams. This approach was proving to be the right choice for us. Moreover, we could adopt it gradually, allowing teams to progress at a comfortable pace. However, to facilitate this for all teams and developers, we knew we had to tackle the numerous rough edges that we encountered when we began this journey.
00:06:30.000 One of the first things we did was create Robocop Sorbet, an open-source set of rules for Robocop that helps with code translation. This means that when you have a codebase and need to make changes to ensure compatibility—such as modifying the account types you use—you can utilize Robocop Sorbet to help automate this transition. This speeds up the adoption of Sorbet while also maintaining code quality.
00:07:35.199 Next, we had to tackle the issue of gems being black boxes. We needed a solution that could comprehend the contents coming from these methods without requiring the application to be running. Therefore, we created the Swiss Army Knife of RBI generation. The first version could load the gems required by your application using the Gemfile and generate interface files for Sorbet to understand what was being exported by those gems.
00:08:22.560 The next steps involved finding a way for Sorbet to understand these elements without actually executing the application. Hence, we developed a second version of Tapioca that loads your application, finds everything you use at runtime—such as ActiveRecord, and identifies the types and methods that are defined. This way, it generates RBI files that Sorbet can utilize, ensuring that even without static artifacts in your codebase, it can correctly type-check your program.
00:09:04.000 Furthermore, to make Tapioca functional, we needed a framework for generating and manipulating RBI files, which we also open-sourced as Airbi. This framework enables the creation, editing, and saving of RBI files onto disk. After making it possible to use Sorbet meaningfully in our codebase, we wanted to assess its adoption across our teams and gauge how entrenched it was within our culture.
00:10:05.519 To track this, we designed an internal survey adoption dashboard called Sorbet Metrics. This dashboard helps us visualize how much Sorbet is incorporated into various codebases, analyzing usage levels and identifying where stricter typing is still needed. Because we wanted to share this tool with the community, we also open-sourced it within Spoon, our toolbox for Sorbet enthusiasts, which includes visualizations similar to what we used internally.
00:10:48.320 Using Sorbet Metrics and Spoon, we can visually represent our progress in type coverage—both for our own internal tracking and for teams that utilize Sorbet in their components. With this visual data, we can easily see how many files are still untyped versus those that have been migrated to typed forms. For example, currently, in our monolith, only eight percent of files remain untyped, which translates to around 39,000 files that have been typed. This makes progress very encouraging.
00:11:53.440 Moreover, we created a system to track everything in an easy-to-understand format. The dashboard displays a map showing the structure and typing strictness of your codebase, utilizing color-coding to represent different levels of strictness. Developers see that areas with more red files need attention—these are generally grouped components that still require typing. This system allows teams to understand where to focus their efforts and promotes progress.
00:12:59.360 We also use Sorbet Metrics to monitor general adoption across various projects. Currently, there are over 880 projects at Shopify utilizing Sorbet. Encouraging autonomy is also essential to us; we want developers outside our team to engage with Sorbet and start utilizing the different tools we provide. We keep track of user engagement through analytics, showing how many distinct users are running Sorbet commands and in what contexts—whether on CI or their local machines. This data is exciting because it means developers are catching type errors locally before they reach staging or production.
00:14:46.720 Simultaneously, we want to encourage more strict typing in individual codebases. To this end, we created a culture dashboard that helps identify components that have not adopted strict typing. Developers can filter by component or error type to see which files still need attention. Each file that gets migrated to a higher type gets an issue closed automatically, gamifying the typing process and motivating developers to engage with it. This approach makes the process enjoyable and provides visibility into what each team is accomplishing.
00:15:53.920 Ultimately, we aim to treat all developers at Shopify as customers. That means providing solutions and understanding their needs and satisfaction levels through periodic surveys. We ask for feedback about their experiences with static typing and the tooling we offer, which helps us gain insight into what the developer community at Shopify needs and what they want us to prioritize next. We also strive to deliver comprehensive documentation on how to use Sorbet effectively, providing internal guides and resources to facilitate adoption.
00:17:12.640 Additionally, we offer support through Slack with a rotation of developers ready to answer questions in real-time. We aim to educate and empower the developer community to self-serve, allowing them to seek out solutions for their queries. Through these combined efforts, we hope to cultivate a better understanding of Solbier and gradual typing in Ruby across our teams. Now, I’ll hand the mic back to Alex to share some of the learnings we’ve gathered.
00:18:50.480 Thank you, Alex. After three years of gradual types adoption in our codebase, I want to share some key learnings. Before jumping into technical aspects, I first want to mention a methodology that was particularly effective for us: the lean methodology—the build, measure, learn loop. The tighter and quicker you can execute this, the more progress you can make in the right direction. We aimed to build something small to showcase to our internal developers early on.
00:19:24.960 We measured the impact and the effects of that, took those learnings, and based our decisions on what to build next. We recognized the importance of treating our internal developers as our customers, ensuring we built a product that would address their needs and not just a technical solution devoid of user consideration. On the technical side, we quickly learned that the longer files stayed marked as typed ignore, the more technical debt we incurred. Sorbet completely ignores these files, meaning any classes declared in these files can lead to errors when trying to reference them in other files.
00:20:37.440 While we initially relied on type ignore for many files, we realized this was counterproductive, prompting us to create tools to facilitate faster movement away from type ignore. Our recommendation is to aim for typed false on initial adoption, then progressively shift back from there until you achieve a state with minimal type ignored files. In our codebase of over 40,000 files, we have only a handful that are still marked as typed ignore, and those are typically either particularly complex or standalone cases.
00:21:36.240 It's also essential to keep the codebase up to date. We built various CI processes to ensure that everything remains synchronized. For example, we have a CI check that verifies that all the files are at the highest strictness level they should be, providing actionable feedback to developers. Additionally, we perform checks on DSL RBI files that decode the metaprogramming in our codebase, ensuring they remain synchronized with the actual code.
00:22:48.240 We've learned that patience is crucial. When we first surveyed internal developers in 2019, their impressions of Sorbet were less than favorable. However, by 2021, those impressions improved considerably; fewer developers reported difficulty using Sorbet within the Shopify core. This change can be attributed to our ongoing effort to enhance the tooling, provide ample documentation, and improve support.
00:23:40.320 Another key realization has been that it helps to use the right tools, which can be a significant accelerator. For instance, we beta-tested the Visual Studio Code extension for Sorbet that integrates with the Language Server Protocol. The ability to view Sorbet violations within the editor, alongside features like autocomplete and extensive refactoring capabilities, greatly improves the developer experience.
00:24:45.919 Over time, we've consistently received feedback indicating that these integration features improve productivity. The fact that Tapioca generates RBI files for gem dependencies further eases understanding and documentation, allowing developers to quickly reference what is exported from any gem without extensive searching.
00:25:48.159 We've also learned that just using the tools is insufficient; we need to actively contribute back to the tools and fill in the gaps. For instance, our team at Stripe had to contribute to Sorbet's understanding of Rails, particularly with ActiveSupport concerns, as this knowledge is pivotal for understanding our codebases. We also try to keep our codebase up to date with new Ruby versions, assisting the Sorbet team in adapting to the latest syntax as it's released.
00:26:51.440 Finally, one of the most important lessons is that we must avoid impeding developers' productivity. For adoption to succeed, we should implement a gentle ramp-up—gradually increasing strictness levels as developers become more comfortable with the tools. It’s entirely acceptable to start slow and progressively build towards higher strictness levels because if we disrupt developers’ work processes, they may resist the tools we've provided. So, go slow, be patient, and embrace the gradual nature of typing.
00:27:55.680 Thank you.
00:28:04.720 If we have time for questions, please feel free.
00:28:11.440 Audience Question: Can you speak to the friction experienced when moving from development to staging to production while implementing these changes? Were there any performance improvements or losses as a consequence?
00:29:16.880 Alex's Response: The transition from development to CI to production has been quite seamless with Sorbet. While it's true that there are performance implications due to background runtime type checking, for production environments, we decided to disable runtime type checking to prevent performance overhead.
00:29:59.360 We wanted to leverage runtime type checking in development and testing environments, as it helps catch wrong assumptions quickly.
00:30:05.600 Audience Question: What's your production stack—do you deploy on AWS or elsewhere?
00:30:18.720 Alex's Response: Our infrastructure is based on Google Cloud Platform. For more specifics about our stack, I suggest checking out our engineering blog as it contains extensive details.
00:30:50.000 Audience Question: I'm curious about editor integration. Are you aware of what editors are mostly used at Shopify, and how do you support them in utilizing Sorbet effectively?
00:31:13.600 Alex's Response: We primarily recommend using Visual Studio Code, as it is our focus for support. We are aware that some developers also use RubyMine, Vim, or other editors, and we're making strides to integrate with them as well.
00:31:25.440 For Visual Studio Code, we're currently beta testing a dedicated extension developed by Stripe, and we're committed to providing robust integration across editors.
00:31:44.720 If you have any further questions, feel free to reach out to us directly or join our Discord channel where we can answer real-time questions.
00:32:05.520 Thank you!
Explore all talks recorded at RubyConf 2021
+95