Talks

Declare Victory with Class Macros

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 Mini 2022

00:00:11.240 my name is Jess uh welcome to declare Victory with class
00:00:16.740 macros so um a little bit about me
00:00:23.699 I'm an Enthusiast of many things the conference is almost over but please
00:00:29.400 feel free to come talk to me about any of these things uh Andy is doing cartwheels in the
00:00:35.640 hallway um I am going to call it soccer Andy soccer
00:00:43.760 we got that term from you it's like Association like you know football I
00:00:49.920 don't know so uh the World Cup is coming up so
00:00:55.020 here we call it soccer um all right so uh I it took me a while to
00:01:04.559 find all these things but I feel like I really have found my communities I moved
00:01:09.600 to Rhode Island in 2015 and now I proudly declare that I am a Rhode Islander and I don't know where else
00:01:16.380 I've where I'm from I'm from Rhode Island and Ruby I found Ruby in 2009 there was
00:01:24.360 a a code base that was written in Pearl and back then there was a script that
00:01:30.840 you could apply to a pearl program and it would convert the whole thing into Ruby and that was the first Ruby code
00:01:37.140 base that I ever worked on I've been doing this for a while and I love it
00:01:43.259 all right um I'm on Twitter or I just signed up for
00:01:48.780 Mastodon that's a thing I guess I don't know um you can tweet me there uh but I never
00:01:55.560 tweet the only way to win is to not play um all right I work for splitwise this
00:02:03.600 is the a shill session uh thank you splitwise thank you uh rubycon for mini for granting me time up here to talk to
00:02:11.340 you all I hope to not waste it much uh talking about splitwise but splitwise uh
00:02:16.860 has a really cool Mission we try to help people with their financial relationships with their most important
00:02:23.700 friends so if you uh I'm sure everybody has had a chance to talk to somebody who knows
00:02:30.599 something about splitwise if you didn't come to the game night last night or talk to Jen at the booth uh you can come
00:02:36.120 up and talk to one of us afterwards there are gen is this correct 29 right
00:02:43.680 now yes I I use the slack directory I think that's the only thing that's reliably up to date uh 29 people
00:02:51.420 um eight back-end Engineers um creating a ruby project that has tens
00:02:57.420 of millions of users around the world it's like pretty dope and you you can visit splitwise.com jobs
00:03:06.300 we're hiring we'd love to have you join the team
00:03:12.420 all right so now what we're actually here to talk about um class macros
00:03:18.000 uh well before I launch into that why don't we talk about what we're going to talk
00:03:24.480 about what are they seems reasonable what can they do
00:03:30.239 uh how would I make one all right so let's jump in um what is a class macro every person
00:03:38.040 who runs the like learn Ruby in 20 minutes sees a class macro you they are
00:03:45.540 foundational to the language they're so basic that they're just people just get tossed right in on page three so what is
00:03:52.739 it um you there's a you can go to the rubyland.org a greeter class it's got an instance
00:03:59.580 variable how do you access that instance variable
00:04:05.879 you can't access instance variables right so there's an Adder reader comes to the
00:04:11.819 rescue so we can call a method that will do something so that we can now access
00:04:19.440 that instance method so we can just say add a reader name this is a class level
00:04:27.000 method so it is a class and it is going to generate some code
00:04:32.639 and so it is a macro so that is equivalent to class reader
00:04:39.120 Define method name and what is the body of that method it's what's put between the do and the N
00:04:45.600 block at name so now you can do this so
00:04:51.840 so greeter will now respond to name great reader you can call name on it it's Andy
00:04:59.759 um I'm going to show you some tips and tricks along the way that you know things that I do in IRB
00:05:05.400 um people have all of their different patterns I think it's really interesting when to watch somebody else program just
00:05:11.580 to see how they navigate the language and how they use IRB and their editors
00:05:17.360 so greeter has a set of instance methods you can pass a Boolean to instance
00:05:22.860 methods and if you pass false you get only instance methods defined on that
00:05:28.500 class otherwise by default or true you'll get the entire
00:05:35.400 inheritance chain so say hi say bye name it's right there
00:05:42.419 greeter you can this is Ruby there are no like first class
00:05:49.100 functions in Ruby oh no but everything's an object so you can actually get the
00:05:56.400 reference to the method so greeter has an instance method it's called name and
00:06:01.979 so you can do stuff to it it's a method object and so you can call Source location on it and it'll say when I'm
00:06:10.800 running this in IRB it'll say IRB line 19 and you can scroll back and be like oh where was I oh add a reader name okay
00:06:18.900 so that's where I came from I actually scoured the C code base to try to figure
00:06:24.600 out how that actually works if anybody knows I would love to know more I don't I don't know where that comes from
00:06:31.979 um and uh another thing that you can look at and you've heard various talks
00:06:37.380 talking about the ancestor chain or what is the method dispatch or double
00:06:42.900 dispatch or all these sorts of things this all comes into how Ruby does method dispatch
00:06:48.780 um anytime you include a module or you subclass something you add to a classes ancestors or
00:06:57.539 descendants and so a greeter has a set of ancestors and right now it is greeter object
00:07:07.440 PP object mix in that comes from IRB kernel and basic object and so that'll come into play later
00:07:16.560 um I fell down this Rabbit Hole let's just take a look for a second so um I said no
00:07:23.699 that that was that is this true Adder reader is equal to defined method
00:07:34.800 so add a reader is defined in C I am not great at reading the C but like you can
00:07:40.800 kind of um piece by piece it together ID for Adder generates a symbol based on
00:07:46.139 something and so coming from the class and some of the arguments it figures out
00:07:52.919 what symbol the attribute should be the name and then it assigns it calls this
00:07:59.160 Ruby Adder method with it so passes in the class ID in true false
00:08:05.160 true so if you look at that the class is indeed the value of class value is like
00:08:12.479 a rubyish object and so it does a bunch of things mostly involving visibility
00:08:19.099 and if it's a read if read is true which in this case it was it's going to add a
00:08:25.199 read method so method type Ivar and then a bunch of
00:08:31.020 other stuff and if you look at Define method which
00:08:36.360 is also the defined in C it calls the exact same method RB add method slightly
00:08:43.020 different constants but basically the same thing all right out of the wrapable
00:08:48.839 other examples um any Panorama ad folks okay shout out
00:08:56.640 memoize uh if you're going to come to a talk and uh you know why not have the
00:09:03.240 organizer's gem in your conference talk so uh Gemma I think was working at
00:09:09.899 panoramat at the time she wrote this and along with her co-author who I heard was
00:09:16.560 at the conference I don't know if he's here check out that one um so memoize
00:09:24.440 is a gem that allows you to memorize
00:09:30.200 something they had a really interesting performance like benchmarks so that like
00:09:36.060 they Benchmark the hell out of it and it's very useful you should check it
00:09:41.519 out and so what we see here is memo wise this method uh
00:09:49.880 is annotating something about slow value
00:09:54.899 and so if you read the documentation or dig into it basically it's saying
00:10:00.600 um well let's see what happens it sleeps for two seconds before returning okay so you call that slow
00:10:07.380 value returns immediately the value is memorized and so people would use
00:10:14.100 memoization all the time uh some of my developers has been like oh let's make a
00:10:19.500 jam about memoization and it's like it's we could it's been done here's libraries
00:10:25.980 or it's not that bad all right finally what is a class macro
00:10:33.420 a class macro is a class method right everything's a
00:10:39.839 method so it's a class method that defines or modifies instance methods
00:10:45.480 so a macro in the old computer science nomenclature is something that's like
00:10:50.700 small that expands to be bigger that's like goes from micro to macro
00:10:55.980 um them has macros Excel has macros the internet used to have macro worms I
00:11:02.399 guess that infected Windows computers um so in this context it's a small
00:11:08.160 method and it's going to make a bunch of other code it's important to note uh unlike Java I
00:11:15.600 don't know how many people are new to Ruby here but like everything gets a run
00:11:21.120 so when the class is evaluated that is a method and it runs at that point in time
00:11:26.519 and so if you make a class macro you might have to add guard statements
00:11:32.339 to it if you're running it in an environment like rails will run methods several times during load maybe your
00:11:39.300 database isn't ready yet so if it's a database if it involves a database the database may not be be there yet and so
00:11:45.600 just know that anytime you are like just evaluating your file that code is going
00:11:52.620 to run um why do we do it um I think we just saw some examples but
00:11:59.220 we are declaring new behavior for the class so we can add new instance methods
00:12:05.640 just like we saw with add a reader we can change the behavior of a class
00:12:11.940 just like oh actually this is a this is a novel use case I couldn't find an open
00:12:17.459 source gem that um had a good usage of this technique we at
00:12:23.399 supplies extracted uh like a library we haven't released that as a gem I'm not here to voice gems upon anybody
00:12:31.140 um uh publishable basically anytime uh we have a bunch of objects that get
00:12:37.260 saved we want to notify other consumers about that and so we wanted to do that via web Hooks and so that seems like the
00:12:45.660 type of a library-ish kind of thing and we really thought it might be have a nice interface if we just could say
00:12:52.019 something like hey this is publishable how do you format it oh we'll give it a
00:12:57.060 presenter um what's our strategy let's do it asynchronously that's
00:13:03.000 um if anybody wants to talk more about that I'd be happy to talk about that too so what does it do anytime you call create
00:13:09.839 on a funds flow it enqueues a web hook delivery job and if you want to talk about funds flows and all the cool stuff
00:13:15.959 that schoolwise is doing again I'm not here to show but
00:13:21.540 um okay so finally we can add new Behavior to existing
00:13:28.440 instance methods I think this is the context you that you're going to see class macros in most especially with
00:13:35.279 gems we saw it here with memoize where
00:13:40.639 there's a method called slow value but when we call slow value we don't get the
00:13:48.180 implementation that we see right in front of us so it's doing something under the covers
00:13:55.440 um well why don't we try to figure out what that
00:14:00.660 is and maybe we'll uh we'll do it why do we do it it's declarative there's like a
00:14:07.019 concept we just talked and learned about functional programming in Ruby there's also declarative programming in Ruby
00:14:13.200 Ruby's a great language for making dsls something that can like look like a statement is really a method call and so
00:14:22.560 it's really nice looking if it just says publishable and then you're like okay
00:14:27.839 yeah I know what that means and then you can just kind of move on with your day it you're not really concerning yourself with like when that code runs or why it
00:14:35.279 ran or anything about it it's just like uh you see it it lets you know something
00:14:40.500 about that class um it's interesting to think this is really
00:14:45.779 just a design choice this is a this is something that you're communicating to your fellow developers about the code
00:14:52.980 that you're writing and so oftentimes we do this as a courtesy to others or our
00:14:58.199 future selves so that when we come back and we're reading the code it's much easier to read and understand and reason
00:15:04.740 about all right hello macro
00:15:15.060 given an example real world example um I
00:15:26.760 um people can add expenses in lots of different currencies sometimes they are
00:15:31.980 not in their native currency and they want to translate those expenses back to
00:15:37.680 their home currency so they can settle up or you know pay somebody on venmo and you can't pay somebody in Kroner on that
00:15:44.579 last I checked so this is a pretty basic
00:15:50.399 um like script uh I signed up for you can go to open exchange rates and sign up for an API key it's pretty cool
00:15:58.260 um you can call all you'll get all the rates and then it's like if you want to do
00:16:04.019 some conversion um you're great so any issues with this
00:16:09.839 other than I'm sorry uh Canadians I don't make us monetary policy it's that
00:16:16.500 I actually looked at the graph it's not that bad um but you know it's you know it's not
00:16:22.139 it's not great um so uh
00:16:29.940 uh so the free service for exchange rate service you only get a thousand API
00:16:36.000 calls a month and so at this rate I'm going to be out of
00:16:41.519 API calls the solution of course is
00:16:48.560 throw money at the problem I like I like this idea I like this idea I actually
00:16:54.060 don't know if we pay for that subscription uh
00:16:59.100 um caching
00:17:04.679 right so let's catch it so we're going to take this class and we
00:17:10.140 are going to uh just wrap given the fact that we like have some cash somewhere I'm using
00:17:17.339 Global variables here um just so for brevity of the slide but most caches will have a method on it
00:17:25.520 called Fetch and you pass it a block and if it's there it will retrieve it out of
00:17:31.799 the cache and if it's not there it'll execute the block it'll return that value to you and it'll store that in the
00:17:38.340 cache it's really a nice interface and you need to have some key there because
00:17:44.460 it's like otherwise what are you fetching so I just named it after the method so all
00:17:51.179 is this better
00:17:57.539 let's double check maybe it's like Julia's DNS caches
00:18:03.059 nope or we're still good okay good okay finally when we're writing a class macro
00:18:09.660 we have to ask ourselves what do we want the interface to be so we saw memoize memoize used to
00:18:17.400 prepend uh it overwrote the existing method these are all choices that you
00:18:22.980 can make you can Define new methods always if you want to like really intercept the call so it's always called
00:18:29.039 you can do that if you want to provide a back door to get to the original method you can do that these are all just
00:18:35.340 design choices none of them are bad it's just really comes down to your use case and what you want to do
00:18:40.980 okay so if we have this is our cached
00:18:47.100 implementation but I I want to extract that into a class macro what do I want
00:18:53.760 the the API to look like yeah cashable it's like I'm annotating
00:18:59.220 some there's a method somewhere called all and it's going to be cached maybe someday I'm going to add some new
00:19:05.160 options to it expiration um there's something called a race
00:19:10.440 condition TTL so you don't like do a Thundering Herd on it
00:19:15.840 um and I I'm old-fashioned I like to include things I I think most people are
00:19:22.080 used to seeing include some people uh don't mind using extend or prepend
00:19:28.559 um I don't know I just you know my Spidey senses start to tingle when I see something and I my
00:19:35.880 brain gets stuck on it and I can't keep moving if I see include I I don't know I'm in my happy place okay so
00:19:43.320 um how do we get there these are the questions that you have to
00:19:48.360 ask yourself as you're going through the process of writing a class macro and so what needs to exist
00:19:56.160 how do we create them and how do we include them sorry
00:20:01.380 okay try to do this in 10 minutes
00:20:07.080 what needs to exist okay I need an all method that adds caching okay
00:20:14.280 so I have all and I want like all with caching and that calls all
00:20:22.020 okay what else I need an all without caching because
00:20:29.580 that's that's what I've decided to do I want to have the ability to call it without that
00:20:36.780 and so you see that here I have an all and an all without caching
00:20:44.580 I can't have two methods called all I'm going to have a cachable module I
00:20:50.880 need something to include somewhere
00:20:57.840 ah so that is the cachable class method
00:21:05.220 I got my I don't know I don't know what that is I have the cachable class method
00:21:13.380 and so cashable.all and then finally I need a cacheable
00:21:19.140 module that I can include somewhere so this is basically what I'm aiming for
00:21:27.419 okay that's what needs to exist okay how do we create it all right
00:21:34.799 so piece by piece I mean in all instance method and all without instance method a
00:21:39.900 cachable class method and a cachable module
00:21:45.059 all right one by one all instance method so
00:21:51.480 I had set up an all method and an all without caching
00:21:58.799 I'm going to move that aside for a moment because I I don't I don't want to have the cash
00:22:04.440 um code in my original class
00:22:10.440 so how am I going to get it like the behavior there I'm going to add wrap a
00:22:16.320 module around it okay well
00:22:21.360 now I need one more thing I need a method Interceptor module I need a place to put these instance methods that I'm
00:22:28.140 creating and so now I have that I can have my include my
00:22:34.380 method Interceptor and all can be there and it can call all without caching and
00:22:41.039 that's in my original method great but I want the name in my original
00:22:49.500 method to still be all okay easy okay so in my method Interceptor
00:22:56.820 I'm just going to have all call you get
00:23:02.640 um circular dependencies stack Trace too deep how how would a method called all call
00:23:11.039 its parent class with a method called all
00:23:17.159 luckily we have a mechanism to do that super we just call Super
00:23:22.980 is right up the stack it'll use the same arguments I've eliminated all arguments
00:23:29.760 from this code just for for ease but it's great
00:23:36.320 the interesting thing and this talks to what I alluded to way back when I was
00:23:43.260 talking about ancestors is now I need all to be in the ancestors the method
00:23:51.240 Interceptor I need that to be in the ancestors before the actual class and in Ruby 2
00:23:57.140 once upon a time we only had include so we had to do a bunch of weird stuff involving Alias method and it was bad
00:24:03.120 but now we have prepend and so we don't have to do that anymore and so if we
00:24:08.760 prepend the method Interceptor it's just like include but instead of in the ancestors that coming after the class it
00:24:15.780 comes before the class so anytime you call exchange rate service all you're going to get the one in the method
00:24:21.900 interceptor we did it okay now let's have all
00:24:29.220 without caching ah
00:24:37.260 all without caching needs to call the original all method
00:24:55.140 I don't know where that slide came from like I said I'm not a keynote expert um
00:25:01.580 no I lost it I lost the slide okay so
00:25:06.960 we're gonna talk about it so all without caching
00:25:13.380 needs to call the method interceptors excuse me the exchange rate Services version of all
00:25:20.340 and so it needs the ability to call alls
00:25:28.500 in method interceptors super method and there's a way to do this and
00:25:34.200 I'd write the code up here if I could um and that is by implementing all
00:25:39.539 without caching there's a another bit of meta programming you can use just like we saw before we can grab the method object so
00:25:47.159 we call method all so we have enough we have that method
00:25:52.860 interceptors all and then you can actually get its super method and so you can just call all method all super
00:25:59.460 method and now you have that super method and you can call it
00:26:07.380 all right and now we want to do something interesting and maybe the the slides will come up here shortly
00:26:13.500 um we need to get rid of all because we need some to make something generic and
00:26:19.620 so we need to just make it generic and so now we're just going to use with caching and without caching
00:26:26.760 and so there's my Method All ah my super method it's back
00:26:32.400 um so in order to do that we're going to replace all with
00:26:37.980 Define method so instead of Def you can use Define method and instead of a like
00:26:46.860 symbol you can just pass in whatever you want and so you get method name so we're going to do this for both
00:26:53.340 so now I have a method name all and a method name without caching all without
00:26:59.159 caching and I have this one here all without caching
00:27:06.200 and I'm not going to go through how to get rid of that but we'll get there okay
00:27:12.659 so so now I have the method name I can just call method on the method name get it
00:27:18.000 super method and so method in minute method Interceptor calls all without
00:27:24.240 caching all without caching refers back to where it came from but knows that it
00:27:29.820 wants to call it super method and that's how it gets out oh I did fix it great all right so
00:27:37.740 finally we need to
00:27:43.880 define the method interceptors and so the method Interceptor you can call module.new you can Define the method the
00:27:51.659 module and open it up later on there's a there's a couple different ways you can do that
00:27:56.940 all right so how do we create them we now we need how do we create the
00:28:05.340 cachable class method and the cachable module these are a little bit more straightforward we can just Define a
00:28:11.820 module Define a method what's in it yeah I'm not going to worry about it yet
00:28:18.000 there is one thing though if you look at the interface that we wanted we wanted casually be a class method
00:28:24.419 and in doing so we need um if we just included cacheable and Define
00:28:31.020 cachable in the cachable module it would be an instance method so we're going to Define this as put it in a separate
00:28:38.279 module and at a certain point maybe we'll just include that in the class okay home stretch how do we include them
00:28:48.840 so we said include cacheable that's our desired interface needs to
00:28:56.340 basically do extend those class methods so we need to
00:29:01.620 load those onto the class itself so that their class methods and I want to
00:29:07.440 prepend the method Interceptor that's where the instance methods are coming from
00:29:13.559 how did we do this hook methods final little bit so module defines a
00:29:22.200 bunch of different hook methods they are run
00:29:27.380 by Ruby itself when various things happen and then you can override them
00:29:32.580 and you can get custom behavior on your classes when these things happen like when your module is included or extended
00:29:39.720 or inherited uh method added method remove method
00:29:45.059 undefined all kinds of fun stuff okay
00:29:50.159 so how do we include our instance methods so we have our method interceptor
00:29:58.440 so we are going to include cachable so in cacheable we're going to open up
00:30:03.899 this hook method included and then we have our method interceptor
00:30:10.020 and so all we have to do prepanned method Interceptor onto the
00:30:16.260 the class handed To Us by the included hook method
00:30:21.899 likewise the exchange rate service includes
00:30:27.840 cashable so when cachable we're going to same thing open up the included
00:30:35.520 hook method and we have this class methods module that we defined earlier
00:30:41.399 and so we're just going to base this time we're going to extend with class methods extend is just basically like
00:30:47.340 include except it includes it at the class level okay let's put it all together
00:30:55.860 given our class with our nice API of include cachable cacheable all
00:31:04.559 we're going to have a module it needs the hook method that hook method needs to prepend
00:31:11.580 our module with our instance methods it needs to extend with the module with our
00:31:19.380 class methods we have a macro
00:31:25.860 and cacheable is going to be past some symbol all in in this case so when we
00:31:31.620 receive all that is the original method name we wanted them without cache method name
00:31:38.460 so all without cash and now we're going to open up our
00:31:44.100 method interceptor and Define two new methods on it and we can do this every time we call cachable
00:31:50.640 so we're going to open that module up to find two new methods on it all
00:32:07.380 it's the worst transition ever okay so
00:32:12.600 um in the body of the all method
00:32:18.600 this is where we have our caching we saw this before so we have this cache and all we're going to do is we're going to
00:32:25.320 call the without cash method name okay
00:32:35.580 and finally our without cash method name is going to call back to the other
00:32:41.640 method except call it's super method to get the parent Behavior
00:32:52.200 all right that's it class macros what are they they are a method that is
00:33:00.779 included in your class that changes the behavior of your class usually by declaring a bunch of instance variables
00:33:07.559 or other things what can they do magic uh they're really cool they can they can show respect to
00:33:15.840 your fellow developers and your future self by making declarative constructs so
00:33:20.880 that you don't have to think so hard every time you read your code how do I make one
00:33:26.220 uh um download my slides I don't know I don't
00:33:31.679 know um I will admit if you remember one thing from the slide
00:33:38.399 I'll I will admit I I don't remember uh all the time how to make a class macro
00:33:44.159 if you remember one thing from this talk call back
00:33:49.380 um please don't remember that splitwise is amazing and that we are hiring that's
00:33:56.340 not that's not what we're here to talk about it's not even how to make a class macro
00:34:02.460 that is like what ostensibly you are sitting in this room for or you work
00:34:07.620 with me and so or for me so you have to be here
00:34:15.419 um uh every time I we spin up a new thing that is a class macro I will refer
00:34:21.599 to uh this gem that one of our former Engineers open source it's called cachable it does this exact same thing
00:34:27.599 and it has a bunch of other meta programming to handle all the edge cases um download it and repurpose it to do
00:34:35.460 whatever it is that you need to do it's a good jump but um if you remember one thing it's that
00:34:42.960 we the Ruby Community the one that I found in 2009 the one that you are all part of
00:34:49.099 uh are amazing and it was I've had such a blast here for the past three days
00:34:54.419 talking to you all uh it was so amazing that Gemma like spun up this conference
00:34:59.520 from nothing and that you all came and so for that I thank you