 This talk is called Building a Better OpenStruct, and that's what we're going to talk about. But I do have to say it's a little bit scary to come up on the stage to be kind of the sequel to Matz. So I want to also introduce something big to the Ruby community. I call it Matawan Swavin. It's short for Matz's talk, was nice, so this talk will also be nice. So I'm going to do my best to give also a nice talk, Matawan Swavin. Okay, so let's talk about OpenStruct. Before we can talk about what it means to build a better OpenStruct, we have to know what OpenStruct is. How does it work? What are the problems with it, and why would we want to build a better one? I like to think of OpenStruct as Ruby's JavaScript object. It's funny. The initial commit on the Ruby repo goes back to 98, so not quite to the beginning of Ruby, but OpenStruct was already there, and it was described as a Python object. But at this point, I like to think of it as a JavaScript object, and you'll see what I mean in just a sec. You can require OStruct anywhere in your code. It's just part of Ruby's standard library, so it's always there. And when you initialize an OpenStruct, you can either initialize it raw, just openstruct.new, or what you'll do most of the time is you'll initialize it with a hash. So in this case, we're initializing it with a hash, with a key of foo, and a value of bar. In this case, both symbols. And generally, keys have to be either strings or symbols. Once it's initialized, it's not too late. You can still add properties. So in this case, we actually have two ways to add properties. We can use dot notation, like dot-bas equals four. Or we can use bracket notation. In this case, we're setting a key of the symbol something to the string of whatever. Once you have all your information in your OpenStruct, you can start getting properties out. So you can either use dot notation. Here we can see it doesn't matter when you put the information in. It's going to be available. And if you try to access a key, you'll see at the bottom, we're accessing a key that's not there as part of the attributes that you've added, it'll just return nil. And it doesn't matter whether you use dot notation or you use bracket notation. Again, with either symbols or strings, it all works fine. So you have an object with arbitrary attributes. You can use dot notation or bracket notation to add and retrieve attributes. That sounds pretty much like a JavaScript object. So that's why I like to think of it that way. Now that's really nice, but why would we use it? So Eric Michaels-Ober gave a really great talk at Rails Israel last year where he listed three common use cases of OpenStruct. And the code examples that we're going to use are my code examples, but the list is his, and I have to give him credit for identifying these three cases. The first and the one that we're really going to focus on in this talk is consuming an API. The sort of more straightforward way that you might do this is you make a call to an API, you get some JSON back, parse out that JSON into a Ruby hash, and then you pass that hash into an OpenStruct. A more complex pattern that you might follow, and we certainly do this a lot in our apps, is you actually subclass OpenStruct, and then create a new instance of your subclass with that hash. So it comes out to pretty much the same thing, but the nice thing about it is that now you can actually put a name to the kind of object you're creating. So let's say you hit the Twitter API, you get back some kind of a hash, you feed that into your OpenStruct subclass, and now you have a tweet object. So it makes it much easier to work within the console to debug stuff. It's much nicer that way. Just to flesh it out more, if the API response looks like that, we have a couple keys, a couple of values. If you were just to leave it as a hash, you would have to use bracket notation. But because we put it into an OpenStruct, we can use dot notation. And it seems like a pretty subtle distinction, like OK, why do we really care? But now you're actually starting to think of your information as not just raw data, not just hashes with keys and values, but as an object. And if you use that more advanced version that I mentioned, you can do this, which is super cool. So here we have a user class that inherits from OpenStruct, and it has a name method. We actually can start defining our own methods, which interact with the data that was passed in. So in this case, we make an API call to slash user slash one, which for some reason I guess I'm the first user, Ariel Kaplan. It gives back the first name and the last name, and we feed that into a user, and then we can call dot name. And what's cool is that if we call dot first name, it would just give us a raw value. If we call dot name, it'll give us a computed property. And from the outside, we don't know which is which. And that's basically the point of objects, is abstraction and encapsulation, and that's a really nice way to get that from an API call. OK, so that's all case number one consuming an API. Common use case number two is a configuration object. Let's say that you made a gem, which requires some configuration, you want to have a DSL like this. So you call dot configure, you get a block, which takes a configuration argument, and then you'll start setting settings on your configuration. This is all the code that you need to do it. The configure method on your gem is just going to yield your configuration object, which is an open struct. And since open struct has that really nice property where it lets you just set and get random keys, so it just works. It's really simple, and it's built in. Third use case, it's a little complicated, so if it goes over your head, don't worry too much about it, but it's good to know about, is a test double. So in this case, we have an order class where it takes a payment gateway and some products, sums up the total cost of the products, and charges the payment gateway. Well, if you're actually going to feed it a real payment gateway and put in someone's credit card information, your tests are going to be really expensive. And I'm not talking in terms of hardware, I'm talking in terms of your credit card is going to get charged every time you run your suite. So you don't want to do that. So instead, what you're going to do is put in something that's going to quack like a payment gateway, but not actually charge a credit card. And it turns out open struct is a really easy way to do this, because every key value pair that you put into your open struct becomes a method and its return value. So in this case, we have an open struct which has a key of charge and a value of the symbol paid. Every time you call dot charge on that open struct, it's just going to return paid. So it's a really simple, easy to use test double. We can inject it into our order class, assert that something's returned. We can also, if we want for some reason, to assert that something is set on the open struct. But again, the basic point is is that open struct, because it has the charge method now, it's a really easy to use test double. OK, so that's why you might want to use open struct. But how does it work? Let's peel back the surface and look at the code. And before I move on, I have to note, just to be honest, the code has been brutally edited in this presentation. And it's not because I want to lie to you. It's because this code has a lot of little details, edge cases, and error handling, and everything that I'm going to show you in this presentation. That's the way the code works. So I've tried to pare it down to just the code that you need in order to very, very quickly understand how these things are working. But it's all open source, and you can all check it out later and find out what I hid from you. So it has to work. Well, under the hood, open struct defines attributes, setter, and getter methods on the object's singleton class. Now, I know that not everyone is necessarily familiar with singleton classes, or you might be a little fuzzy on it. So here's a quick refresher. In Ruby, if you have an object, in this case, we'll call it foo, but it could be any object of any class. You can define a method that exists just for that object. So in this case, we define a dot bar method just for the foo object we can call it. And if we call foo.singleton methods, we'll see that the singleton method bar has been defined on foo. So the reason we talk about a singleton class is that technically that method doesn't live on the object itself. It lives in the secret class that every Ruby object has that exists just for that one object. Because methods live in classes and not in objects. But if that's too much to remember, the basic point is that a singleton method is a method just for one single Ruby object, one single object in your program, and nothing else has access to it. So let's look at what happens when you create a new open struct. In this case, we pass in a key of foo and a value of bar. We're going to hit initialize, and this is what initialize looks like. The first thing that we do is we create an at table instance variable, and we set it to a hash. That table, you want to follow it very closely, because that's where all of our information is going to be stored inside of our open struct. Now, assuming that you've passed in a hash as an argument, which in this case we have, we take every key value pair, coerce the key to a symbol, we set that key value pair inside of our at table hash, and then we call newOstruct member. And I will show you what that is, but you're going to have to wait just another minute, because I want to talk about something else first. What happens when you already have an open struct, and then you set a new property using dot notation, dot baz equals four? The first thing to remember is that in Ruby, that's just in tactic sugar for calling the baz equals method with an argument of four. There is no baz equals method, so we're going to hit method missing. The first thing it's going to do is convert that baz equals to a string. We're going to say, OK, what is the nature of this method? It's going to be either a setter or a getter. If it's a setter like we have right here, so we chop off that last character, which is an equal sign, we're left with just baz. And then we call newOstruct member on baz. We get back the symbol, baz, and then we're going to set baz to four inside of our internal hash. The other option is that it's a getter, like dot baz. So all we do is we pull the value for baz out of our internal table. Now, I said we're going to talk about newOstruct member, and this is where that happens. But remember that it's called during initialize and during method missing. Here's what it looks like, and this is kind of where the real magic of open struct happens. So the first thing that we do is we just coerce the name to a symbol, and then we say, OK, let's say we call this with baz. Do we respond to the baz method already? If so, then we're done. We don't want to overwrite any methods. But if there's no baz method yet, then we're going to start defining methods. So the first thing that we do is we define a baz method, which just gets the value for baz out of the table. And the next thing that we do is we define a baz equals method, which sets the value of baz to the argument within the table. OK, so it's kind of like add a reader and add a writer, just that instead of what we'd normally do, which is write to an instance variable, instead we're using that internal at table hash. What you might have figured out from this is that no two open structs share a set of methods, even if they look exactly the same from the outside. But those methods are being defined every time for each individual open struct object. Now, here's the problem. Open struct is slow. And you might be wondering how slow is open struct. So I had my Madiwan's Twavin, however it was. Matt's has animals, so I have animals. Here are a bunch of snails. So my snail friends will tell you that it's about, depending on your use case, 10 to 40 times slower than an explicitly defined class. And that seems really, really bad. That's terrible performance. Why is that happening? So I would say that there are two and a half reasons why that happens. Reason number one, and this is the main reason, defining methods is slow. In Ruby, when we define a method, it just takes a long time. Number two, as you've seen, we're going to rely on method missing a good deal. Method missing is also pretty slow. Not as slow as defining methods, but it's a contributing factor. And before I tell you the third reason, or the two and a half reason, let me do a quick survey. How many of you have heard, just by show of hands, in the last, let's say, a year and a half, two years, don't use open struct because it's going to invalidate your global method cache? All right, we got a good number of hands here. You've been lied to. It's not true anymore. In Ruby 2.1, this is fixed. It's no longer a problem. And this bothered me because I see a lot of misinformation out there about open struct, about this particular issue. It's not a problem anymore in 2.1 and above. But why that is, is actually kind of an interesting story. So I want to delve into that for a second. And we have to talk about how method lookup in Ruby works. So let's say we have this object graph. We have an animal class, which is just a standard class. It has dog and cat, which inherit from it. And meowable is a module included on cat. Now we're going to create a new cat object, and then we're going to call 2s on cat, the 2s method. Well, where does that exist? Initially, we don't know. We have to figure it out. So Ruby is going to actually start hunting around in the class and its ancestors and include modules. We found it in kernel. So we're going to mark, OK, in the future, any time we call 2s on a cat instance, we know that that is defined in kernel. We know how we can execute that method. So let's not look it up next time. But what if you reopen the cat class and you define 2s? Well, that information is no longer true. So because of that problem, every time you reopen a class in Ruby, that happens. The global method cache is blown away because it's a simplest way to say, we don't know what to rely on anymore. Now the problem is, every time that you use an open struct, you're defining methods. And that happens. So our global method cache has been busted every single time that we created a new open struct. And it turns out that looking up methods is actually really expensive. And if you're wondering why, look no further than active record. Every method you look up an active record the first time, you have to look through all those places, all those classes, and all those modules in order to find where that method is. And that method probably defines other methods, which you then have to go through that same series of events again. So it's really expensive. James Golic was a New York City Rubyist. He unfortunately passed away about two years ago very suddenly in a car accident. And this is one of the big things that he did for the Ruby community, so I like to talk about it because it's part of his legacy. So he did a little bit of profiling. He found that 10% of the time spent in his Rails application was just rebuilding the global method cache. It was reset 20 times on a request because of all these method lookups. And open struct was, of course, one big culprit. So he said, look, I could complain about it. I could go on Twitter and rant about it. But why don't I just fix it? So he did. He implemented what's called a hierarchical method cache. And we'll see how it works. But he actually wrote it for 1.9. It was not included in Ruby Core until 2.1. But from 2.1 and on, this is no longer a problem. So I'll show you how it works. We effectively have one method cache per class in our Ruby program. So we have an animal method cache and a cat method cache, and there are links between them. So that if we invalidate animal, then it's possible that some of the information we have about cat is also incorrect, so we also invalidate that. But let's say I reopen dog and define a method there. Well, dog is gone. But cat's still fine. There's no reason why a defining method on dog would ever affect cat. So we only invalidate the cache with respect to the classes that would be affected by it. And the same thing is true with open struct now. When you add methods to your open struct, we just invalidate that open struct cache. But everything else in Ruby is totally fine. That's a blog post where you can see all the details. It's actually a really interesting read, all kinds of cool stuff about monotonic sequences. And anyway, check it out. But the bottom line is, again, let's bust that myth. Ruby 2.1 and above, we can absolutely create open structs without slowing down the rest of our application. But there's still that little problem that open struct itself is kind of slow. So hi, everybody. Don't worry, the intro was supposed to take this long. It's fine. My name is Ariel Kaplan. You can find me online at amkaplan. I tweet about mostly programming, but increasingly about software teams and what it means to write code. I also have my GitHub profile there where you can find much of the source code for this presentation. And on amkaplan.ninja, I'll post the slides after the talk. I really like coming to conferences because I get to meet new people and hear new perspectives and ideas and just geek out. So I really want to meet all of you. And I want you to say hi after the presentation. And just to bribe you, I brought a lot of chocolate that I want to give out. And it's not just chocolate. It's actually popping chocolate. So come over and say hi after the presentation and get some sweet treats. Even if you don't like chocolate, I just don't want to meet all of you. I work for Vitals. We pay people to use high-quality, low-cost health care. Basically, we're sending people checks in the mail for going to their doctors. So that's kind of a fun business. And in order to get all this information, it's really a lot of number crunching. And so we use a system of microservices and external APIs communicating via JSON, parsing to Ruby hashes, which are fed into open structs. So essentially, in our whole system, open struct is mediating the communication between our various services. Now we had some performance problems. I did some profiling, and I found, oh, open struct. Yeah, big problem. Now I know people are afraid of profiling, so I just want to quickly show you what I had to do. This slide is basically all that I had to do in order to get profiling information. So I use the StackProf gem. I find it to be really nice. What you do is you call the run method on StackProf. You give it a mode, which will be either CPU if you want to test how long things take, or object if you want to check object allocations. You have memory issues. And then you just give it an out file, a place to dump the information. And then you just run the block that you want to profile. So that's all it takes. Then you have that file, which you can't really read. But if you use the StackProf command line executable, just give it the name of the file and call dash, dash, text, boom. You have all this information about what's going on in your app and what's taking the most time. So yeah, open struct. It was taking a lot of time. Here you see it was 17% of the time. But this is not even all the information, because there's a long tail there. So it ended up being about 20% total of our app time, at least the CPU time, was just spent on open struct. And that was not acceptable. But as an aside, congratulations. Now you know how to profile. So there you go. So we wanted to keep open struct, because it was really nice. It was really a flexible interface. It let us change things upstream, have access to them downstream. It was very pleasant to work with thing, but it was so slow. And so the question became, can we have our cake and eat it too? Can we build a better open struct? Nope. So I'm sorry that you're all bored. I'm going to provide this talk. You're going to get a little bit early today. Enjoy the chocolate. I'm just playing with you. This talk would not have been accepted, I hope, if the answer was no. Yeah, we can absolutely build a better open struct. And I'm going to tell you four stories, or really four more stories, because what James Gullock did in terms of method caching is sort of like the zero width story. Because it's not open struct specific, but it is a story of how someone basically fixed stuff because of open struct. But we have four more stories to share with you today. So let's get to it. Round one is open fast struct. So I still remember opening up my Ruby Weekly email. This was around the time we were having these issues, and I was trying to figure out what we were going to do about open struct. And I saw this. Open fast struct. A faster open struct. And that was really exciting. Like, that looks really cool. Let's follow the link, check it out. So it says, yeah, this is the GitHub repo. It says it's still slower than a hash, but four times faster than open struct, which is a significant improvement. And it basically works the same. So I said, let's open up the code. Let's see how it works. Now, again, it's supposed to be the same interface. So let's talk about this is going to look the same, right? Just instead of open struct, it's open fast struct. So what happens when we call open fast struct.new, and we pass in a hash with, in this case, we'll just keep it simple, single key value pair. We hit initialize, which first defines an at members hash, which is actually, it's the same as the add table. I guess Arturo Herrero, who created this, liked the name members better than table. Maybe it's more specific. And then we call update with the arguments, with our hash. Update just basically takes each key value pair and calls a sign, which converts the key to a symbol and puts it into our members hash. So if you kind of do the math, you add this all up, it basically comes out, it's the same thing that open struct is doing, minus new O struct members. So we're not defining any methods. Well, if we don't define any methods, what's going to happen when we start trying to access stuff or assign things? We're going to hit method missing. And this method missing is kind of wacky. So I'll walk you through it. But it's trying to deal with not two, but three separate possible situations. The first situation is, let's say we're calling method missing and we're trying to get a value out of our open fast struct. It's going to call fetch on our at members hash. And fetch is a method which has, it has different behavior depending on what arguments you give it. But if you give it a single argument and a block, as we're doing here, it will first check, is this a key in the hash? If so, return the value. If not, run that block as a kind of a default result. So first we say, OK, is Baz a key in the hash? If so, then we're done. If not, run the block. And the block has to decide, is this a setter or a getter? If it's a setter, if the last character of the method name is an equal sign. So just take the rest, the beginning of that method. If it's a Baz equals method, so take the Baz part and assign Baz to the first argument, which in this case is going to be four. So what's left is when we're trying to, it's a getter, but the key doesn't exist in the hash. And this is kind of interesting behavior here. What he does is, or what it does, I guess, is it assigns to that key of Baz a new instance of open fast struct. So where open struct would just return nil here, it's going to assign a new open fast struct. So that was an interesting design decision. The main thing to remember is that open fast struct does not pay the upfront cost of defining methods. Like I said, it costs a lot to define a method. And it's really only worth it if you're going to get that debt repaid. But if you're only going to access the properties once, then it doesn't really matter. Method missing is still going to be much, much cheaper. So that's an idea of open fast struct in there. With just one little problem, it broke our app because of the different behavior that I alluded to earlier. But I want to understand why Arturo Herrero made that design decision. So if we go back to the documentation, it actually says outright there are a few differences between open struct and open fast struct. And if you look at the last one, it says it allows infinite chaining of attributes. And there's a link to an example. So the example says that open fast struct is a black hole object, which supports infinite chaining of attributes. And we have an example of that. We create an open fast struct called person. And then we call person.address.number equals 4. And indeed, we can retrieve person.address.number. And that's kind of nifty. So how's it working? Well, again, when you create an open struct and you try to call a property that does not exist, you just get nil back. But if it's an open fast struct and you try to get a property that doesn't exist, then it's going to give you back a new open fast struct. So when we call person.address, we get a new open fast struct. And then we can call .number equals 4 on that. So that's really nice. The problem is in our app, we did a lot of this. So let's say our API would sometimes leave out a key if the value would be nil. So when we call open struct.foo, and there's no foo key, then it's just going to return nil. And that's going to be a full c value. If it's an open fast struct, we call .foo. It's truthy. So our conditionals were all flipped. Turns out that's not very good for your app working. So my point here is that if you're going to create a drop in replacement, it should just be a drop in replacement. It should be the same thing, but faster. Faster, but also the same thing. And if you break the API, that's going to create certain barriers to adoption. And for us, it was to the point that it would have been easier to monkey patch open fast struct, not to do this, than it would have been to rewrite our app. So the question became, can we do better? Can we have something that's closer to the actual open struct API, and still is performant? So I thought about it, and I came up with an idea, and I called it persistent open struct. I want to show you how it works on the outside, and then we'll dive into how it works on the inside. This is specifically designed for the use case that I spoke about, the advanced version of API consumption. So you have your class, which inherits from open struct, in this case. So you're going to inherit from persistent open struct. We're basically just substituting in persistent open struct. Then you can define methods. In this case, we have a speak method, which relies on type and sound. And then we can go off to the races. So we create a new animal with some information. This is our dog. We can call that dot speak method, and we see that type and sound are properly used. And we can set properties with dot notation, with bracket notation, and it all works fine. But when we look at the instance methods that are defined on animal, meaning the methods that are accessible to every instance of the animal class, we start seeing type equals and type, sound equals, ears, nose, tail. We have all of these methods already defined, ready to go for every instance of the animal class. How's that working? Well, it turned out it did not take a lot of code to make this happen. I literally copied over the open struct code, and I changed two lines. And just to make it pretty obvious, I'm going to underline it. So instead of defining singleton methods, we're basically just reaching out to the class and defining standard instance methods. Otherwise, it's working exactly the same. All we're doing is skipping that stage of method definitions. So it's kind of a terrible hack, because you have instances of your class changing the way that the class works in the middle of runtime. It might have security implications also, by the way. If you ever had this facing user input, they could start passing in random keys, and then eventually you would run out of memory, because you're defining all these methods, and your server crashes. So never for user input. This is only for trusted APIs. But user-responsibly, which I think we are doing, it made our app 10% faster, which I thought was pretty good for a two-line code change. If you look at just the solutions themselves, so in this benchmark, which I'm not even showing you honestly, but you can look it up on the persistent open struct repo, in the use case that we're looking at, persistent open struct is about five times faster than a standard open struct. It's still about five times slower than a regular class, but that is a much, much closer margin. And I noticed what some of you were thinking, like, OK, that's nice, looks cool, but I want to see math. I want to see incontrovertible, absolute proof of the supreme superiority of persistent open struct over open struct, and I will give that to you. I will give you the information that you crave using big O notation. Let's define our terms. n is going to be the number of methods that you have to define, which is just two times the number of attributes, right, one getter, one setter, a piece. O is going to be the number of objects that you want to create. If you use persistent open struct, it is an O of n definition, or sorry, an O of n operation, because you only have to define all those methods once. It doesn't matter if you create a million objects, you're just defining those methods once. If you use open struct, it's O no, because that's the sound that you make when you realize that open struct is killing your application's performance. OK, so the goal here was, OK, honestly, the goal was to make a terrible pun, but there is also a serious point here, which is that that doesn't tell you anything, right? Math is a really useful framework for thinking about problems in the abstract, for considering various solutions and figuring out where you should put your efforts. But we're engineers, we work with real apps, and we have to have answers that are grounded in reality, and that means benchmarks. If you're writing a library or a gem, benchmark it. You're writing an app. All of you are presumably writing apps, otherwise I'm not sure what you do for a living. So benchmark your apps. Have running benchmarks which are checked into source control, and you can follow them over time, make sure you're not having performance regressions. Just benchmark everything. And I know a lot of you are thinking, OK, Arielle, that's really nice advice, but how do I benchmark? That sounds hard. Nope, it's not hard. This is all that you need to know, this one slide. So I like to use a benchmark IPS gem. There's a lot of really nice benchmarking tools even in the RubyStandard library, but this gem is just beautiful. Also written by Evan Phoenix, who is one of the organizers here. So if you have used this in the past, you can go think him. So what you do is you just call benchmark.ips. You pass it a block which takes an argument of x. I don't know what x is. I just know that you have to call all the methods on it. So for every test case, you call x.report. You give it just a string that labels your test case, and then the block that you want to check gets performance characteristics. And at the end, if you want, you can call x.compare. And then you get this beautiful output. So what it does is it'll run your code for a short warm-up period, and then it'll run the code each block that you gave it for five seconds as many times as it can, and it'll tell you how many times was I able to get this done in those five seconds. So in this case, we see that our new code is 1.28 times slower than the old code. And it looks like a performance regression, so you might want to check that out. So now you know how to benchmark. That wasn't too bad. So congratulations. Back to our topic now, persistent open struct. So it's really good when you're repeatedly creating data objects with the same shape, like in the case of API consumption. This would be terrible, or at least overkill, for any of the other use cases that we mentioned. But in the case of API consumption, it's really, really good. Round three, open struct. Turns out, open struct actually still has a few surprises up its sleeve. So Eric Michael's over, wakes up one day, and tweets out this thing. Hey, I just made open struct initialization 10 times faster. Cool. So that sounds actually really cool. So how did he do it? How much code did he have to write? Not very much. It was actually negative code, believe it or not. He got rid of the new old struct member and initialized. So essentially, the idea is that let's hedge our bets. Why are we paying down that upfront cost of defining the methods when we don't even know if we're ever going to use them? Let's say I'm going to call out to the GitHub API just to get, I don't know, the name of a repo. It's going to give me 20 different keys, and I'm only going to use one of them. So why would I bother paying the cost of defining the other 39 methods that I'm not going to use? So just don't. And as we see that we need the methods, we define them. This was introduced in Ruby 2.3. It's just part of Ruby now. And it's a really dramatic improvement when some keys are never accessed, because defining methods is the slowest thing of all this. So it's nice to skip it. Round four. So I wrote a persistent open struct, and I waited about a year, and I just kept having this nagging feeling. I think I could have done better. I was very bound by the open struct way of thinking. And I said, I think if I just try to re-engineer it from scratch, I can make something better. So finally I found some time, and I got the motivation, and I just did it. And that's where dynamic class came from. This slide is hopefully going to look very, very familiar. The major difference is that, unlike persistent open struct, here you actually call dynamicclass.new and you give it a block. So if you ever work with struct or you've worked with delegate class, there are some tools in Ruby that work like this. It's a class factory. So when you call .new and you give it a block, it creates a new class for you, and then it'll run the block in the context of that class. So you can do anything in there that you would do in a standard Ruby class declaration. Now the rest of this is going to look really similar. We can initialize a new instance, give it all the attributes, and all those instance methods, all those getters and setters are defined. So pretty close to persistent open struct, but the implementation is totally different. The first thing that you have to see is that on the class level, we have two methods. We have an attribute method, which just holds onto a set. A set is kind of like an array, but you can look up much more quickly whether something's a member or not. That's probably its own talk. It's actually pretty cool. So we have our set of attributes that we've defined on our class. And then we have the add methods method. And what add methods does is it just calls add a writer and add a reader, just like a regular Ruby class. And of course, it just checks if you want to write your own add writers and add readers, so we're not going to step on your toes. But otherwise, we just call add a writer and add a reader, and then we mark that attribute as having already been added. We're going to just add that key to the attribute set. So keep that in mind. What happens when we create a new animal? Again, animals are a dynamic class, example, with a key of bads and a value of four. So this is how initialized looks. It's very different from what you've seen before. So for every set of attributes that you're passing in, it actually will send the attribute or the key equals method with an argument of value. So in this case, it's basically the same as creating a new animal, and then you call dot baz equals four. Well, there is no baz equals methods. We're going to hit method missing. Here's how method missing works. That reg x is scary. I know, but just trust me that it works. It's testing. Is this a setter? So if it is a setter, then we're going to call the bracket equals method with our key and our value, so baz and four. Otherwise, it must be a getter. So we call the brackets method. So like dot baz, we're just going to call bracket and then baz in the middle of the brackets. Here's how those methods look. So the brackets method is pretty straightforward. It's just an instance variable get. We're getting whatever at baz is inside of our instance. And the bracket equals method is where the magic starts to happen. We coerce the key to a symbol. We do an instance variable set. We're setting, in this case, at baz to four. We're setting our key to our value. Again, just standard instance variables. And then we say, OK, if we got here, it's possible that we have to define a new attribute. So do we already have that attribute defined? And if not, then we just add those methods. That's what we saw before, that add methods method on the previous slide. And what's cool is that the next time that you create an instance, those methods are already defined. So in initialize, when we call baz equals, we're just going to have that baz equals method. And it works just like a standard adder writer. So we no longer are relying on this internal hash table. We're just using instance variables. It is a regular class. Just it's dynamic, because on the fly we're defining the adder writers and adder readers that we need. And that's actually a lot faster. It's 40% faster than persistent open struct. It's eight times faster than open struct. And it's still about three times slower than a regular class. But it's close enough at this point, I think, that you can justify using it in your production apps unless they're really, really performant sensitive. It's good for the same purposes as persistent open struct, but it's faster. And to me, what's really important is that it works the way that persistent open struct really always should have. When I wrote persistent open struct, I was very, very focused on the open struct way of doing things. And when I freed myself of that restriction, I realized that there was just a better way to do this and basically keep the API, but have a different underlying way of how it works. OK, let's start to wrap up. Persistent open struct made me happy. It was a really good learning experience. And it also worked really well. Dynamic class made me happier, because I think it was just a much cleaner solution. And to me, that's the bottom line. Maybe people are going to see this talk and start using it in their apps, and that would be great. I'm really happy to help out people. But even if that doesn't happen, even if I'm the only one who ever uses this, that's fine. I had a really great experience and learned a lot creating these solutions. So what have we learned? I would say five things. One, we kind of had this worshipful attitude towards the standard library in the Ruby core. It can be improved. It was written by human beings, and there's always room for improvement. We saw from what Eric Michaels over did, and we saw from what James Golic did, there's room for improvement. Two, optimize for your use case. This is really important in terms of performance work. It doesn't matter how good your solution is in every use case. All that matters is your use case. And sometimes a specialized tool might work a lot better than a standardized tool. Think about knives. If you have to cut watermelons and you have to cut grapes, you could theoretically do them both with the same kind of standard knife, but it'll probably work a lot better if you get specialized tools for each situation. Three, I can't emphasize this enough. You have to benchmark. Okay, don't trust your intuitions. I can tell you my intuitions are wrong all the time, and yours will be too. So make sure that you benchmark to validate your hypotheses. Four, I wanna encourage everybody to experiment. Experiments are awesome. You learn tons of things. It's just a lot of fun. And occasionally you could create something that's actually really useful. So do more experiments. There's little downside, and you always learn something. And here's the last thing. And to me, this is the most important point here that I wanna stress. I had a problem. I profiled to zoom in on the problem, looked at the source code, looked at the solutions that are out there, and thought about it a little bit camp with an idea. Benchmarked to verify that my idea was correct. And then made it into a gem. There's nothing in that whole series of events that any single one of you can't do. You will come into problems. You don't have to be the most brilliant programmer to make a difference. You will come in with your own perspectives and your own ideas and your own experiences and your own intuitions. And they will enable you to create solutions that no one else can think of. Or to make improvements to existing solutions that no one else would think of. So, please, don't sell yourself short. You can do it. It only takes one person to invent a really great tool. But if that tool is gonna work for the community, everyone in the community has to be involved. OpenStruct is a really, really nice tool, but it took a lot of different solutions coming from different people in order for it to really work for everybody. There's more work to do on OpenStruct. There's more work to do on a lot of things. Be part of the community and make our community so much richer. Thank you.