00:00:11.240
My name is Jess. Welcome to "Declare Victory with Class Macros".
00:00:16.740
A little bit about me, I'm an enthusiast of many things. The conference is almost over, but please feel free to come talk to me about any of these things.
00:00:23.699
Andy is doing cartwheels in the hallway. I'm going to call it soccer. We got that term from you; it's like association football.
00:00:29.400
The World Cup is coming up, so here we call it soccer.
00:00:35.640
It took me a while to find all these things, but I feel like I really have found my communities. I moved to Rhode Island in 2015 and now I proudly declare that I am a Rhode Islander. I discovered Ruby in 2009 when I encountered a codebase written in Perl. Back then, there was a script that could convert a Perl program into Ruby, and that was the first Ruby codebase I ever worked on. I've been involved with Ruby for a while and I love it.
00:01:04.559
I'm on Twitter; I just signed up for Mastodon. You can tweet me there, but I never tweet. The only way to win is not to play.
00:01:09.600
I work for Splitwise. This is a bit of a promotional session, so thank you, Splitwise, and thank you to RubyConf Mini for granting me the time to talk to you all. I hope to use it wisely while discussing Splitwise. Splitwise has a really cool mission — we help people manage their financial relationships with their most important friends. If you haven't had a chance to talk to someone who knows something about Splitwise, you can always come see us afterward.
00:02:03.600
Currently, we have 29 people on our team, including eight back-end engineers, working on a Ruby project that serves tens of millions of users around the world. It's really exciting! You can check out splitwise.com/jobs if you're interested; we'd love to have you join our team.
00:02:51.420
Now, let's discuss what we are actually here for — class macros. Before diving into that, let's outline what we're going to cover: What are class macros? What can they do? How would I create one?
00:03:11.780
So, what is a class macro? Every person who runs ‘Learn Ruby in 20 minutes’ sees a class macro. They are foundational to the language; so basic that they are introduced on just page three. For example, consider the Greeter class from rubyland.org. It has an instance variable, but how do you access that instance variable? You can't access instance variables directly, but then there's `attr_reader`, which comes to the rescue.
00:03:45.540
We can call a method like `attr_reader :name`, which is a class-level method that generates some code for us. Effectively, this is equivalent to defining a method called `name` for the class, which returns the value of the instance variable.
00:04:04.560
So now, the Greeter will respond to the method `name`. This allows us to simply call `name` on it, getting the response "Andy". Along the way, I'm going to share some tips and tricks I’ve learned, particularly things I do in IRB. The interesting part is watching how others navigate the language and their tools.
00:04:51.840
The Greeter class has a set of instance methods. You can pass a boolean to instance methods, and if you pass false, you'll only get the instance methods defined on that class. Otherwise, by default (or true), you’ll receive the entire inheritance chain. For example, say `hi`, `bye`, and `name` are all right there.
00:05:42.780
In Ruby, everything is an object, and though there are no first-class functions, you can get a reference to a method. The Greeter class contains an instance method `name`, allowing you to perform various operations on it. For instance, you can call `source_location` on it to see where it was defined.
00:06:01.979
When I run this in IRB, it shows me the file and line number, helping me track down how everything is connected. I’ve spent quite some time scouring the C codebase that powers Ruby, so if anyone has insights, I’d love to learn more.
00:06:37.380
Additionally, you might have heard talks discussing method dispatch, the ancestor chain, or double dispatch, which all relate to how Ruby handles method calls. Whenever you include a module or subclass something, you add to a class's ancestors or descendants. The Greeter class will have a set of ancestors, including `Greeter`, `Object`, `PP`, `BasicObject`, and others.
00:07:07.440
I just fell down this rabbit hole, and I mentioned earlier that `attr_reader` is equivalent to defining a method. `attr_reader` is defined in C, and while I’m not proficient in reading C, I can glean some understanding piece by piece. It generates a symbol based on something in the class and some arguments, allowing it to know which attribute name to assign.
00:07:16.560
The method handles visibility concerning reading. When it evaluates to true, it adds a read method, with various steps involved, which can be seen when you look at `define_method`. This is also defined in C but calls a similar method with slightly different constants. Now, let’s look at other examples.
00:08:19.099
Shout out to the folks at Panorama. Memoization is a common technique that allows you to store method results to optimize performance. Gemma, a developer at Panorama, created this gem, which has excellent benchmarking and is very useful for performance. If a method sleeps for two seconds before returning, any subsequent calls can return the cached value immediately.
00:09:03.240
Class macros, in essence, are class methods that define or modify instance methods. They originate from the smallest methods that expand into more complex functionalities. This concept appears throughout various applications: in programming languages, spreadsheets, and even in the context of internet worms.
00:10:00.600
Everything runs when a class is evaluated, and if you create a class macro, you should consider adding guard statements, especially in environments like Rails, where methods can run multiple times during loading and database connections might not be available.
00:10:31.159
We are essentially declaring new behaviors for the class. For example, at Splitwise, we extracted a library we haven’t published yet but it works with webhooks to notify other consumers any time we have objects that get saved. Having a clean interface to express that functionality is essential. When we talk about making our objects publishable and formatting them with presenters, it illustrates how we can extend existing functionalities.
00:12:31.260
As seen in the memoization example, we can enhance existing instance methods. The goal is to have a declarative approach, especially with Ruby being a fantastic language for creating Domain-Specific Languages (DSLs). You can write code that feels very readable and intentional. This ideally reduces cognitive load for developers when understanding the code.
00:13:09.000
In practice, working with expenses that exist in various currencies showcases another use case. We can tap into services like Open Exchange Rates to get currency conversions. However, free services often have limitations, such as only allowing a limited number of API calls each month.
00:13:55.260
When facing such limitations, one common solution is to implement caching mechanisms. Caching allows us to store results temporarily to minimize redundant computations. Implementing a caching strategy typically involves wrapping our classes, allowing us to fetch values from an existing cache or compute the result if it’s not already cached.
00:14:59.520
Writing a class macro initiates a process of deciding how you want the interface to feel. With memoize, for instance, a method may override existing ones, which are options available when designing your class macro.
00:15:25.320
So, if we had to create a `cachable` class macro, we would want our existing methods to internally reference the original functions while providing additional methods to manage cache behavior. By making the decisions of what interface you want to have — creating all methods with or without caching — balances functional needs with usability.
00:16:25.740
When defining a `cachable` class macro, anticipate the necessary structure. Generally, you would need several methods to ensure both functionalities can be executed reliably without conflict. Identifying the differences between how methods interact in a class structure helps define a clear path forward.
00:17:41.700
You ultimately want the `cachable` class method to perform efficiently, and through a proper setup, you can guarantee that calls to cached and non-cached methods refer back to the correct original behavior.
00:18:11.700
Finally, the last important aspect we’ll cover is the implementation of hook methods within our module. These methods allow us to precisely control how our methods behave and interact when included or extended in different contexts. This adds a tremendous amount of flexibility to the solutions we develop.
00:19:45.839
These hook methods will run automatically when certain events happen, allowing us to keep our class methods consistent and functional.
00:20:25.560
When structuring all these elements together, we need to ensure that, once included, the caching components function correctly and seamlessly with the rest of our class's logic. By opening up the module, we can insert those two new methods, ensuring we are passing the creations and behavior exactly as we desire.
00:21:07.680
To recap, class macros primarily serve to enhance your class's functionality by adding behaviors that can simplify or abstract complexity, making it easier for others to engage with your code and for you, later, to revisit your own coding.
00:22:01.800
In conclusion, if you're seeking to explore class macros further or need guidance, check out the `cachable` gem created by a former engineer of ours; it's a treasure trove of insightful examples and practical applications. Each macro we explore can expand your understanding and abilities within Ruby.
00:22:50.300
Remember, the Ruby community is filled with amazing individuals. Connecting with one another, sharing insights, and learning collectively makes for a truly enriching experience. I’ve had such a wonderful time over the past few days at the conference, and I am grateful to each one of you for participating. Thank you!