Design Patterns

Summarized using AI

Teaching Ruby to Count

Joël Quenneville • February 28, 2023 • Providence, RI

The video "Teaching Ruby to Count" presented by Joël Quenneville at RubyConf 2022 focuses on the iterative capabilities of Ruby, emphasizing its strengths and the delight it brings to developers when writing code. The talk centers around several key themes tied to counting, looping, and working with series of data within Ruby, contrasting it with approaches in other languages such as PHP.

Key Points Discussed:

  • Looping Basics: Joël begins by highlighting how standard loops, particularly the for loop in languages like PHP, can lead to common mistakes such as off-by-one errors. These arise from manual indexing and conditional checks that are prone to programmer error.

  • Ruby's Iteration Methods: In Ruby, tasks like iterating a specific number of times or going through an array are simplified using methods such as 3.times and array.each. This functionality significantly reduces opportunities for bugs and contributes to the overall design philosophy of Ruby aimed at programmer delight.

  • Use of Ranges: The presentation discusses Ruby's range feature for creating concise loops and handling series effectively. Joël provides an example of generating values over a defined range, showcasing Ruby's elegance in syntax.

  • Creating Custom Objects: One of the more complex tasks discussed involves developing a series of reports based on months. Joël describes creating a custom month object to accurately model dates within a series rather than forcing date objects to fit the monthly requirements. This illustrates the principle of using appropriate building blocks for the task at hand.

  • Implementing Enumerable: To make custom objects iterable, the presentation details how to implement Ruby’s Enumerable module by defining necessary methods (<, >=, succ) to allow comparison and progression through custom collections. This leads to making custom month objects usable within ranges, demonstrating how Ruby can be extended seamlessly.

  • The Power of Enumerable Methods: Joël explores various convenient methods available through the Enumerable module, allowing easy manipulation and traversal of collections. These methods enhance the developer's ability to write cleaner, more understandable code.

  • Understanding Enumerators: The final segments introduce the concept of enumerators, elucidating their role in Ruby. He provides mental models for enumerators as cursors, series generators, and lazy lists, helping the audience grasp how Ruby efficiently handles iteration across potentially infinite collections.

Conclusion:

Joël concludes that the joy in programming with Ruby arises from its built-in tools that minimize complexity while maximizing expressiveness. By integrating these tools, developers can produce delightful code effortlessly. Overall, the talk not only showcases practical coding techniques but also emphasizes a philosophy of utility and pleasure in programming that Ruby nurtures.

Takeaways:

  • Ruby significantly simplifies iteration tasks, reducing common errors associated with manual looping in other languages.
  • Custom objects enhance flexibility when dealing with non-standard data types, allowing for tailored solutions that maintain code readability.
  • The Enumerable module offers powerful methods, which combined with understanding enumerators, provide Ruby programmers with tools to write elegant and effective code efficiently.

Teaching Ruby to Count
Joël Quenneville • February 28, 2023 • Providence, RI

Ruby has some of the best tooling in the business for working with iteration and data series. By leveraging its full power, you can build delightful interfaces for your objects.

In this case-study based presentation, we’ll explore a variety of problems such as composing Enumerable methods, generating a series of custom objects, and how to provide a clean interface for a collection with multiple valid traversal orders. Beyond just the beloved Enumerable module, this talk will take you into the world of Enumerators and Ranges and equip you to write objects that bring joy to your teammates.

RubyConf 2022 Mini

00:00:12.240 Welcome, everybody. We're here to talk about teaching Ruby to count. Counting, looping, and working with series are core parts of building software. During this talk, I'm going to collectively refer to these concepts under the general idea of counting. However, there are two basic actions that we perform in software: we loop and we branch.
00:00:22.500 If you were at the coffee machine talk earlier this morning, they delved deeply into how to work nicely with conditionals. This afternoon, we're going to focus on working with loops.
00:00:50.640 The traditional way to count in programming languages is by using a for loop. When I was a very young programmer, I learned to code using PHP. If Andy is in the room, you might want to look away; the next few slides are not for you. But, when I was first learning to code, this is how I was taught to repeat tasks.
00:01:18.600 For instance, if I wanted to do something three times in PHP, I would use a for loop. Here, I initialize a variable, 'i', to be 0 and then keep looping while 'i' is less than three. At the end of each loop, I would increment 'i' by one.
00:01:39.900 There are a few ways this could go wrong. For example, if I initialize 'i' to the wrong value or if I don't perform the right check, such as checking if 'i' is less than or equal to 3 rather than just less than, I could easily encounter an off-by-one error. Let me tell you, Junior Programmer Joël made that mistake multiple times.
00:02:02.159 Another common task in programming is to iterate over an array. This is how one might do this in PHP. It's similar to before, but now we have another variable—an array. As we loop through it, we use our counter variable 'i' to index into the array. While this works, it is again an easy place for a junior programmer to make mistakes.
00:02:25.980 Indexing into arrays can lead to off-by-one errors because array indexing starts at zero, though we often think of counting starting from one. I’ve made that mistake too. Here’s something a bit more complicated: trying to filter an array. This is similar to our earlier code where we loop over the array, but now we have a third variable, the result array. We append values that are divisible by two to this result.
00:02:43.920 There are a lot of mechanics involved here, a lot of exposed variables, and it's not the easiest code to read. There’s definitely a lot of room for making mistakes. Sometime later, I got into another language called Ruby. Some of you may have heard of it.
00:03:16.019 Here’s how Ruby handles these tasks: when you want to iterate three times, you simply use '3.times' followed by a block and do the task you want to accomplish. For iterating over an array, you would use 'array.each', which gives you access to each item directly, allowing you to work with it without needing counters.
00:03:44.099 Notice that there are no counters or indexing into arrays here—the opportunities for mistakes are greatly reduced. For Junior Programmer Joël, this was delightful. The reason it's so delightful is because building delight is core to Ruby's design. It's made to be a language that is pleasant for programmers to use.
00:04:03.600 As a Ruby community, we care about delight for our users, our colleagues, and ourselves. We aim to write delightful code. One of the reasons Ruby is so enjoyable for iteration is that Ruby knows how to count—it has built-in methods for iteration. Its integers know how to do something a number of times; it's baked into the language.
00:04:48.780 Beyond just arrays and numbers, Ruby has some really cool features for counting. Let's start with ranges. A range looks like this: you can read this as the range from 5 up to 7. A range can be thought of as a series, and sometimes we want a series of data to generate some values for our use.
00:05:13.920 Another common use for series is as a plan for iteration. We have a certain series, then we want to iterate through it, controlling how we handle iteration. Ruby shines in this aspect as well. Here's how to iterate an arbitrary range: use '5..7.each' followed by a block to output the numbers five, six, and seven.
00:05:51.060 Now, let’s look at a more complex task: building a series of reports. Imagine we need to create a report for every month from March to September of 2022. We might begin by creating date instances for March and September, placing them in a range, and then trying to iterate over them. However, this attempts to iterate over every day, not the months themselves.
00:06:22.560 In this case, we need to filter down the set first because our initial series isn’t what we wanted. So, we will select only the first days of each month and then iterate over those, resulting in March 1st, April 1st, May 1st, and so on. While this works depending on our task, it diminishes some of the delight.
00:06:49.740 The core problem here is that date objects represent days, not months. Often, when a problem feels clunky to work with, it’s because your underlying building blocks don’t map well to the work you're trying to do. Instead of forcing a month problem into a date-sized container, we could introduce a custom object.
00:07:15.600 By creating a custom month object as a wrapper around a simple counter for the number of months since an arbitrary point, which might be year zero or something else, we can model the dates more accurately. We don't have time to dive into why that's significant right now, but if you're interested in discussing this further, please talk to me afterwards.
00:07:41.880 We care about delight for our users, so we don't want them to create objects with bizarre numeric values like 55 months. Therefore, we implement a constructor that allows you to provide a year and month values, which will take care of the underlying counter calculations for you.
00:08:03.600 Now, we can again build our range with our custom month objects for March and September, thus trying to put them in a range. However, we hit an error: 'ArgumentError: bad value for range.' The problem is that custom objects can't be used in a range by default, but not all hope is lost.
00:08:27.600 We just need to teach Ruby how to count with our custom objects. To be used in a range, two requirements must be met: first, your object has to implement the '<,' '>=', or '>' operators, often referred to as the spaceship operator; this allows the comparison of two objects.
00:08:51.560 Secondly, you need to implement the 'succ' method, which will return the next object in the series. Here's how we might do that. One of the advantages of our implementation is that we can delegate these two methods to our internal counter.
00:09:15.640 For comparison, we will compare our internal month counter to another custom month object’s counter. When we want to get the successor, we simply retrieve the successor for our internal counter and create a new month object for it. With these two methods in place, our previous code will function properly.
00:09:40.160 Now, we can place our custom month objects into a range! Would you look at that—March 2022, April 2022, May 2022, and so on. We've successfully created custom objects that behave just as well as the built-in ones. We're back to writing delightful code!
00:10:04.199 We’ve seen arrays and ranges, but Ruby has additional classes with handy convenience methods like each, map, reduce, select, find, and many more. These all stem from a module called Enumerable that you may be familiar with. Now, I want to tell you a story.
00:10:32.430 One of my favorite novels is 'The Count of Monte Cristo.' It's a tale of betrayal, revenge, and intrigue, but it also explores deeper themes of human nature, fate, and free will. However, I must warn you: this is a long book. Very long.
00:10:54.060 So, if we were to write some code to deal with this book, we might want to structure each chapter as a separate file for performance optimization. While this works nicely, it becomes cumbersome for those who want to interact with the book as a single collection, not as multiple files scattered across a server.
00:11:25.920 Thus, when your building blocks limit easy code writing, creating a custom object can help. We might create something called a composite file that sits over multiple chapter files, pretending to be one file while operating with multiple files under the hood.
00:11:59.160 If we create a composite file with all the chapters, we would like to find the first line that refers to Villifor, one of the villains in the story. Unfortunately, we encounter another exception: 'undefined method find for composite file.' This issue arises because custom objects are not enumerable.
00:12:39.540 Again, we need to teach Ruby how to count using our custom object. We might implement 'each' for the composite file. The implementation here doesn't matter too much; it simply loops through all the underlying files and yields each line.
00:12:55.600 This gives us convenience because, now, we can iterate through the entire book, one line at a time, without worrying about its storage as multiple files on disk. The real magic will come from this next line: when you include Enumerable, and if you have 'each' defined, you gain access to more convenience methods.
00:13:22.800 When you have 'each' defined, you can do things like map, select, reduce, and find from it. This means that the task we were trying to do earlier now works flawlessly, finding the first line in the book referencing Villifor.
00:13:50.520 Those who write this code don't need to understand the underlying implementation; they just need to know that it works intuitively. They won't care how it's parsing through multiple files; it’s our responsibility to handle that complexity and continue writing delightful code.
00:14:18.180 Innumerable provides many convenience methods, but sometimes you want to go a bit further and combine multiple methods together. For instance, if you want to output a nice numbered list with prefixes, you could use the each_with_index method. This method allows you to loop through an array while keeping track of each item’s index.
00:14:51.600 By default, it counts from zero, but you can customize the starting index to count from one. This block will output the result you’re looking for. However, what if you want to capture an array instead of writing directly to standard output because you might want to perform further operations? Enumerable offers the each_with_index method.
00:15:07.800 Yet, when you're capturing an array, you typically use the map method. Unfortunately, Enumerable does not provide map with index directly, leading you to wonder if you’re out of luck.
00:15:29.760 Well, no! Here's something that looks familiar: array.map. Notice there's no block on the map. You're immediately chaining another method, and surprisingly enough, this works.
00:15:34.720 You end up with an array styled as "1st," "2nd," etc. This behavior doesn’t stop at map; most of the Enumerable methods support similar chaining. Here’s an example with cycle—again, no block, and it still functions as intended.
00:15:55.770 To understand how this works: if you call map on an array without a block, you don’t receive an array but instead an enumerator. Referring back to Enumerable, it’s the module, while the enumerator is an object instance.
00:16:21.480 So, what is an enumerator? An enumerator is the component that makes counting in Ruby delightful. Each time we use '3.times', without a block, that's generating an enumerator. If you use '5.up_to(10)' without a block, that too is an enumerator. Even 'each' calls give us an enumerator when called without a block.
00:16:48.360 As library and code writers, if we do our jobs right, users often won’t notice that enumerators drive the delightfulness under the hood. Today, I’ll provide you with three mental models to conceptualize what an enumerator is and what it does.
00:17:18.600 One mental model is thinking of enumerators as cursors. An enumerator is not inherently the collection; rather, think of it as a cursor guiding you through iteration over a collection. For example, in this diagram, our cursor is currently on the second item. It can move along and continue through the set.
00:17:40.920 This cursor can move and pause; passing the enumerator to another method can allow someone else to advance it one step further. Thus, you don’t have to iterate through all at once.
00:18:01.920 Another mental model for enumerators is thinking of them as series generators. They frequently establish fairly complex series for controlling iteration and building sets that execute tasks.
00:18:22.920 For instance, you can construct enumerators using the 'produce' constructor, signaling that every time you wish to get a value, it can give you a random number between 1 to 100. You can keep requesting values, and this particular series can even be infinite.
00:18:46.560 You might find yourself questioning how to iterate through something infinite, raising another mental model: that of a lazy-ish list. Enumerators yield the values that have been evaluated; however, there may be potentially infinite additional values left in the list that have not yet been processed.
00:19:02.520 As such, we only evaluate them should they be needed, allowing us to work with seemingly infinite lists. Another example of an infinite series is the cycle method on an array; if you don’t provide an argument, it will simply repeat that array infinitely.
00:19:38.760 But don't worry; you can call a method like 'take' to obtain only the first four elements, meaning just those first four values will be evaluated, letting the rest remain.”} ,{
Explore all talks recorded at RubyConf 2022 Mini
+29