Ruby
Metaprogramming? Not Good Enough!
Summarized using AI

Metaprogramming? Not Good Enough!

by Justin Weiss

In this talk from RubyConf 2016, Justin Weiss explores the depths of metaprogramming in Ruby and the possibility of altering fundamental language behaviors beyond the conventional metaprogramming techniques.

Main Topic

The session centers on how Ruby's metaprogramming capabilities can be expanded to create a more flexible object model, enabling developers to redefine core language functionalities.

Key Points

  • The Power and Joy of Ruby: Weiss opens the presentation by sharing his long-time affinity for Ruby, highlighting its ability to boost programmer happiness and productivity through its intuitive design and metaprogramming features.
  • Beyond Basic Metaprogramming: He challenges attendees to consider how to modify fundamental aspects of Ruby, such as method dispatch and inheritance rules, using metaprogramming strategies.
  • Building a Flexible Object Model: The talk delves into creating a new object model in Ruby, focusing on a few core methods like add_method, lookup, build_object, delegate, and send.
  • State vs. Behavior: A key concept discussed is the difference between state (unique to each object) and behavior (shared among objects), emphasizing the significance of understanding these differences in the context of object-oriented design.
  • Method Lookup Dynamics: Weiss discusses how method lookup can be redefined and made flexible by allowing developers to create their own logic to identify which method to invoke in various scenarios.
  • Interception of Method Calls: He introduces the concept of using interceptors to log method calls and handle errors like retrying failed methods, showcasing the model's potential flexibility.
  • Multiple Inheritance: The talk concludes with an example of how to implement multiple inheritance in Ruby, demonstrating the versatility of the newly created object model and how it allows for diverse class structures.

Significant Examples

  • Weiss presents a series of coding demonstrations to illustrate how new classes and methods are defined, enabling the flexibility of method calls and behavior manipulation. These examples include creating a Greeter class and redefining how methods are called and logged.

Conclusions and Takeaways

  • Experimentation Encouraged: The key takeaway is the importance of experimenting with metaprogramming techniques to deepen understanding of programming languages. Weiss promotes building and breaking conventions to discover new ways of coding.
  • Understanding Language Architecture: By exploring these advanced concepts in Ruby, developers gain insights that can enhance their programming practices across different languages. Through this exploration, they are encouraged to be creative and flexible in their programming approach.
00:00:15.209 All right, well hey everyone! Hope you've been having fun so far and you're not totally talked out yet. I'm Justin Weiss, and I lead the web development team at Abu, where we get people the quality legal help they deserve.
00:00:21.330 Before we get going, at the bottom of these slides is my Twitter handle. Feel free to ping me with any questions or feedback, and I'll check it out afterward. There's also a hashtag over there at EOG's, feel free to use it if you tweet about or take pictures or anything like that.
00:00:32.920 I guess most of us are here because we love Ruby. This slide kind of says it all for me. But how many of you have started to wonder what your next favorite language is going to be? This isn't a totally new thing. One of the biggest ideas that stuck with me after reading 'The Pragmatic Programmer' so many years ago was the concept of learning a different language every year.
00:00:45.680 So, I did, and I still do. After reading that book, I decided that Ruby would be the language I learned for the year. I built everything in it, feeling like it was my new language. Naturally, I thought the next year's language would also be my new focus. I learned another language but came back to Ruby three times.
00:01:08.200 Here I am, over ten years later, and my primary language is still Ruby. This kind of bothers me a little bit because I want to think of myself as open-minded. I want to be more flexible than that; I don’t want to identify myself solely by the language I write in, nor do I want to default to one language just because it's the one I'm most familiar with.
00:01:22.270 But why doesn't anything else stick quite as well as Ruby? If you think about this a little bit, you come to the question: what makes Ruby, Ruby? Why do you use it? When you think about Ruby, what comes to mind?
00:01:39.850 What about programmer happiness? I mean, if Ruby was designed to make programmers happy, I feel like it's wildly succeeded. I've never had a language fit my brain so well or helped me turn my thoughts into code so easily. That’s really cool, but there’s also something else. Think about language features: just like you might think Erlang equals actors or Haskell equals monads, with Ruby, I think about metaprogramming.
00:01:58.420 So, is it programmer happiness or is it metaprogramming? Why not both? I think the metaprogramming part, in a way, leads to the happiness part. I can see by some of your faces that you don't totally agree, but I do. With metaprogramming, you can craft the language to fit your needs. You aren't stuck with what the language gives you, and the language doesn’t fight you.
00:02:16.360 Those things make the language feel like a part of you, like your favorite jacket or your laptop covered with stickers of your favorite things—not just a tool you use to get through the day. That’s how I feel: metaprogramming leads to happiness. Ruby helps you feel joy by getting work done more quickly and having a lot more fun while you do it.
00:02:41.439 In my experience, metaprogramming plays a big part of that. But what if you wanted to know what else is possible? What if you wanted to do more than what metaprogramming allows you to do? What if you wanted to change how a language works at a deeper level? Ideas like basic inheritance, like if you don't find a method in your class, call it in your superclass.
00:03:12.609 Those patterns are usually hard-coded into a language and can't really be changed easily. Even if sometimes you want to skip a generation. But what if you wrote code in the language using the same pieces you use to build that language? You’d be able to change almost anything about how the language works. You'd be able to mold the language to the ideas and mental models that your app uses.
00:03:39.210 You can learn about a language at a deeper level than you normally have access to. The neat thing is, we can get there. To do it, we’ll try building a brand new object model on top of Ruby. Okay, 'object model' is a little jargony, but what I mean by that is the core concepts, the data structures, the methods you use to build things in your language. You have classes, objects, method missing, inheritance—all of these things are pieces of an object model.
00:03:56.970 If a language is how it looks, you can think of an object model as how it works. If our object model is flexible enough, it can change itself, and that’s what we’re going for. So what would that flexibility look like? There’s a paper I found called 'Open Extensible Object Models' that describes how to create a flexible object model like this from only a few small parts.
00:04:26.289 We’ll follow that, but we’ll also take advantage of some of the flexibility that’s already built into Ruby, so we won’t follow it exactly. And all of this starts with a question: if you’re given a method name and you have some arguments, how do you know which version of the method or which code to call? There are so many interesting differences between languages.
00:04:49.749 For instance, if it has inheritance or pattern matching or prototypes or proxying, they all come from answering this question in a different way. But what if you could describe, in code, how to find the right method? And what if you could change that code to act differently in different situations? You could change the entire style of your language just by overriding a method.
00:05:20.500 In a language with simple single inheritance, that lookup code might look something like this: you look up a method by name in your object. If it doesn’t exist, you find it in your parent instead. But already we're starting to assume some things, like object, class, parents. Do we really need to lock those ideas down already? What's the bare minimum we need to really be an object model?
00:05:45.129 It helps to think about what an object actually is. It needs to keep track of some states, like a person's name or their favorite food, and it needs some way to access or change that state, like a set name or an eat method. Those methods make up the object's behavior. So, state and behavior—those are really the two kinds of things that we need.
00:06:10.899 There’s a big difference between state and behavior, though. You can share behavior between a set of objects—like every person object might have an eat method, and that method would run the same code no matter who calls it. State, however, is specific to a single object. For instance, while two people could have different favorite foods, that food can be eaten in the same way.
00:06:35.330 This is an important part and it kind of drives a lot of the rest of the stuff that comes in here. It’s good to solidify: state is unique to an object, but behavior is shared between a set of objects. So what is behavior then? If you think of behavior as a group of methods shared by a bunch of objects, the answer starts to solidify a little bit. A group of methods is state, right? It's just a collection.
00:07:00.600 So, what if behavior could be an object itself? It might look something like this: it has a set of methods that could be used by another object that points to it. You could even call this kind of object a class, like a person class, because it acts a little bit like a Ruby class. Different objects that inherit from or use it can all use its methods.
00:07:25.000 But, if a class's state is a set of methods, what would a class's behavior be? What if you went one step further? This is where it starts getting a little tricky because it's a bit beyond what we're used to day-to-day. Remember, behavior helps you access and change your state. So if you wanted to add a method to something we're treating like a class, where would that 'add method' come from?
00:07:54.000 How would that class know how to look up methods that are on it or its inheritance tree? Just like how the 'eat' method on a person object comes from its behavior, the 'add method' on a class object comes from its behavior. You just follow those arrows, those links. You can think of this object, this one on the far left over there, as the class's behavior or the class's class.
00:08:19.370 This is a little bit weird and it gave me a headache at first, because what is the class's behavior's behavior? And then what is the class's behavior's behaviors' behavior? And so on. But eventually, it all goes recursive and you can stop thinking about it. What's cool though is that class, class objects, and behaviors—they're all kind of, at their core, the same thing: they’re just state and behavior.
00:08:43.500 But you start to extract these kinds of patterns as you start playing with them and hooking them together and filling them out. Even though they’re all the same thing, here I’m going to describe them as a few different things, because if you think too abstractly, the stuff becomes impossible to keep in your head. Trust me, I tried. For now, I’m going to define an object as any of these things: a class, a thing that builds objects, holds methods, and has a parent—like one level abstracted from your objects.
00:09:09.780 And a behavior is a thing that holds specific methods, like 'lookup' and 'add' method, which make up the core of this object model. So what do we have so far? We have a simple object, a way to add methods to an object's state, and we have a lookup method that you can use to find methods given a name.
00:09:29.000 There are really only a few more things that we need. If you have a class, you need some way to create objects from it, like new objects that can use that class's methods. We'll write a method for that; we'll call it 'build object'. And you'll also need to create new behaviors or subclasses, so you can override methods. For now, basic inheritance is enough to get things started, so we'll need a method to handle that; we'll name that one 'delegate'.
00:09:49.720 Because after calling it, unknown methods get delegated to the thing that's linked as the parent—the class that you call 'delegate' on. And then, the last thing we need is a way to actually call methods by name, so we'll name that one 'send'. So we have five methods: 'add method', 'lookup', 'build object', 'delegate', and 'send', and one super generic object layout. With just those parts, you have a healthy object model you can use to build in almost any language and extend in almost any way that you can imagine.
00:10:12.010 I said we can build it in almost any language, which is true, but let's build it in Ruby to keep Ruby's helpfulness as far away as possible. We'll define our object as a basic object. If you haven’t seen 'BasicObject' before, it’s just like a normal object except it’s missing all the methods that make Object useful—so we want to keep ours small and simple.
00:10:34.290 We don’t want to accidentally run into that stuff and cheat a little bit, so this is a good fit. By the way, people don’t need to fully remember or completely understand all this code. I’ll include a link with a gist for all the sample code at the end.
00:10:47.930 We’ll need to write those core methods, the ones that I just described. Once that’s done, we can use those core methods to start creating objects and hooking everything together. The first one we need is 'add method'. This one defines a method by adding it to a class. Remember, classes store methods in their state, so this one is pretty easy; it just adds the method implementation to the state under a methods key.
00:11:06.340 For all these methods, as you can see by my beautiful ASCII art arrow up there, we pass the thing the method was called on as the first argument. So if we're calling 'Person.add_method' or 'Justin.add_method :hello', Justin would be passed as that first argument and Person would be passed as that first document.
00:11:27.180 The next one we’ll write is the 'lookup' function—at least the default one. This one’s kind of big, so we’re going to take it in parts. The first thing we’ll try to do is find the method that we’re trying to call in the current class’s behavior state. Remember that 'add method' we just put in there? Here we’re just pulling it right back out.
00:11:44.990 If it can’t find it, it’ll go through the parents and all of the rest of its ancestors by calling itself recursively. So here’s what that looks like all put together: we try to pull the method out of the behavior state, and if we don’t find it, we call itself recursively on the parent to try to track it down somewhere in our parent hierarchy.
00:12:03.300 The next one is 'build object'. This one is like 'new'; it creates a new object from a class or from a behavior. This creates a tiny object, one of those we just defined earlier. It sets an empty state so we don’t have to worry about initialization or anything like that, and it points the behavior to the class that we’re calling 'build object' on. So if you call 'Person.build_object', it’s going to create your new object and say, 'This is a person; it has access to those person methods'.
00:12:21.330 The last one we’ll need to build is our 'delegate' method for inheritance from a class, creating a subclass, that kind of thing. This one is probably the trickiest out of all of them, so we’ll start with a diagram. In this example, say we want to create a 'Baby' subclass. This baby subclass needs to have access to our core methods like 'add method', 'lookup', all that stuff.
00:12:45.700 But how does it go? How do we get that? Maybe we need to create it somehow. So we have to call 'build object' on something to create it, but what do we call 'build object' on? Well, just like we just saw, you call 'build object' on a class or behavior, and it links to that class, so it has access to all those methods.
00:13:09.540 You can see that arrow pointing over to it, so we want to call our 'build object' on this default behavior way up in the upper left. The problem is we don’t really have direct access to that. We’re not calling 'delegate' on that default behavior; we’re calling 'delegate' on the person, but the person has a reference to that behavior.
00:13:33.190 So what we want to do, all put together, is grab the default behavior from the person, call 'build object' on that to build a baby object, and then set up the parent link and that methods hash. You can see from the code up here we do exactly that: we get the behavior from the parent class, call 'build object' on it, and we get our new subclass.
00:13:54.210 The rest of the method is the easy part. We set the parent class in the state, we set an empty methods hash to the state so we don’t have to worry about lazy initialization, and then we return the new subclass. So this has been pretty fast, but we built our object structure and most of the methods we need.
00:14:16.380 We still need the 'send' method so we can call methods by name, but because we can call each of these things manually for now, we actually have enough to set up the kernel of our object model. So we’ll do that. When we’re done, it’s going to match this diagram: we have our default behavior, which is going to hold all of the default implementations of the methods that we just wrote.
00:14:39.330 We have a root object class, which you can think of like 'Object' in Ruby, from which all other classes in the system will ultimately inherit. So if you want something to be on all objects, you would add it as a method on the root object class. We'll build this step by step.
00:15:02.830 The first thing we need is a thing that we can start hooking all of our methods onto: that default behavior. We'll create one by calling 'behavior.delegate' because this is going to set up that methods hash and the parent link. In this case, we don’t have a parent because there are no objects in the system to inherit from, but we’ll fix that later.
00:15:17.340 The next thing is this default behavior needs behavior, right? Like, you need to be able to add methods to it, you need to be able to look up methods on it. So where is that gonna get its 'add method' and 'lookup method' from? If it’s the thing that holds those and you follow those behavior arrows to figure out which object you need, then you just cycle back around to itself.
00:15:31.570 So we’ll do that by setting the default behavior that behavior equals default behavior. The next thing we’ll do is define the root of our object tree—like I said, this is kind of like Ruby’s object class. This one doesn’t have a superclass; it’ll never have a superclass because it’s where inheritance goes to die.
00:15:54.030 So, we'll set its method to that, so we pass nil again, and it will need access to 'add' and 'lookup' and all those other methods that we’re putting on the default behavior. So we'll point over to that default one that we just created.
00:16:18.160 Okay, so we have a root object; we have our links for default behavior; we have our default behavior. We can start cooking methods onto it, but there’s still one thing missing. I said we were gonna fix up that default behavior's parent thing later on, so this is a good place to do it.
00:16:39.450 We want our default behavior to act like an object. If we define a method on Object, the default behavior should be able to use it. This is pretty simple; we’ll just set it manually for now. So, this is actually all the code that we just saw for hooking up our object model.
00:16:59.760 It’s a few lines of code: we create our default behavior, we set it to loop back to itself, we have our root object class pointing to our default behavior, and then hook that parent link back up. We’re still missing those important methods, though; you know, like 'add method', 'lookup', all that kind of stuff.
00:17:17.990 So here, we’re going to use 'add method' to attach each of those to our default behavior, so we have something that holds those default implementations to the system. And that’s pretty much it for setting up this object model. This is an example of how you might use this.
00:17:39.020 Imagine that you wanted to create a 'Greeter' class. You could call 'delegate' on the root object class that would point the Greeter class's behavior to the default one. It would point the parent link up to the root object class, and you have a class that acts like a class.
00:18:02.270 Then, if you wanted to add that 'hello' method to the Greeter class, you can call 'add method' on it. It’s gonna find the 'add method' implementation from its behavior and it's going to add that method into its state. You could call 'build object' on the Greeter class to create a Greeter, which is going to then be able to use that hello method.
00:18:23.000 Because again, you just follow those behavior lines to figure out where your methods are, and then you could set some state in your Greeter object, which could then be used by your hello method or any other methods that are defined on that Greeter class. Remember, it's all about what's shared and what's not, and that's what drives a lot of this implementation.
00:18:44.590 State is unique, so state exists on an object. Method implementations are shared. That’s why you find them through that behavior link, because many objects can all use that behavior link to connect to a single class or behavior.
00:19:03.830 But we still need to be able to call those methods. You don't want to have to pass actual objects to invoke methods; you just want to be able to say, 'Hey, I want to call a method named whatever.' For that, we need to define 'send'. 'send' uses a helper method called 'find method'.
00:19:26.220 'find method' takes an object and a method name and it returns the right code to call. Then, if it finds it, it’s going to call it with the right arguments. If it can't find it, it's going to throw an error. Pretty simple! All the complicated stuff is in 'find method'—just kidding, 'find method' is also really easy.
00:19:49.660 It calls the 'lookup' to find the right method. And this is where you get your power because that 'lookup' method could do anything. I mean, you could append a '2' to the end of all your methods before you find it and look them up that way. I don’t know why you would, but you could.
00:20:12.960 But you might have noticed a problem with this 'find method': it calls objects and calls 'find method', which means you have an infinite loop over here. You might have been able to tell from that suspicious white space that there's a little more. So, if we know that we're going to call that default implementation of lookup, we’ll hard code it.
00:20:38.420 So in this case, we say, 'Hey, if we're trying to find a lookup method on the thing that informs the kernel of our object model, just call the method manually.' That behavior lookup that we defined a few minutes back.
00:21:02.639 Okay, we’re done with setup! But before we try this out, it’ll help if you get a little more comfortable with each of these core methods. So just to quickly review: we have 'add method', which adds a method to a class; 'lookup', which will find an implementation from a method name; 'build object', which like 'new', builds an object from a class or behavior; 'delegate', which inherits from a class or behavior; and we have 'send', which calls a method by name on an object.
00:21:28.900 Now let’s try this out. The first thing we’ll do is call 'delegate' to subclass the root object. This is like in Ruby doing '< Object' when you're defining a class. You can see this is a little bit wonky because we’re calling methods manually. This is using 'object.send' to call the method by name; that method it’s calling is 'delegate', and it’s calling it on the root object class. This is going to return a subclass of the root object class, the Greeter class.
00:21:53.640 The next thing we’ll do is add a method to that class that’ll print 'Hello, world!' All of our methods take at least one parameter, which is the sender of the method. You can think of that as a way to access 'self' in Ruby.
00:22:17.490 The next thing we’ll do is create a Greeter object from the Greeter class. Using that, again, 'object.send' to call the method by name, it calls 'build object' on the Greeter class, and that’s going to return a new Greeter object that has access to all the methods that that Greeter class defines. Then, we’ll call the hello method on the Greeter object and we get our output.
00:22:41.270 So here’s what we just did: we subclassed the root object to create a Greeter class, added a hello method to it, built an object from it, and called hello on that object. We called hello, it followed that behavior line to find the hello method on the Greeter class and then called the implementation and printed out our message.
00:23:03.300 This code looks terrible, I know! We did a lot of work to create an object model that looks a little more functional than it does object-oriented. We’ve dug ourselves into a pretty deep hole of awful, awful syntax. So how are we gonna get out of it? Well, we’ll dig our way out. What could fix bad syntax except adding a whole lot more metaprogramming?
00:23:27.650 We’re going to go straight to 'method_missing'. Luckily, it doesn’t take much; this looks big—don’t try to completely get it right now! All this does is walk through our object's ancestors looking for an 'object send' method. Once it finds it, it calls it, passing our method name and arguments. It's just a fancy way of turning code like this into this—much, much nicer code.
00:23:55.020 So instead of calling 'object send hello', we can just call 'Greeter.hello'. This is going to make the rest of this much easier. Finally, we'll add 'object send' to the root object so that all of our classes in the system have at least one they can fall back to.
00:24:19.170 This might seem like cheating, and it is, but I figured the syntax is hard enough to understand without the syntax fighting you the whole way through. Now remember, objects can have their own different states! So let’s try that out and let’s write a method that accesses the object state to print out a different message depending on what kind of state is in that object.
00:24:43.720 Say we’ll create a few objects, Alice and Bob. We’ll give them names, Alice and Bob, and we’ll call that new method, 'hello_name'. You can see 'hello_name', even though it’s the same implementation, reaches into the object state to print out different messages.
00:25:02.640 Now that we have this object model, what kind of stuff can we do with it? I mean, you can define classes and add methods in C++, and that was invented like forever ago. It seems like we’ve done a lot of work and just ended up making the same kind of stuff we could always do a whole lot harder.
00:25:23.670 But remember, we’re trying to get out of this; it’s flexible method dispatch. So let’s take a look at some interesting new models that we can build on top of our basic one. What if you wanted to log all of your method calls to an object? Like, every time you make a method call, you want to print some information about that method call.
00:25:44.000 That’s not that hard to do! We can write a new lookup method that uses the old lookup method to find a method, wraps it in some function that can do anything, and then returns that function so that any method we call goes through that interceptor function on the way through.
00:26:04.360 A method that wraps another method? You know, we’ll call that interceptor method. It might look like this: you pass it a method name, a method implementation, and some arguments. It prints out some stuff and calls the method as part of its implementation.
00:26:30.190 So we can do the work that we were planning to do. Now, if you want to override a method on a class, you subclass it and then override the method. It works the same way on a behavior. If you want to override one of those core methods, you can just subclass that default behavior, add a method implementation, and point the new behavior to it.
00:26:53.840 So now, like I said, if you follow those behavior lines, you should be able to see that the person class and anything that comes from the person class is going to use that new lookup method, the intercepting behavior, to do our intercepting stuff before the method is called.
00:27:19.210 Here’s what this looks like in code: first we’ll use 'delegate' to inherit from the default behavior to create our intercepting behavior. Then, any methods that we define on that intercepting behavior are going to override anything on our default behavior for anything that points to that behavior.
00:27:42.680 This is our new lookup method, again it’s big, so we’ll take it in pieces. First, we need to be able to find our old lookup method, so we need something that acts kind of like 'super'. So how do you find that old version? If sender is our person class, like we’re calling lookup on our person class, you can once again just kind of try to navigate to find that lookup method on our default behavior.
00:28:05.140 So if we want the old version of that method, that’s centered up behavior to get to our intercepting behavior, you go through the parent link to find the lookup method to find out its parent, and it'll find it that way.
00:28:26.000 You can see it grabs the super behavior from the sender behavior, about state parent, it grabs the lookup method from that super behavior, and then it calls that old lookup method to grab our original implementation.
00:28:47.950 If we find a method, we'll wrap it in a new interceptor function. You define that function so you can do whatever you want in it; you can log method calls or do something totally different. Then we pass some details, like the method name, the original method so we can call it, and the arguments so we can invoke it.
00:29:09.930 Then we’ll create a person class that uses that new behavior. Just like in Ruby, our person class inherits from the root object class. Again, this is like saying 'Person < Object'. Next, we’ll manually point the person class to our new intercepting behavior.
00:29:35.420 This is kind of cool. So take a look at what’s happening here with a single line of code. We’ve changed how method lookup works for anything created by that class. You see, using the old method is the new method, is the old method, is the new method.
00:29:58.720 So here’s that logging function again. Same thing as before. In the last line, we make it the interceptor method for that class. We’ll define a few methods on this class, 'name' and 'location', so we can test it out.
00:30:22.490 Then we’ll create a person object using 'build object' on the person class and call those two methods. There’s our logging! Now, what happens if you reset the intercepting class's behavior back to the default one?
00:30:45.900 Check it out—logging goes off! So it’s not only that you can change fundamental ways of finding and calling methods; you can change it on the fly. You can change it at runtime just by pointing to a different behavior.
00:31:07.550 One more quick intercepting example: what if you have a method that sometimes randomly fails? I mean, I know nobody in this room has that problem, but I hear of developers that have this problem—methods randomly failing sometimes. Here’s one that fails two-thirds of the time. What if you could automatically retry this method until it succeeds?
00:31:27.630 Here’s an interceptor function that does just that! It’s going to try to call the method every time it returns false, which we’ll consider a failure, and it’s going to sleep a little bit until the method succeeds.
00:31:49.860 What happens when you try it? Hey, it works eventually! Okay, now we're going to move on to something completely different. But before we do, let's take a second to let this intercepting method stuff drop out of our heads so that we’re ready for the next one.
00:32:13.350 Take a sec. All right, all good! Let’s go! Last example: we’re going to build multiple inheritance where a class can have more than one parent class. If you can’t find a method on your class, it’ll search all of the parents to find a method implementation.
00:32:34.560 So let’s draw this out again. I always find it easiest to start with diagrams. We’ll start with this object—in this example, the very well-named 'Object'—this object has a class, and this class will have multiple parents.
00:32:59.570 So how would you build something like that? A class with multiple parents will look just like a normal class. It still has the methods and state, but instead of a parent, it has a parents array. Those parents could be anything—they can be normal classes, interceptor classes, or basically whatever.
00:33:25.680 In order to get those multiple parents, we need to be able to add parents to that parent list. So where's it going to get that? I mean, when we said that any of these objects are going to be able to get their methods from their behavior, but there’s no add parent behavior, except for that suspicious-looking blank space in the lower right hand corner.
00:33:49.610 So I’m going to fill that. Since methods come from a behavior, we need a behavior that can do two things: we need it to be able to add a parent to that class, and we need a way to look up a method for all the parents in that class, which our original implementation doesn’t do.
00:34:12.430 We’ll write an 'add parent' method and will override the lookup method with a new one. Once again, you subclass the default behavior to get your new one, and then we’ll write the 'add parent' method. This one looks big, but that whole first section there is just for initializing it.
00:34:31.800 If we don’t already have a parents array, at the end it appends the new parent to that parents array. So you’ll end up with your original parent plus a new one, and then any time you call this afterward, you’re just going to end up appending the new one to the end of the array.
00:34:51.970 Now here’s the new lookup method. This is almost exactly the same as our original one, with just one change: instead of looking up methods in a single parent, it’s going to loop through all the parents and it’s gonna break when it finds the first one.
00:35:13.010 So let’s try this out. Here are two normal classes: a person class and a greeter class. Nothing special about them; they just inherit from our root object, the same as anything else would. Then we’re going to define 'name' on the person class and 'greeting' on the greeter class.
00:35:33.130 Here’s a class at the top that inherits from both the person and the greeter classes. To get that 'add parent' method, we need to point the behavior to the new one; otherwise, that method isn’t going to exist. So this inherits from the person class and then adds the greeter class as a second parent to the social person class.
00:35:57.320 Now if we try it out: it fails? Just kidding! It gets both the name and the greeting method from both of its parents. So it gets the name method from the person ancestor and the greeting method from the greeter ancestor.
00:36:09.780 This is a lot of flexibility! We built three different styles of objects: basic single inheritance, two kinds of method interception, and multiple inheritance. And it all came from a class and five methods: 'lookup', 'add method', 'build object', 'delegate', and 'send'.
00:36:24.860 And our little tiny object; each piece of it is simple enough that you could build it in almost any language from C all the way to Ruby. But Ruby is also a really flexible language! I mean, you could write a lot of these examples straight in Ruby using things like modules and 'method missing' and basic objects.
00:36:42.290 So what’s the difference between this object model and Ruby's? I see it as intent. Just like how Ruby makes metaprogramming a normal thing to do, this object model makes changing things at a deeper level a totally normal thing to do.
00:37:00.060 Even though all of this eventually becomes a bunch of binary code running on a processor in our machines, the language you use and what’s normal inside it changes your brain. It makes some things more natural and other things more difficult.
00:37:17.310 That’s why I love playing like this. That’s why I love learning new languages, even if they don’t stick with me. These experiments help you think in a totally different way, you try on a new way to see your code, and you keep what works and you drop what doesn’t.
00:37:34.650 So after these examples, I can see some of you looking at me like, 'Why? Why would you do something like this?' How many of you learn best when you break something? You test the boundary when you're not sure it’s gonna work—when you’re pretty sure it’s not going to work?
00:37:51.650 I strongly believe that the best way to understand the system is to test it, to push it to break it, and metaprogramming is a fantastic way to break all of the systems that you depend on.
00:38:04.150 When you push the boundaries, you’ll be more confident with a language. You'll understand at a deeper level how it all works, so try writing code that might seem weird or irrational, because who knows, you’ll probably learn something new.
00:38:22.380 At the very least, you’ll have a good story to tell! It could be a mess of metaprogramming spaghetti code that generates Active Record models on the fly based on entries in a config file. That was a bad idea—don’t do that.
00:38:35.640 But it could be an entire object model built from five methods in a class that helps you understand how object-oriented programming actually works. And when you learn something like that, when you learn a tool that you can use to investigate more tools, that’s a platform you can use to learn new things.
00:39:01.660 By building them! Like building multiple inheritance or building prototype object cloning or building pattern matching. And when that happens, not only do you learn new stuff, but you also increase your rate of learning new stuff. I really can’t think of any better way to become a better developer.
00:39:18.700 So what I’m asking of you is to give this a try! Take a look at the sample code I’m about to give you and try to understand it—not by reading it, but by playing with it, by trying to maybe re-implement these examples or take it a step further or build something totally new.
00:39:36.140 If you’re anything like me, as I was several times when putting this together, you’re gonna fail miserably a few times—well, a few more than a few times! But eventually, it’ll click, and you’ll get it. When you're done, you’ll understand a lot more about metaprogramming and those deep levels of object models that you never understood before.
00:39:51.610 If you want to get in touch about that or want to talk programming or really anything else, I would love to talk with you! My email address is up there—use it! I read and respond to everything.
00:40:07.780 If you’re ever in Seattle, let me know! I would love to grab a coffee with you and chat about programming. I’m also on Twitter at Justin Weiss where I share programming and other tech-related articles. That last link points to a list of resources for this talk.
00:40:25.260 It’s an important one; you can think of it as like the show notes. It has just the sample code with some stuff I couldn’t fit in, like prototype object cloning, and it also includes the slides for the talk and some other articles that are kind of in the same vein.
00:40:42.880 And I know I have some time—about ten minutes—so I’m happy to answer any questions or go deeper into any of this stuff as you’re interested.
00:41:03.150 Yeah, the first time I thought about this? So the question was have I thought about using things like 'define method' and 'method missing' to try to override Ruby's method implementation with something more flexible like this?
00:41:19.940 When I first read the paper, I was so fascinated by it. I decided to try that and then I had second thoughts when I realized just how awful the performance was likely going to be, if it’s not built into the language.
00:41:35.600 So the question was, how do other languages compare with Ruby in terms of metaprogrammability? There’s a huge spectrum and it’s also hard to tell because it’s not until you really start to get deep into a language that you can really start to make use of it.
00:41:51.280 For instance, my language for this year is Elixir, and Elixir has a lot of heavy macro support, which can be used for some really interesting metaprogramming stuff.
00:42:06.080 But I haven’t gotten to a point where I’ve been using it enough to really be able to have a solid opinion on it. Definitely, I think the most metaprogramming-ish languages that I’ve run into over the years have been surprisingly Objective-C.
00:42:19.390 I actually tried giving this talk using some of that and Lisp. But yeah, those are probably the two biggest. All right, well thank you once again for the time!
Explore all talks recorded at RubyConf 2016
+78