 Hi, everyone. I'm Max Jacobson. Hello. I am here to talk about how there are no rules in Ruby, which what does that mean? What am I talking about? I started thinking about this idea a few months ago when I learned something new about Ruby that really surprised me. And the effect that it had was I just started hearing this phrase, kind of unbidden, coming to mind. There are no rules in Ruby. I just kept popping into my head, and I just had to write it down. And so that's now what this is. So I'll talk a little bit more about where I'm coming from. So first of all, thank you to the organizers. This is my first conference talk, and they made everything super easy. And I'm really excited to be here and very honored. So thanks to all of them. Just a little bit about myself. I'm in from New York. I'm excited to be here. I wish I'd gotten a haircut before I came. I hear they're filming this. And so yeah, I am a software engineer at Code Climate in New York. I have some stickers if anyone wants any for later. So what we're going to be talking about is Ruby and how it works and how we think about it while we're writing Ruby code. I sort of feel like I should be clear that this isn't like Ruby is bad talk or Ruby is dead talk or anything like that. This is just sort of a little bit about my personal journey as I have been learning more about it and learning about other languages. And more than anything, I'd say this is a story about awkward phases, which I'm in one now. So I don't know about you. I have certainly had a lot of awkward phases. I was a little bit too into BlinkVanity 2 for a little bit too long. And yeah, and so this talks a little bit about an awkward programming phase that I am in. And I think I'm starting to gain a little perspective on. And I'm sort of wondering if maybe it's a thing that a lot of people go through. So if you are maybe beyond it, you can tell me what's on the other side. So maybe a more appropriate title for this talk could be something like how I learned to stop worrying and love that there are no rules in Ruby as a movie reference. So before we jump into some code, let's just talk a little bit about rules. So just an introduction to rules and maybe an example. Here's a rule. Employees must wash hands before returning to work. I like that one. I follow that one. In an alternate reality, there is this. Maybe employees may wash hands before returning to work. The small difference matters, right? It's not quite a rule if it's not a must statement. I Photoshopped this. So the thing I started recently feeling like is what if Ruby is more like this sign than the other one? What if working with Ruby can sort of feel like being in the restaurant that has this sign? So I'm going to talk a little bit about what I mean. I'm going to try to show you what I mean by that. And before I do, jump into some code samples to show you what I mean. We've had an introduction to rules. Here's a quick introduction to my childhood dog Milo. I've been made to understand that conference docs feature photos of pets. I don't have a pet. I used to. This is him. Milo is a caranteria, like the dog from Wizard of Oz. And, but I always used dogs for examples when I'm sharing little code examples. So I wanted to put a face to the variable. So let's jump into some code examples to talk a little bit about rules and what we're, how we can think about them. So here we are. This is a super small program. We define a dog class and then use it. Your programs that you're writing at work are probably a little bit longer or more involved. But we're starting here. So we can read this and we can build an understanding of exactly how it works and what are the things that we can assume to be true about it. And so when we run it, probably as predicted, we see Milo is a dog. That's the expected output and that's what we do get. And we could run this a million times and it would always do the exact same thing. So let's make two small changes. So first of all, let's pull the line out into a method, getDog. So we can now call this getDog method to get a dog. We pass it a name. We make a new dog with that name. And let's make one more small change. So now the implementation of getDog is hidden. Why is it hidden? You can sort of think of this as an example from the docs for some library that you're reading where you see, OK, there's a getDog method. It returns a dog. You're not exactly always looking at the source code. But you then incorporate those classes and methods into your projects. And so you can sort of think of it like that. So what are the things about this that we can trust to be true? Given that we don't have complete information about our system anymore, is there any implementation of this method where when we run it, the program will crash? So it seems like our assumptions could be written as this rule. So the rule that we're living by in our world so far is the method getDog returns a dog. So that might be true. Who's convinced? Who is willing to trust this rule? Can I see some hands? It's a trick question because I can't see anything. So yeah, I see everyone's hands are up. Cool. So unfortunately, this isn't true. So what we're going to do is take a look at an implementation of getDog where this isn't true. So this is almost like a petulant implementation that I'm going to show you. But hey, maybe it doesn't return a dog. Maybe it just raises an error. Maybe it does. So that rule that we tried to live by for a fleeting moment isn't a rule. So OK, so maybe it raises an error. So we can say, OK, it returns a dog or raises an error. So this is another potential rule that we can live by in our program. I'm not going to ask you to raise your hands anymore. But what I like when I show these rules is to just take a moment to think, does this sound right to me? And so looking at this, I think that seems right enough. Like that's what it says it does. It returns a dog, it gets a dog. But maybe not. Maybe it instead returns a string. That, again, says sorry because I feel very apologetic today. And when it returns a string, then we get a different crash. We get a different runtime error where, at runtime, we get an undefined method error because strings don't have a name method. And so our program crashes. And so the rule we were living by didn't turn out to be something that we could really live by. So let's look at another example. So it's a reset to the state before we've decided a new rule. So OK, so maybe we don't have any guarantees about the type of the thing that will return from the getDog method. There are other languages where you can specify the return types. And Ruby doesn't do that, which is cool. But yeah, so maybe let's say we have a dog. So let's say we have a dog. Then when I call the method name or send it the message name, I can read back the name, the same name that I initialized it with. That would be a nice thing. That seems to be how it works, and that's how we're using it. Well, OK, I'm not going to ask you to raise your hands, but I'd like you to take a moment and think if this feels satisfying. Well, maybe not, though, because you can always just define a singleton method for an instance to redefine that method and have it do something completely different. In this case, again, returning the string sorry. In this case, we don't crash, but we do have an incorrect behavior where instead of printing out Milo as a dog, it prints out sorry as a dog, which is a phrase that I find lovely for reasons I can't explain. Yeah, so I'm not really yet saying anything about how you should write your code. I'm just exploring what are the boundaries of the things that can happen in your Ruby systems and your Ruby programs. So the thing that we're seeing here is that if you are curious about the behavior for a class, you can look up the source code and you can see the definition of the class, but you don't really have any guarantees about that you're seeing the full story because the boundaries are very open and you can make changes at runtime to instances or classes. OK, so let's try another one. Maybe the method will be messed with, but it'll have a method name. That seems to be true so far. That feels true to me. When I have an instance of dog, it will have a method name because we did the thing. We have the add a reader where we defined that it will have an attribute that we can read called name. Well, sure, it could have had a method name, but what is the joke about being ashamed of something happened to it? Yeah, so like I said, the boundaries are super open, so for any given object, you can reach in and just undefine an arbitrary method, and then it just doesn't have it anymore. So at the bottom, we crash in a different way with undefined method name for dog, even though every other dog has a name. OK, so let's reset again. Almost done through these examples, then we're going to talk a little bit more about why this is haunting me. So resetting once more, and then OK. So we saw in that error message at the bottom that even though it didn't have a method name, with the inspect output of the dog, we can see that it has an instance variable name that is still there. So maybe we can no longer access it via method, but it's in there. So that gives me an idea for another rule that maybe we can live by. So we reset. So when I have an instance of dog, it will have an instance variable name defined internally. OK, so this is cool. So how do we even check this? So Ruby does provide methods for checking things like this. So you can ask any object, do you have this particular instance variable defined? It'll return true or false. You can also ask it to get that value for that instance variable, and you can see what the value is. So we do have the tools to check this. So does this feel right? At this point, probably none of you trust me even a little. So I'll just jump to it and say, no, this is not possible. This is not a rule, because just like you can undefine methods, you can reach into arbitrary objects and discard whatever instance variables they might have liked to have. You can just reach in to whatever you want and remove an instance variable. So that's, unfortunately, not something that we can fully iron-cladly rely on. Jeez, OK. So let's reset. And then this next example is the last one. This is actually the one from a few months ago that when I learned it broke me. It might be overstating it just a little. But here it is. So when I have an instance of dog, the code in the initialized method must have run. So you have your class dog. You have an instance of it. You check the type, and it says, yeah, I'm a dog. And it must have been initialized. Right? So this is something that is deep in the vowels of my mental model of how Ruby works. And anyway, so no, you can't. So the way that this works is that instead of dog.new, you do dog.allocate. And what happens is you get an instance of dog that just hasn't been initialized. So that's cool that you can do that. And the truth is that I actually do think it's cool. I don't yet know when you would, but I do think it's cool that you can. So brief digression to talk a little bit about what actually happens when you call dog.new. So I'm gonna show you a small amount of C code that I don't really understand, that I found in the Ruby source code in the file called object.c, that I guess is the definition of objects. And so I've actually taken the liberty to translate this to Ruby for the people in the room like me who are not particularly fluent or comfortable to see. So I've sort of rewritten this. This isn't actually how it works. This is sort of like through a, you know, resistant to glass. But you can imagine that when you call dog.new, the first thing that happens is we allocate an instance of that class. And it is an instance just like any other except that the initialized method has not yet run. And so you can see in the output here that we have a dog. And you can see that there are no instance variables inside of it yet. And so the very next thing we do is we call the initialized method forwarding the arguments that were passed to new. I don't know if you ever wondered like, hey, how come I define an initialized method but then I call the new method? This is why? So then the next thing is we just return the object. And so ultimately we return the initialized dog. I actually think this is super interesting. It means that in your code, if you ever are using some library and you want to bypass the initialized method for some reason, you can. So take that with you. So I said that was the last rule, but actually one more bonus rule, which is the only thing I actually feel comfortable saying at this point, which is when I have an instance of dog, it's a good dog. Okay, so like I said, I do really love Ruby. I'm not criticizing Ruby here exactly. I'm more talking about how we can think about how our systems work. And so my feeling is that because of a lot of the possibilities that there are, we need to build additional confidence in our code before we ship it out. And so we have the ways we get by, the things that we do to stay on top of our code quality and to build confidence when we're shipping out our code. So I'm gonna go through a couple things that I think are really useful practices when you're shipping Ruby code to feel confident about the correctness. And so first one is pretty big, which is have an error tracker. So we're seeing a lot of different ways and unexpected ways that your program can crash. If you aren't tracking when those crashes happen, you're gonna be missing out on your bugs and you aren't really able, unless you're superhuman, to figure out in advance what are all the different ways your program might crash. There aren't really the tools available to do that. And so this is hard to write really resilient code and you should sort of plan for having an error tracker so you know what goes wrong in production. You can and should write loads of tests to build up your confidence and define what are the expectations for how your system works. And that can be a way to make sure that no funny business goes on. You should also do code review so that even if you didn't catch something, maybe your teammate does. There are a lot of things that, if I tried to merge some of that kind of code into work, it probably would get a little bit of polite feedback. And this is one of them that I really like, which is it's a good skill to cultivate to get comfortable reading source code. This can include your own source code that you wrote before, your teammate source code, the libraries that you're depending on, and even the source code of Ruby itself, if you ever are wondering what actually happens when I initialize a new object. So I think that's a really good skill to cultivate. The last one is thinking about types is really valuable. So we were looking at some methods that we thought would return one type and they returned a different one. And even though Ruby isn't like a statically typed language, you still have types, right? There is no compiler checking types for you, but there is a type checker and it's you. So it's on you to think about that and it really behooves you to do so. So I want to talk a little bit about how I got to here. So I wasn't always like this. I used to be more calm. So here's my personal program or timeline journey. So I started learning Ruby, it's five years ago, and then I started learning Ruby on Rails, the Flutter in school, 2013. And then I started reading about Rust, which is another programming language in 2015. And then in 2017, I just started to panic. So why is that and what is Rust? So I'm gonna show you a little bit of Rust code to kind of show you another prism into the similar situations and show some examples of how a compiled language can help catch these kinds of problems up front and help keep you honest. So super quick intro to Rust. Rust is a systems programming language that runs blazingly fast, prevents segfaults, guarantees thread safety, it has a crab as a mascot named Ferris. It had its 1.0 release in 2015. In 2016 and 2017, it was the most loved programming language on the Stack Overflow Developer Survey. And I'm gonna come back to that idea a little bit at the end, but I think that's impressive. And my thought is that it might be related to this stuff that I'm talking about. So in contrast to Ruby, there are rules in Rust. So I sort of written a similar dog example in Rust. You can see it looks kind of similar. It has the same shape where we define a struct dog instead of a class dog, and we have our getDog function, and we get one, we pass it in the name, and then we print it out. It's the exact same program, but in Rust. Don't worry too much if you don't know Rust syntax. I'm happy to chat more about it later in the future if you're curious, but don't worry too much if you're like, why is there double colon or whatever. Okay, so when you run this, you get exactly the same output as we expect. So what are the rules about Rust that we can try to follow? So for this program, the function getDog returns a dog. That was the same kind of thing that we wanted to be able to say about the Ruby example, and we sort of couldn't really feel confident about saying that. So let's try to break that rule and see what happens. So if we rewrite the function so that it tries to return a string instead with sorry. When we try to compile that, it won't compile and we'll get this error message. I really liked the earlier talk about errors, and I think Rust is a really good job of communicating to you what you haven't done exactly right, and so here we have a mismatch type error and it's underlining exactly where on line five we promised that we would return a dog. And it's like, you promised you would return a dog and we got a string. You said, and so it won't compile. So it does still fail, but failing at the compile time means that it's impossible for you to ship this bug out to production because it won't even build. And so yeah, so we actually can list by that rule that this function does return a dog and nothing else. All right, that's cool. Let's try another. So when I read the field's name from a dog, it'll be the same string that I created it with. So this is getting at some of the funny business with Ruby where we were getting back different values than we expected. Yeah, and so let's try to break that. So here in the main function, if we try to reassign a different string to the name field just to see if we can, we will get a different compiler error that will say, hey, that's an immutable field. You can't do that. And so everything in rest is immutable by default which sort of lets you feel more confident that your data is what you think it is. It's the same thing that you started with unless you explicitly opt into mutability. Cool, okay. So let's try one more and go into an example of how Rust has a different idea for how to manage error handling. So in our Rust program, can we say that the getDog function does not raise an exception? The answer in this one is sort of on a technicality which is that Rust doesn't have exception so kind of it can't raise exceptions. And I'm not saying that like, oh my God, Rust is so great, it doesn't have exceptions because of course things can go wrong. So let me show you a quick example of how they compare. So briefly let's go back to Ruby. So imagine we have this Ruby function that reads a file at a file name. All it really does is call file.read file name. Just by looking at the function, we don't necessarily know what happens when we call this function. We think it'll return a string, it'll return the contents of the file that we're reading but there are a lot of scenarios where it might go wrong. Like for example, if the file doesn't exist or the permissions for the file don't allow you to read it. There are a lot of different ways where it could go wrong and the way that Ruby will tell you is by raising a particular exception. So let's say you wanna write some error handling code where you can know exactly, you can decide exactly how it should behave in the different error scenarios. How do you know what you need to cover? It's not exactly exposed to you. You can go and read the docs and you can go and read the Ruby source code to go see exactly how it's implemented. I actually couldn't quite figure out what the errors it might raise were just by reading the docs but through some experimentation in IRB, I determined that it will raise a system call error and there are like many subclasses so there's a specific one if the file doesn't exist and a specific other one if it doesn't have permission. So yeah, just by looking at the code you don't necessarily know how it behaves and you can research it and you can document it with comments but you could imagine that this function read file could grow and change over time so maybe in the future we add some validation where we raise a different error if the file name doesn't end in TXT or something and then our comment is no longer correct because we haven't kept it up to date. So that's always a challenge is to figure out the right balance for documentation. So by contrast with that kind of priming you, let's look at how Rust handles error handling. So here is a similar function in Rust where we are taking in a file name and returning a string that is the contents of the file but instead of returning the string we return what Rust calls a result. So a result is a kind of wrapper around the result so you can see right by reading the function signature that this will return a result of string or standard IO error and so what that means is that when you call the function you get back this container that you can ask well did it work or did it not work and so what that looks like is well I've got these things highlighted so you can see these question marks that's where we're doing the early return so those two places of the question marks are the places where it could go wrong so for example when you try to open the file if it doesn't exist that will not work and even in a language I'm sort of trying to sell I'm making it easy to do things right that file just might not exist and that's life. So yeah so the question marks are the places where things might go wrong and they mean return early with the error if something does go wrong and you know that it'll return a standard IO error which is a class of error and if something else might go wrong then it won't compile because we promised that we would return a standard IO error in the error case and so we can feel confident about that so then the way that it works from the calling side so when we're calling the function we get back our result and we can do pattern matching where we can say okay in the happy path where we were able to read it and we did return a string do one thing and that's this first bit where we say okay content so we're gonna call that line with the contents if it succeeded and then the second pattern match branch is the error case so all we're doing here is printing out that it didn't work and then printing out the reason but if we wanted to we could break it on further and have different behavior for the different kinds of standard IO error and so this is pretty nice because you can't be surprised by things going wrong you have to determine what your game plan is for proceeding when things go wrong and so it's pretty appealing because you are sort of made to think about that upfront and then you can write a more resilient program so then when we call it it prints out this where we get the error message printed out but our program didn't crash so we have like failure but in a handled way and so the more I started learning about Rust the more excited I got to say wow we have so many crashes in our Ruby apps it'd be so cool if we had better tools to prevent that and be more confident about our code and then I learned that in Rust you totally can crash so I'll show you a little bit about what that looks like so TBH Rust totally can crash and so here's the same function but you can see from the function signature that we promise that we will return a string but how can we promise that when things might go wrong? We can't, right? And so what this is really promising is we'll either return a string or we will panic which is what Rust calls crashing so the syntax here is that whenever we are at the site where things might go wrong we can use this expect method to say I expect it won't and then if it does go wrong you just crash so what that looks like when you run it is it'll print out thread main panic at couldn't open file da da da and so this is more like a crash that you might be familiar with from Ruby so once I learned about this I realized like oh my goodness I've been going around telling everyone Rust is so great it makes you write perfect code and you are everyone should use it and I realized no what every function from every Rust library I use could be panicking internally and I have no way of knowing unless I go and read all the source code so I went and read the docs for panic so this is the docs for what the panic macro does in Rust so this macro is used to inject panic into a Rust thread causing a thread to panic entirely gosh I love that sentence the as I read this I started thinking maybe it was me who had panicked entirely I started getting real worked up that having started to learn Rust that maybe Ruby is so dynamic that anything can happen and how can we ever feel confident about anything and I was like Max calm down, you're gonna be fine so the more that I explore this stuff the more I started thinking do rules matter after all this phrase started echoing through my head there are no rules in Ruby, there are no rules in Ruby but maybe they don't matter and so there were a few different questions context where I think they may or may not and I wanna sort of frame these as more questions than answers because I think it's very much an opinion thing and also I don't have the answers so there's three ways that I think are interesting to think about this first one, for me this is the most important one do rules matter for new programmers who are learning part of me wants to say yes I think that having predictability and consistency are very appealing for teaching and I think that inconsistency can be really detrimental and so I want to say yes but ultimately I really should defer to the educational psychologists who have done tons of research on schematic and mental model building and the thing that I'm tempted to argue is that Rust is a great language for beginners and Ruby maybe isn't as good as one that I always kind of thought to bring in beginners into our field we want Ruby to be welcoming and inclusive and I started fearing well what if it's not and so I talked to a lot of people a lot of friends, a lot of people who like me learned Ruby as their first programming language and the more I talked about it the more I realized like I don't think that's true I think in a way there are no rules in Ruby is a huge, huge asset because you are free to explore and you are free to play around and get quick feedback and have a program running and iterate from there and that was my experience too so ultimately I'm less worried about this than I was at the height of my and Rust I think is a totally solid language to learn for beginners I think there probably is a steeper learning curve but if you have a good teacher then I think it could work out great and the language itself is only becoming more friendly over time that learning curve is flattening as they learn from people who are learning it about what are the barriers and they make those less barriers some and yeah so okay so what about do rules matter for making good software? This is another concern but this one I feel pretty clearly not really people make great software out of sticks and stones the most people will remember to wash their hands like the system works so ultimately I don't feel like this is a major concern I think that tools like Rust can help you write more crash free programs but that's not the be-all and end-all of good software I think that's more to it and even with Rust you are going to want things like code review and tests and error trackers and to read the source code so the one last way that I started thinking about do rules matter is for programmer happiness this is explicitly one of the design goals of Ruby and I think Ruby has been incredibly successful at being a language that brings happiness and joy to programmers and I think it, so that's a testament that maybe not but I think that there is some room for disagreement there and particularly when it comes to like maintaining Ruby projects over time I think that there is some pleasure to lacing your shoe laces up real tight one day and just feeling like all right I got this which is a little bit harder to do with Ruby than others and I started wondering like you know there's a lot of conversations about Ruby right now about should there be a tight system added should there be, I think you know we heard about all the new ideas that maybe will come and how there's always trade-offs and I'm wondering maybe Ruby is in an awkward phase too where it's sort of in between who it is and who it's gonna become so I think this one is more up for user preference so one last thing I wanna talk about before we ramp this up is so okay so maybe I was really just panicking and there maybe are some rules in Ruby because there have to be some right so what are the actual rules in Ruby I'm gonna just hit a few of them this is not exhaustive so for example if you have a syntax error your program won't run that's pretty ironclad I think this is kind of the big one in object-oriented Ruby programming you can send messages to objects and they'll respond in a predictable way which is to say they'll go through the lookup chain and they'll figure out which implementation to use and that's true keywords are a little bit special they can kinda do their own thing and then really the last big one the thing that I've sort of concluded after a lot of soul searching in conversation is that the actual rules are whatever you make them you can decide for yourself and for your team and for your projects how do we wanna do things and what are the rules in our system you can decide like okay this method returns a dog and that's how we're going to work and that can be a rule for you and you can make that happen and you have the tools to go and read all the source code and burrow all the way down to the roots and make that true so ultimately I think that you have the power to decide what you want to live by so thanks