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.”} ,{