Talks
Why Is Nobody Using Refinements?
Summarized using AI

Why Is Nobody Using Refinements?

by James Adam

In the presentation "Why Is Nobody Using Refinements?" given by James Adam at RubyConf 2015, the speaker explores the underutilization of the Ruby refinements feature, which was introduced as a way to mitigate the problems associated with monkey patching. Key points covered include:

  • Understanding Refinements: Refinements allow controlled modifications to the behavior of objects by adding or redefining methods without affecting the global scope or other parts of the software. They are defined in modules using the 'refine' method, and activated within a lexical scope via the 'using' method.

  • Lexical Scope: The behavior of refinements is highly influenced by lexical scope, which determines where and how refinements are activated and available. A thorough understanding of lexical scope is critical to using refinements effectively. The speaker discusses how different scopes can lead to unexpected behavior, illustrating that methods retain the lexical scope present at their definition time.

  • Examples of Refinement Use Cases: Adam discusses various scenarios where refinements can be advantageous. These include:

    • Avoiding the dangers of monkey patching
    • Preserving original API behaviors amidst library updates
    • Enhancing Domain-Specific Languages (DSLs) without side effects
    • Implementing design patterns to confine method usage to specific parts of the codebase.
  • Challenges and Controversies: Despite their potential, refinements are not widely adopted due to misconceptions about their performance, usability, and the necessity of explicitly activating them in every file. Adam highlights the misconceptions surrounding their comparative slowness and warns against the simplistic categorization of refinements as 'bad'.

  • Community Perspectives: The speaker concludes by noting the importance of questioning established beliefs within the Ruby community regarding refinements, emphasizing that exploring the language’s features critically can lead to greater understanding and potential improvements in software development practices.

This exploration reveals the complexities of using refinements and highlights an ongoing need for Ruby developers to engage with these features more deeply, thus expanding their toolkit and considerations in coding.

00:00:11.990 Hello everyone. That's interesting. I just did a run-through and I hit 45 minutes exactly, so I'm just going to get started so that you can all have a nice coffee break after this.
00:00:21.300 Chances are that you've heard of refinements but never used them. The refinements feature has existed as part of Ruby for around five years, first as a patch and then subsequently as an official part of Ruby since version 2.0. Yet, to most of us, it exists only in the background, surrounded by a haze of opinions about how they work, how to use them, and indeed whether or not using them at all is a good idea.
00:00:35.430 I'd like to spend a little time looking at what refinements are, how to use them, and what they can actually do. But don’t get me wrong, this is not a sales pitch for refinements. I'm not going to try to convince you that you should be using refinements and that they're going to solve all your problems. The title of this presentation is 'Why Is Nobody Using Refinements?' and that's a genuine question. I don't have all the answers. My only goal is that by the end of this session, both you and I will have a better understanding of what they actually are, what they can do, when they might be useful, and why they've lingered in the background for so long.
00:01:14.220 Simply put, refinements are a mechanism to change the behavior of an object in a limited and controlled way. By 'change,' I mean add new methods or redefine existing methods. By 'limited and controlled,' I mean that by adding or changing those methods, it does not have an impact on other parts of our software that might interact with the same object. So, let's look at a very simple example.
00:01:46.920 Refinements are defined inside a module using the 'refine' method. This method accepts a class—in this case, a string—and a block, which contains all the methods to add to that class when the refinement is used. You can refine as many classes as you want within a module, and you can define as many methods as you want within each block. To use a refinement, we call the 'using' method with the name of the enclosing module, and when we do that, all instances of that class—which is a string in this case—within the same scope as the 'using' call will have the refined methods available. Another way of saying this is that the refinement has been activated within that scope. However, any strings outside the scope remain unaffected.
00:02:30.150 Refinements can also change methods that already exist. When the refinement is active, the refined method is used instead of the existing method. However, the original method is still available via the 'super' keyword, which can be very useful, and anywhere the refinement isn't active, the original method gets called exactly as before. That's really all there is to refinements: 'refine' and 'using.' However, there are some quirks, and if we want to properly understand refinements, we need to explore them a little bit more, and the best way to approach this is by considering a few more examples.
00:03:03.420 Now, we know that we can call the 'refine' method within a module to create refinements, and that is actually all relatively straightforward. But it turns out that when and where you call the 'using' method can have a profound effect on how the refinement behaves with our code. We've seen that invoking 'using' inside a class definition works; we activate the refinement, and we can call refined methods on string instances.
00:03:49.250 In this case, we can also move the call to 'using' outside the class and still use the 'refine' method as before. In the examples so far, we've been calling the 'refine' method directly, but we can also use them within methods defined in the class, and that works as well. However, we cannot call our 'shout' method on the string returned by our method, even though that string object was created within a class where the refinement was activated. Here's another broken example: we've activated the refinement inside our class, but when we reopen the class and try to use the refinement, we get a 'no method error.' If we nest the class within another, it seems to work, but it doesn't work in subclasses unless there are also nested classes.
00:04:37.290 Even though nested classes seem to work, if you try to define them using the double colon or the compact form, the refinements will have disappeared again. And even blocks seem to act a little bit strangely. Our class uses the refinement, but when we pass a block to the method in that class, it suddenly breaks; the refinement has disappeared. For many of us, especially those relatively new to Ruby, this is going to be quite counterintuitive. After all, we're used to being able to reopen classes or share behavior between super and subclasses, but it seems like that only works intermittently with refinements. It turns out that the key to understanding how and when refinements are available relies on another aspect of how Ruby works, which you may have already heard of and possibly even encountered directly.
00:05:38.370 The key to understanding refinements is understanding lexical scope. To understand this, we need to learn about some of the things that happen when Ruby parses our program. First, as Ruby parses the program, it is constantly tracking a handful of things to understand what the meaning of the program is. Exploring all of these in detail would take far more time than I have, but for the moment, the one that we're interested in is called the current lexical scope.
00:06:26.640 So let's pretend to be Ruby as we walk through the code and see what happens when it starts parsing the file. It creates a new structure in memory—a new lexical scope—which holds various bits of information that Ruby uses to track what's happening at that point. When we start processing, we create this initial one; we call that the top-level lexical scope. When we encounter a class definition or a module definition, Ruby creates a new lexical scope nested inside the current one. We can call this lexical scope 'A' to give it an easy label; it doesn't actually have a name. Visually, it makes sense to show them as nested, but behind the scenes, the relationship is modeled by each scope linking to its parent. So, 'A's parent is the top-level scope, and the top-level scope has no parent.
00:07:53.050 As Ruby processes all the code within this class definition, the current scope is now lexical scope 'A'. When we call 'using', it stores a reference to the refinement within the current lexical scope. Within lexical scope 'A', the refinement has been activated. You can now see that there are no activated refinements in the top-level scope, but our 'shout' refinement is activated for lexical scope 'A'. Next, we can see that the call to the method 'show' on a string instance will reference this activated refinement.
00:08:46.480 When Ruby dispatches for the 'show' method, it checks the current lexical scope for the presence of any active refinements. In this case, there is an activated refinement for the 'shout' method on strings, which is exactly what we're calling. So Ruby then looks up the correct method body within the refinement rather than the class and invokes that instead of any existing method. Therefore, we can see our refinement working as we hoped.
00:09:32.210 What about when we try to call the method later? Once we leave the class definition, the current lexical scope becomes the top-level scope again. When we find our second string instance with a method being called on it, once again, Ruby checks the current lexical scope for the presence of any refinements. In this case, there are none, so Ruby behaves as normal, which is to invoke 'method_missing', raising an exception. That's why we get a 'no method error.' If we called 'using' for 'shouting' outside of the class at the top of the file or something like that, we can see that our refined method works both inside and outside the class. This is because we're activating the refinement for the top-level lexical scope, and once the refinement is activated for the top-level scope, it's activated for all nested lexical scopes.
00:10:27.190 So, a call to 'using' at the top of the file means that it will work everywhere in that file, and our call to the refined method in the class works just as it did at the top of the file. So this is our first principle about how refinements work: when we activate a refinement with the 'using' method, that refinement is active in the current and any nested lexical scopes.
00:11:30.390 However, once we leave that scope, the refinement is no longer activated, and Ruby behaves just like it did before. Let's look at another example from earlier. Here, we define a class, activate the refinement, and later, either in the same file or a different file, we reopen the class and try to use it. We've already seen that this doesn't work, so the question is why? In watching Ruby build its lexical scopes, we can reveal the explanation.
00:12:26.430 Again, we have our top-level lexical scope. When we encounter the first class definition, Ruby creates a new nested lexical scope, which I’ll call 'A’. Within this scope, we activate the refinements. Once we reach the end of the class definition, we return to the top-level lexical scope. However, when we reopen the class, Ruby creates a nested lexical scope that is distinct from the previous one, which I'll call 'B’.
00:13:18.040 While the refinement is activated in the first lexical scope, when we reopen the class, we’re in a new lexical scope that is distinct, and the refinements are no longer active. The second principle then is that just because the class is the same doesn't mean you're back in the same lexical scope. This also explains why our example for subclasses didn't behave as expected.
00:14:04.030 We can just draw these scopes. It should be clear now that the fact that we're in a subclass has no bearing on whether the refinement is active; it's entirely determined by lexical scope. Anytime Ruby encounters a class or module definition via the 'class' or 'module' keywords, it creates a new, fresh lexical scope, even if that class or module has already been defined somewhere else.
00:14:50.850 This is also the reason why, even when activated at the top level of a file, refinements only stay activated until the end of that file. Each file is also processed using a new top-level lexical scope. So now we have two principles about how lexical scope and refinements work. Just as reopened classes have a different scope, so do subclasses; in fact, the class hierarchy has nothing to do with the lexical scope hierarchy. We also know that every file is processed within a new top-level scope, so refinements activated in one file are not activated in any other files unless those other files also explicitly activate that refinement.
00:15:44.070 Let's look at one more example. Here we are activating a refinement within a class and then defining a method in that class which uses the refinement. Later, we create an instance of the class and call that method. Even though the method gets invoked from our top-level lexical scope—which is where the call to 'greet' occurs—where the refinement is not activated, the refinement still somehow works, and the behavior is what we hoped.
00:16:36.400 So, what's going on here? Well, when Ruby processes a method definition, it stores a reference to the current lexical scope at the point the method was defined. When Ruby processes the 'greet' method definition, it stores a reference to lexical scope 'A' along with it. Thus, when we call the 'greet' method from anywhere—even from a different lexical scope—Ruby evaluates it using the lexical scope that it has associated with it. So when Ruby tries to evaluate 'hello ciao' inside our greet method and dispatches the 'shout' method, it's checking for activated refinements in lexical scope 'A', even though the method was called from an entirely different lexical scope.
00:17:27.190 We already know that our refinement is active in scope 'A', and so Ruby can use the method body for 'shout' from the refinement, and it works just like we hoped. A fourth principle is that methods are evaluated using the lexical scope at their definition, no matter where those methods are actually called from.
00:18:07.600 Now, one more example, I promise—just one more. A very similar process explains why blocks don't work. Here's that example again where a method is defined in a class where the refinement is activated that yields to a block. When we call that method with a block that uses the refinement, we encounter an error. We can quickly see which lexical scopes Ruby has created as it processes this code. As before, we have a nested lexical scope, which we'll call 'A', and the method defined in our class is associated with it.
00:18:57.440 Scope 'A' has the refinement active; however, just as methods are associated with the current lexical scope, so are blocks and procs and lambdas. When we define the block, the current lexical scope is the top-level one. So, when the run method yields to the block, the block will be evaluated within the context of the top-level lexical scope, and thus Ruby’s method dispatch algorithm finds no active refinements, and therefore no 'shout' method.
00:19:52.920 Finally, blocks and lambdas and so on are evaluated using the lexical scope of their definition too. With a bit of experimentation, we can demonstrate that even blocks evaluated using tricks like 'instance_eval', 'define_method', or anything else retain this link to their original lexical scope. Even if the value of 'self' might change depending on how you pass them around, this link for methods and blocks to a specific lexical scope might seem strange or even confusing right now, but it’s precisely because of this that refinements are so safe to use.
00:20:52.330 Let’s recap what we know: refinements are controlled entirely using lexical scope structures that are already present in Ruby. You get a new lexical scope anytime you do any of the following: entering a different file, opening a class or module definition, or running code from a string using 'eval'. You might find the principle of lexical scope surprising if you've never thought about it before, but it's actually a very useful property for a language because without it, lots of the things we take for granted in Ruby would be much harder, if not impossible.
00:21:54.430 Lexical scope is actually part of how Ruby determines which constant you mean and is fundamental to using blocks and procs—closures, for example. We also have our five basic principles that enable us to explain how and why refinements behave the way they do once you call 'using': refinements are activated for the current and any nested lexical scopes. The nested scope hierarchy is entirely distinct from any class hierarchy in your code. Superclasses and subclasses have no impact on refinements at all—only nested lexical scopes.
00:22:55.490 Two different files get different top-level scopes. So even if we call 'using' at the very top of a file and activate the refinement for all code within that file, the meaning of code in all other files remains unchanged. Methods are evaluated using the current lexical scope at the point of definition, meaning we can call methods that make use of refinements internally from anywhere in the rest of our codebase. Finally, blocks are also evaluated using the lexical scope, thus making it impossible for refinements activated elsewhere in our code to change the behavior of blocks, or indeed other methods, or any other code written where that refinement wasn’t activated.
00:23:44.870 Now, you basically know everything there is to know about refinements, but what good are they? Anything? Nothing? Let's try and find out. Again, I must add a disclaimer: these are just some ideas—some are more controversial than others—but hopefully, they will help frame what refinements might be good for, what they might make more elegant or more robust.
00:24:44.290 The first one is probably not going to be a surprise, but I think it's worth discussing anyway. Monkey patching refers to the act of modifying a class or object that we don't own or didn't write. Because Ruby has open classes, it's trivial to redefine any method on any class with new or different behavior. The danger of monkey patching is that those changes are global; they affect every part of the system as it runs. As a result, it can be very hard to tell which parts of our software will be affected if we change the behavior of an existing method. For instance, there's a good chance that some distant part of the codebase, hidden somewhere in Rails, is going to call a method expecting the original behavior—or even worse, its own monkey patch behavior—and things are going to get messy.
00:25:46.950 Let's say I'm writing some code at a jam, and as part of that, I want to be able to turn an underscore in a string into a camelized version. I might think that the easiest thing to do would be to reopen the string class and just add this method. Although it's innocent-looking and simple, it can cause issues. As soon as anyone tries to use my gem—even myself, in a Rails application—the test suite could explode entirely. The errors may be cryptic and not related to the intended functionality, making debugging challenging.
00:26:56.890 This is exactly the problem that Yehuda Katz identified with monkey patching in his blog post about refinements almost five years ago. Monkey patching has two fundamental issues: the first is breaking API expectations. For instance, Rails has certain expectations about the behavior of the camelize method on strings—we obviously broke that when we added our own monkey patch. The second issue is that monkey patching can make it harder to understand what might be causing unexpected behavior in our software. Using refinements in Ruby addresses both these issues. If we change the behavior of a class using a refinement, we know that it cannot affect parts of the software that we don't control, because refinements are restricted to lexical scope.
00:28:02.830 If I wanted to use a version of camelizing in my gem, I could define a new version via a refinement, but anywhere that refinement wasn’t specifically activated—like in any Rails code—the original behavior remains. It is virtually impossible to break existing software using refinements. There's no way to influence the lexical scope associated with code without editing that code itself. As a result, the only way we can introduce refinement behavior into a gem is by literally finding that gem's source code and updating it.
00:28:59.040 Refinements also make it easier to understand when unexpected behavior may be coming from. They require an explicit call to 'using'. When we are in the same file as the code that employs that behavior, there’s a clear indication. If there's no call to 'using' in a file, we can comfortably assume that there are no refinements active and that Ruby should behave as expected. That’s not to say that it's impossible to create convoluted code, which can still be tricky to trace and debug in Ruby—this will always be possible. However, if we use refinements, there will always be a visual clue indicating that the refinement is activated.
00:30:08.790 Now, onto my second example: sometimes software we depend on changes its behavior over time as new versions are released. APIs can change in newer library versions, and even the language itself can evolve. For example, in Ruby 2, the behavior of the 'chars' method on strings changed from returning an enumerator to returning an array of single character strings. Imagine migrating an application from Ruby 1.9 to Ruby 2 or later, and discovering that some part of that application relies on the old chars behavior.
00:31:01.770 If some parts of our software still rely on the original behavior, we can use refinements to preserve the original API without impacting any other code that may have adapted to the new API. A simple refinement can be activated only for the code that depends on that Ruby 1.9 behavior, while the rest of the system remains unaffected, along with any new dependencies we bring in afterward.
00:31:57.500 A third example should be familiar: one of Ruby's major strengths is its flexibility, which helps us write very expressive code. That's a primary reason I was drawn to Ruby. In particular, it supports the creation of Domain-Specific Languages (DSLs). DSLs are collections of objects and methods designed to express concepts closely to the terminology non-developers might use. These languages strive to read more like human language than code.
00:32:44.040 Adding methods to core classes can help make DSLs more readable and expressive; thus, refinements are a natural candidate for this purpose, ensuring that those methods do not leak into other parts of the application. RSpec is a great example of a DSL—testing. Until recently, RSpec emphasized writing code that reads fluidly. For instance, we can see a valid Ruby line that reads 'developer should be happy,' which feels more like English than code. To enable this, RSpec used monkey patching to add a 'should' method to all objects.
00:33:36.070 Recently, RSpec moved away from this DSL. While I cannot speak for the developers maintaining RSpec, I am confident that part of the reason was to avoid monkey patching the Object class. However, refinements offer a compromise that balances the readability of the original API with the integrity of our objects. It's easy to add a 'should' method to all objects in your RSpec files using a refinement without leaking that method into the rest of the codebase. However, the compromise is that you have to write 'using RSpec' at the top of every file, which isn't a large price to pay.
00:34:23.010 RSpec isn't the only DSL that is commonly used, and you might not even view it as a DSL. You can also consider the routes file of a Rails application as a DSL of sorts, or even the query methods of Active Record. In fact, the Sequel gem provides a mechanism to write queries more fluently by adding methods to strings and symbols, and a few other classes using refinements without affecting the rest of your codebase. DSLs are prevalent, and refinements can make them more expressive without resorting to monkey patching or other brittle techniques.
00:35:20.420 Lastly, refinements might not just be useful for monkey patching or implementing DSLs; we could harness refinements as a kind of design pattern to ensure that certain methods are only callable from specific, potentially restricted parts of our codebase. For instance, consider a Rails application with a model that has a dangerous or expensive method. By using a refinement, we can ensure that the only places that can call this method are where we've explicitly activated that refinement.
00:36:34.290 Thus, from everywhere else—all other normal controllers, views, or other classes—even if they handle the same object or the same instance of that object, the dangerous or expensive method is guaranteed not to be available. I find this a really interesting and useful proposition: using refinements as a sort of design pattern rather than as a means of monkey patching. While there could be some objections to that suggestion—and I have some myself—I'm certainly curious to explore it further and see if it's worthwhile.
00:37:32.290 Those are some examples of things we might be able to do with refinements that I think are potentially interesting and useful. Now, finally, to the question I’m curious about: if refinements can accomplish all of these things in such an elegant, safe way, why aren’t we seeing more use of them? It’s been five years since they appeared, and almost three years since they were officially part of Ruby. Yet, when I searched GitHub, almost none of the results are actual uses of refinements. In fact, some of the top hits are gems that try to remove refinements from Ruby.
00:38:33.240 You can see in the description that nobody knows what problem they solve or how they work. So, hopefully, over the last 25 minutes, I may have addressed some of that. I asked another speaker from this conference, who will remain nameless, what they thought the reason might be, and they said, 'because they're just bad,' as if it were a fact. My initial reaction to this kind of answer is somewhat emotionally charged, but my actual answer is more like, 'Are they? Why do you think that?' I don’t find this answer very satisfying. Why are they bad?
00:39:46.400 I asked them, 'What do you mean?' and they replied, 'Because they’re just another form of monkey patching.' Well, yes, sort of, but also not really. Just because they might be related in some way to monkey patching does that automatically make them bad or not worth understanding? I can’t shake the feeling that this is the same mode of thinking that leads us to ideas like code that’s too magical or using single or double-quoted strings consistently being very important. Everything that you type into a text editor can be described as 'awesome,' when that term should really be reserved for moments in your life like seeing the Grand Canyon for the first time, not when installing the latest gem.
00:40:43.350 I have my suspicions about 'awesome,' and so I’m also suspicious of 'bad.' I asked another friend if they had any ideas about why people weren’t using refinements, and they said, 'Because they’re slow,' again asserting it as fact. If that were true, that would be a totally legitimate reason not to use them, but it’s not. If you look at a recent blog post, someone's done some nice benchmarking, and it shows almost no difference in the amount of time it takes to dispatch refined method calls.
00:41:39.460 So why aren’t people using refinements? Why do people form these ideas that they’re slow or bad? Is there any actual basis for those opinions? I told you at the start that I don’t have a neatly packaged answer, and maybe nobody does. When I proposed this talk, it was genuinely a question. I didn't know much about refinements and wanted to find answers.
00:42:32.340 So here are my best guesses based on tangible evidence and the understanding we now have about how refinements actually work. While refinements have been around for five years, the refinements you see now are not the same as those introduced half a decade ago. Originally, they weren’t strictly lexically scoped. While this permits more elegant code—like not having to write 'using RSpec' at the top of every file—it also breaks the guarantee that refinements cannot affect distant parts of the codebase.
00:43:22.460 It’s also probably true that the concept of lexical scope is not familiar to most Ruby developers. I’m not ashamed to say that I’ve been using Ruby for over 13 years now, and it’s only recently that I really understood what lexical scope meant. You can probably make a lot of money writing Rails applications without really caring about lexical scope at all. Yet, without understanding it, refinements will always seem like confusing and uncontrollable magic.
00:44:21.500 The promotion of refinements hasn’t been smooth, which may indicate why some people feel like nobody knows how they work or what problem they solve. Many blog posts you find when you search for refinements in Ruby now are outdated, and even the official Ruby documentation is wrong; this has been the case since Ruby 2.1.
00:45:14.910 This is a nudge to any Ruby core team members: issue one 1681 might fix that. I think some of this misinformation explains why refinements have stayed in the background. There were genuine and valid questions about early implementation and design choices, and those concerns may have diminished the momentum of the new feature when it was unveiled.
00:46:04.350 But even with all the outdated blog posts, I don’t think this entirely explains why no one seems to be using them. Perhaps people dislike the current implementation. Maybe the idea of having to write 'using' in every file goes against the 'DRY' principle—don't repeat yourself—that we've adopted as a community.
00:47:11.760 After all, who actually wants to remember to have to write 'using RSpec' or 'using Sequel,' or 'using ActiveSupport' at the top of every file? That doesn't sound fun. This points to another potential reason: a huge number of Ruby developers spend most, if not all, of their time using Rails. Rails wields significant influence over which language features gain prominence and are adopted by the community.
00:48:03.010 Rails contains perhaps the largest collection of monkey patches ever, thanks to ActiveSupport, but because it doesn't utilize refinements, no signals are sent to developers indicating that they should or even could be using them. You might be starting to assume that I don’t like Rails. Let me clarify: I love Rails. Rails nurtures and enables me to connect with all the wonderful developers who contribute to it.
00:48:50.440 However, I think it's very feasible that there’s no way for Rails to be using refinements in something like ActiveSupport at such a large scale. Further, nothing in the Ruby standard library utilizes refinements. There’s no call to 'refine' anywhere in the Ruby standard library. New language features like keyword arguments and refinements won't see widespread adoption until Rails and the Ruby standard library start promoting them.
00:49:25.160 Rails 5 has adopted keyword arguments, and I expect to see them spread to other libraries as a result. But without compelling examples of refinements in the libraries and frameworks we use every day, there’s little direction for us to really understand when they're appropriate or not.
00:50:06.330 As noted, there are a few quirks with refinements that can lead to unexpected outcomes, which could be another explanation for why they're not in widespread use. For instance, even when a refinement is activated, you can't call methods like 'send' or 'respond_to?' to check if those refinements are active. Additionally, you can't utilize them in forms like 'to_proc' and can fall into strange situations when trying to include a module into a refinement where methods from that module cannot call other methods defined in the same module.
00:51:03.290 These situations don’t automatically mean that refinements are broken; they’re either by design or direct consequences of lexical scoping. Even so, these unintuitive aspects could limit the ability to use refinements at the scale of something like ActiveSupport.
00:52:06.220 But as easy as it is for me to stand up here and argue logically and rationally why monkey patching is bad, it’s impossible to deny that software written using libraries relying heavily on monkey patching has made millions of dollars.
00:53:05.050 So, perhaps refinements solve a problem that nobody actually has. For all the potential issues that monkey patching might bring, the solutions we have for managing those issues are good enough—tools like test suites. And even if you disagree with that—something for which I wouldn’t blame you—perhaps it suggests a more compelling reason that refinements might not be ideal: they may not be the right solution for the problem of monkey patching.
00:54:14.960 Perhaps the better solution is object-oriented design. The Ruby community has become much more interested in object-oriented design over the last few years, as illustrated in presentations by Sandy Metz and her book, alongside discussions of patterns like hexagonal architecture.
00:55:20.370 The benefits that object-oriented design offers in terms of software development are significant, resulting in smaller objects with cleaner responsibilities that are easier and faster to test and change. All of this helps us do our jobs more effectively, and anything that aids in that pursuit must have value.
00:56:02.610 From our preferred perspective today, there's nothing you can do with refinements that you cannot achieve by introducing a new class or a new method encapsulating new or altered behavior. For instance, rather than adding a 'shout' method to all strings, we can create a new class that knows about shouting and wrap any strings we want shouted within instances of this new class.
00:57:21.440 I don’t want to discuss whether this approach is better than the refinement version because it’s obviously trivial and not realistic. What’s more interesting is that while sound object-oriented design delivers a lot of tangible benefits, the cost of proper design can be a burden. Just as a DSL tries to hide the active programming behind language that appears natural, creating many objects can complicate grasping what the code overall aims to achieve.
00:58:25.720 Finding the right balance between explicitness and expressiveness differs among teams and projects. Furthermore, not every user of software is a developer, let alone someone trained in software design. Consequently, we cannot expect everyone to easily adopt sophisticated principles.
00:59:38.140 Software is for its users, and sometimes making them deal with objects or methods may not necessarily be worth the design purity. This brings us to the scrum of reasons I've provided for why nobody seems to be using refinements. Which is the right answer? I don’t know. There may be no way to know.
01:00:34.320 All of these reasons are potentially valid and defensible explanations for why we might collectively ignore refinements or even consider removing them from Ruby entirely. However, I suspect that the answer lies closer to the notion introduced at the beginning of our journey: because other people have told us that they are bad.
01:01:34.880 Let me make a confession: when I said this is not a sales pitch for refinements, I truly meant it. I’m fully open to the idea that it may never be a good notion to use them. While I doubt it, it’s possible. But what truly matters to me is that we start to accept and internalize opinions, like 'this feature is bad' or 'this is horrible,' without pausing to question them or investigating the feature ourselves.
01:02:43.320 Nobody has the time to investigate everything, and it would not only be unrealistic, but one of the advantages of being part of a community is that we benefit from each other's experiences. We can use our collective knowledge to learn and grow, which is a positive aspect. Nonetheless, if we blindly accept opinions as fact without asking why, it becomes a bit more dangerous. If nobody ever questioned established beliefs, we’d still think the world was flat.
01:03:29.460 It’s only by questioning opinions that we make new discoveries, learn for ourselves, and push the community forward. The oversimplification of the 'good or bad' binary may be tempting, but it's an illusion; nothing is ever that straightforward.
01:04:20.560 To close, there’s a quote from British journalist and physician Ben Goldacre that he uses whenever someone presents something as strictly good or bad. He states, 'I think you'll find it's a little bit more complicated than that.' That's how I feel when anyone tells me something sucks or is awesome. It may suck for you; however, unless you explain why, how can I ascertain how your experience may apply to mine? The issues with one person can easily appear distinct to another, and they are not mutually exclusive.
01:05:14.920 It’s our responsibility to listen and read critically, and then explore for ourselves what we think is best. I believe this holds particularly true in software development. If we delegate most, if not all, of the exploration of new features to the relatively small selection of individuals who speak at conferences, maintain popular blogs, or who tweet significantly, those perspectives represent only a limited viewpoint compared to the vast community of Ruby developers.
01:06:07.320 We have a responsibility to ourselves and each other—not to limit our use of Ruby to the ways that are implicitly or explicitly promoted to us—but to explore the frontiers, wrestle with new and experimental ideas, features, and techniques. There are many different perspectives to inform the question of whether or not a feature is good.
01:06:57.040 As a pun, there are no constants in programming. The opinions about Rails, refinements, and even great benefits will shift over time. Furthermore, the principles of design are only principles and do not represent laws we need to follow blindly for eternity. There will always be alternatives to consider. Change is inevitable.
01:07:47.270 At last, we’ve concluded. I might not be able to tell you precisely why so few people seem to be using refinements, but I have one small request: please carve out a little time to explore Ruby. Perhaps you will uncover something simple or even something wonderful. If you do, please share it with everyone. Thank you very much.
01:08:40.210 Does anyone have any questions?
01:08:48.130 I'm not really sure what the question was, but I have the question: what was the history of refinements?
01:09:11.680 They were inspired by a concept called class boxing from a different language, which effectively does something similar. Originally proposed as a patch at RubyConf 2010—literally ten years ago, which is coincidentally about the same time refinements started to surface. The motivation was to resolve monkey patching issues, likely because Rails was gaining popularity, and as people encountered monkey patching problems, the need for a solution became evident.
01:09:52.380 This refinement history is interesting but would take a while to read through; there are 278 comments on the Ruby tracker issue that introduced it over the course of two years. The question was about refactoring.
01:10:36.850 That might be a bit confusing, but I think they are separate. You're not really extracting a method from anywhere; it's not like an object loses a responsibility or something else.
01:10:49.580 Okay, I think that's the time up now. Thanks very much for your time.
Explore all talks recorded at RubyConf 2015
+75