Metaprogramming

Summarized using AI

Declare Victory with Class Macros

Jess Hottenstein • February 28, 2023 • Providence, RI

In the talk titled "Declare Victory with Class Macros," Jess Hottenstein explores the concept of class macros in Ruby, aiming to enhance code readability and extensibility through metaprogramming techniques while avoiding complexity. She begins by introducing herself and her background, sharing her journey with Ruby since 2009 and her involvement with Splitwise, where a team of developers create a product that helps manage financial relationships. The session outlines several key points:

  • Definition of Class Macros: Class macros are methods defined on a class that can create or manipulate instance methods, facilitating easier code management. Hottenstein exemplifies this with attr_reader, which enables object attribute access.
  • Understanding attr_reader: Using the Greeter class, she explains how attr_reader :name functions as a method generator to create accessors for instance variables, showcasing Ruby's object-oriented nature.
  • Exploration of Metaprogramming: Hottenstein discusses how class macros can be employed to expand functionality and include methods dynamically as needed, emphasizing the balance between usability and functionality.
  • Caching Techniques: Caching is illustrated through the example of memoization. She relates this to a gem created by a developer at Panorama, explaining its performance benefits by storing method outcomes to reduce computation time.
  • Hook Methods: The implementation of hook methods is presented as a means to manage method behaviors upon inclusion in other contexts, providing flexibility in code design and functionality.
  • Creating a cachable Macro: The practical implementation of a cachable class macro is covered, focusing on inter-method references and ensuring that both cached and non-cached operations function correctly.
  • Community Engagement: Hottenstein encourages the Ruby community to participate in discussions and share knowledge, closing with a reminder of the enriching experience fostered by collaborative learning.

In conclusion, Hottenstein emphasizes that mastering class macros can significantly enhance a developer's ability to manipulate Ruby's syntax to create clean, readable, and purposeful code. She directs attendees interested in further exploration to a related cachable gem for more practical examples and insights, and is grateful for the community's collective knowledge shared throughout the conference.

Declare Victory with Class Macros
Jess Hottenstein • February 28, 2023 • Providence, RI

How can we write classes that are easy to understand? How can we write Ruby in a declarative way? How can we use metaprogramming without introducing chaos?

Come learn the magic behind the first bit of metaprogramming we all encounter with Ruby - attr_reader. From there, we can learn how different gems use class macros to simplify our code. Finally, we’ll explore multiple ways we can make our own class macros to make our codebase easier to read and extend.

RubyConf 2022 Mini

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!
Explore all talks recorded at RubyConf 2022 Mini
+33