RubyKaigi 2015

Refinements - the Worst Feature You Ever Loved

http://rubykaigi.org/2015/presentations/nusco

Refinements are cool. They are the biggest new language feature in Ruby 2. They help you avoid some of Ruby's most dangerous pitfalls. They make your code cleaner and safer.

Oh, and some people really hate them.

Are Refinements the best idea since blocks and modules, or a terrible mistake? Decide for yourself. I'll tell you the good, the bad and the ugly about refinements. At the end of this speech, you'll understand the trade-offs of this controversial feature, and know what all the fuss is about.

RubyKaigi 2015

00:00:00.000 There are three types of problems. The first type is obvious problems, which are those that you know how to solve. That doesn't mean they're easy; for example, I can ask you to write an algorithm and prove it to me. You might say, "Okay, it’s going to take two weeks," and then you come back, and you've completed that task.
00:00:10.000 The second type consists of problems that might take an unspecified amount of time. You know you will eventually solve them, but you don’t know how long it could take or how much effort will be required.
00:00:20.000 The third type of problems are the ones that keep you awake at night—the problems that might not even have a solution. Maybe there is no ideal solution, and you might have to settle for something that is good enough. These problems can take years to resolve.
00:00:34.000 Today, we're going to talk about one such deep problem: refinements. But first, let’s see how many people here are currently using refinements in their code. It seems like just a few. That’s an interesting fact; refinements have been around for a long time, and they’ve been an official part of the Ruby language since version 2.
00:00:45.000 We’ve actually been discussing refinements for years before that. So, what is the problem that refinements want to solve? Why do we need refinements in the first place?
00:00:58.000 In Ruby, you can open a class and define methods within that class. If you've been coding in Ruby for even a few days, you’re likely aware of this feature. A common example would be reopening the String class to define a new method.
00:01:10.000 Now every string has this new method. Some people refer to this practice as monkey patching, which is a slightly ironic and somewhat negative term.
00:01:20.000 What is monkey patching good for? It’s important for several reasons. You can probably think of several use cases, but I want to discuss a few significant ones.
00:01:34.000 The first significant use case for monkey patching is in the context of domain-specific languages. For instance, this piece of code from an older version of RSpec uses monkey patching, where a method called `shoot` is defined, even though it is not native to numbers.
00:01:45.000 Somebody monkey patched that method in some ancestor class, potentially `Numeric` or even `Object`. But why do we do this instead of just using `assert_equal`? The answer is aesthetics; it's simply nicer.
00:01:58.000 In Ruby, we care about aesthetics, so we prefer to write code that looks clean and readable. Another example is convenience methods, which fall under the umbrella of domain-specific languages.
00:02:10.000 For example, instead of saying `minutes(20)`, you can simply say `20.minutes`. It’s again a case of preferring the nicer syntax.
00:02:22.000 The third use case, while not as common, is method wrappers, where you wish to wrap additional functionality around an existing method. For example, if I wanted to change the behavior of `length` in strings, I may not want to subclass. Instead, I redefine the method using monkey patching.
00:02:34.000 However, the moment you do that, your code may face issues because everyone else is relying on the old method functionality. This poses a significant risk since monkey patches are global, which brings chaos and confusion to your code.
00:02:49.000 Global state is considered evil in coding, and thus there is a strong desire for something similar to monkey patches that can restrict functionality locally. This is where refinements come into play.
00:03:02.000 Refinements allow you to create local versions of methods that do not affect the global state. To use refinements: first, define your refinement, and then apply it. Defining a refinement requires the creation of a module.
00:03:14.000 Modules serve many purposes in Ruby—they're used for mixins, as namespaces, and now to contain refinements. Once you have the module set up, you indicate the desired class or module you want to refine using the `refine` keyword.
00:03:27.000 You then write your refinement similar to how you would with monkey patching. To utilize the refinement, you employ another module as a namespace, ensuring that it’s distinct from the module that defines the refinement.
00:03:41.000 Inside this user module, you employ the `using` method to activate the refinements. This is key: the refinement is only active within the scope defined by the `using` statement.
00:03:53.000 You don't necessarily need a module, either. You can use a refinement directly at the top level of your code, and it will remain active until the end of the file.
00:04:03.000 Additionally, you can apply a refinement in a string and call `eval`. In this case, the refinement only remains active immediately after the `using` statement, until the end of that string.
00:04:14.000 This seems simple enough, but there’s a deeper complexity involved, especially when you consider what happens if you try to reopen the class after defining a refinement.
00:04:26.000 You might expect refinements to continue working, but they become tricky with things like inheritance and dynamic scoping, which brings about potential confusion in your code.
00:04:40.000 As we’re working with scopes, it’s important to understand that the unexpected behavior of refinements can introduce dangerous pitfalls, which requires careful consideration. Let’s think about this through some examples.
00:04:55.000 For instance, if you redefine a method but are also utilizing `class eval` to reopen the class’s scope, one might think that the refinement would still apply due to shared scope.
00:05:06.000 But that’s not the case. It’s important to remember that refinements are lexically scoped. They only work within the specific area they have been defined and cannot simply be assumed to work just because you have the right class in scope.
00:05:18.000 To further complicate matters, renaming methods with `super` in a refined method leads you to the unrefined version, which affects behavior unpredictably.
00:05:31.000 This becomes especially clear when you introduce the aspect of timing in your code, since refinements can drastically change how a method behaves depending on which refinements are active in the current scope.
00:05:43.000 In practice, this means each time you call a method, its behavior can change dynamically based on the refinements that are in effect. Ultimately, this can lead to a complicated web of meaning, making code difficult to read and understand.
00:06:00.000 Notably, the confusion can also create security risks. Malicious code could leverage these changing meanings to execute harmful tasks stealthily, manipulating which code gets executed without the programmer's awareness.
00:06:14.000 On the flip side, some developers argue that Ruby already allows for significant messiness, so introducing these changes in refinements may not fundamentally shift the balance.
00:06:28.000 However, the debate over refinements is contentious, with serious opinions on either side regarding their impact and usefulness in real-world Ruby code.
00:06:42.000 Moving on to performance, dynamically scoped refinements can indeed impact performance. The nuances of how method calls are resolved in Ruby's class hierarchy add layers that could introduce delays.
00:06:55.000 When a method is called on an object, it traverses up to find the corresponding method in its ancestors. This may seem trivial, but the time taken adds up, especially in larger class hierarchies.
00:07:09.000 Consider a common class like `ActiveRecord`, which can have nearly 70 ancestors. Every call could hit every class in this hierarchy until it finds the correct method.
00:07:23.000 Although Ruby employs caching to improve performance by not re-evaluating the method hierarchy on every call, activities like defining new methods or including new modules can invalidate the cache.
00:07:34.000 With refinements, this cache can be invalidated more frequently, which presents performance concerns that are magnified across all Ruby code.
00:07:47.000 Confusing behavior and unexpected results can stem from refinements leading you to assume certain resolutions of method calls that might not exist due to the dynamic scoping rules.
00:08:05.000 Lastly, I want to touch upon one particular corner case to illustrate just how perplexing refinements can be. If you define a refinement and try to use it in a REPL context like IRB, you may find it doesn’t work as expected.
00:08:15.000 This is because the `using` keyword is effective only within its lexical scope. In IRB, the environment continuously evaluates strings of code, and thus any refinement becomes inactive after the `using` call.
00:08:35.000 To recap, dynamically scoped refinements present both advantages and disadvantages. On one hand, they serve as a solution to monkey patching, allowing local rather than global alterations.
00:08:45.000 On the other hand, they can introduce confusion into your code, present security risks, and impact performance negatively. Debates around whether their benefits outweigh these drawbacks have been ongoing.
00:09:00.000 In practice, what we have today with refinements is that they still function similarly to earlier concepts. You define a refinement and use it, but if you attempt to modify lexical scopes in unpredictable ways, the expected functionality fails.
00:09:15.000 The result is that refinements become only active in the scope where those refinements are explicitly defined—if they seem to disappear, that’s because they were never there in the first place.
00:09:30.000 To summarize, when considering the three primary cases where refinements could excel, it’s essential to understand that they do not necessarily replace monkey patching unless implemented correctly.
00:09:40.000 For existing libraries or frameworks, continuing to use monkey patches may be more prudent, given that refinements still rely on additional effort to implement.
00:09:50.000 In terms of our previous examples of convenience methods and their integration into user-defined classes, refinements offer potential but require conscious maintenance and effort to leverage effectively.
00:10:05.000 Particularly with the emergence of new syntax and tools, we see that not every library or codebase is embracing refinements due to the complexities and potential drawbacks involved.
00:10:20.000 Ultimately, there are still exciting possibilities within Ruby, especially as people are willing to explore the intricacies of new features and functionalities.
00:10:35.000 I appreciate your time and attention today as we've navigated through the merits and pitfalls of refinements, as well as the depth of language design.
00:10:45.000 As we look toward the future, may we continue to experiment and uncover all the layers that make Ruby the unique and powerful language it is.