 The mic's working? Cool. Hey, I guess right now I'm without boats, as long as I'm on this stage. Names are hard. So this talk is about Rust's generic system, like traits and type parameters. And it's about how you can use those features when you're writing code. And I've been a Rust user for about two years since shortly before it was stabilized. And I have a couple of my own projects in Rust, open source, sort of side projects that don't, by day I write mostly Ruby. And I participate pretty actively in the RFC process. And so one of my projects is a project called Naughty. And Naughty is an experimental terminal. And what I mean by that is that a Unix terminal normally will implement a protocol called ANSI escape codes. And that protocol has pretty much been stagnant since about the 80s, since they were actual physical video terminals, not virtual terminals being emulated on a machine. And so Naughty implements that protocol, but also a new protocol to support other features. And so I'm gonna maybe have some code examples in this talk, and they're all gonna be coming from this project. So I wanted to give you that context about what these examples are about. But so let's talk about traits. Traits are my favorite feature of Rust. I considered them a bigger selling point of the language than even, you know, it's awesome, like memory management model and ownership and borrowing and things like that. And we've been using traits in a lot of really cool ways, especially in, you know, like very broad libraries. So this is the iterator trait. And so if you wanna define a new iterator, all you need to define is the next method, which defines how to get, you know, the next item out of your iterator. And you get all these awesome adapters for it, like map and filter, you know, fold, collect, and so on. And what's especially great about these is that not only do you get them for free, but they're also written in a way that doesn't need to perform any heap allocations or virtual calls or anything. So they're all very transparent to the optimizer and you get not only the convenience of having these written for you, but you also get really performant code. And we've been writing not only in the standard library, but there's a lot of other libraries that have been using traits really effectively. So this is just one example from Serde, the serialization library. And so what's great about the way that these traits are defined is that there's this handshake with, you know, another trait, like you have a serializer trait, which is implemented by different serializers for a bunch of different formats. And you just have to define serialize once and you can get serialization to any different format. And this is also implemented in such a way that it's transparent to the optimizer and the performance is competitive with a handwritten serialization for that specific format. But I'm actually not here to talk about those libraries. So what I wanna talk about is about how we use traits when we're writing our application code. So those libraries, you know, they're going to be used in a whole bunch of different applications with a lot of different use cases. But when we have some specific business logic that we wanna implement, how can we use traits to make our code more clean, more maintainable, easier to read and refactor and such? And there isn't really a lot of good guidance for this. And it's, you know, my projects are more on this higher level of implementing a particular use case and I've been grappling with it while I've been building my different projects, especially Naughty. And so I was preparing this talk, I looked at some other applications to see how they were using traits and compare it to the ways that I was using traits. And so I have some numbers. Before I continue, I wanna be very clear that I'm going to be highlighting a difference between my application and applications written by other people. And I am not criticizing the way that those other applications are written. I'm not only saying that because their authors are in the room right now. I'm also saying that, because I think it's really important that we be able to have conversations about differences in the way that we code without it being like this sort of dogmatic statement of oh, this is right and this is wrong or this is better and this is worse. So first cargo. And really what I wanna look at in this table is the ratio between, so you have structs and enums, which you can think of as just types, right? Like these are like the concrete types in the system and then traits. And so the ratio between types and traits. And so cargo has a little over 200 types and it has 10 traits. And secretly like a third of those are actually just because they're features from the standard library that haven't been stabilized yet. So Rustup has a similar ratio to cargo. It has 70 types and three traits. And so these traits are pretty consistent with application code in the Rust ecosystem in general. Like looking at a lot of different crates. This about one to 20, maybe a little less ratio but from trait to type is pretty normal. So my project in contrast, I have 90 types and 12 traits. And so that ratio is closer to one to eight. So I'm using like proportionally as significantly more traits than other people are using. And if we visualize this, you can see that the bars for types and traits in cargo and Rustup are pretty proportional. And then for Naughty, I'm just using significantly more traits than these other libraries. And so again, or sorry, these other applications. And so again, I'm not saying that these should be refactored or these have tech debt or anything like that. But clearly I'm doing something differently and so I wanted to share how I'm using traits just so we can have a conversation about it. And so fundamentally traits are about polymorphism, right? And so that's one of those like CS words. So I went on Wikipedia to find a definition. And polymorphism is the provision of a single interface to entities of different types. So you have a method which, or so you have a value which is of a type that implements some trait and you call a method from that trait. And depending on which actual type it is, it's going to be dispatched to a different implementation. And so that's the important thing about polymorphism is that you have one interface and multiple implementations. And so when you have this kind of like branching in your code where you want to jump to different places from here, you can use a trait, but I think it's a lot more common in us to use an enum, right? So it's sort of intuitive that I have some value that could be of these various different shapes. And all I do is I just define each of the variant for each of those different shapes and I pass that around. And whenever I want to use that value in some way I just match over it and destructure it into each of its variants. And so I think starting out with an enum makes a lot of sense. Most of the time you should probably start with an enum unless you really understand the abstraction that you're building and where it's going to be complicated because you don't want to implement a trait that doesn't actually capture the consistency between your different variants. But traits do have advantages. And they allow you to more and more clearly define the interface that is shared by the different implementations. And so I have a case study here where I had an enum and I refactored into a trait and I think I improved the code around it. So in a terminal the sort of central structure that you're dealing with is a character grid, right? So when you open a terminal that's what you see on the screen is this grid of cells each of which can contain one character. And we're gonna be looking at the right method on Naughty's character grid and this is a simplification so it doesn't deal with anything like color or text styles or anything like that. And so the grid contains an actual grid which is the implementation of like a two-dimensional array that contains just charts. And a cursor which is the coordinate point into that grid at which the cursor is currently sitting. So whenever you write something to that grid it's going to be written at the position of the cursor. And the right method just, like this is the naive implementation, it just takes a char and the first thing it does is it assigns that char to the cell in the grid underneath the current cursor. And then because you don't wanna keep assigning to the same cell in the grid you move the cursor right by one. And this move within method handles some of the complexity around making sure that the cursor stays within the grid and things like that. And so I think that write is like a pretty nice method right now. It's readable, it's only two lines. Someone without context can come into this and pretty clearly understand what it's doing and how it could possibly change the grid. But it has a really fundamental problem and that problem is Unicode. And so as anyone who's done string handling and Rust knows, text is not simple when it's not ASCII, right? Like there's a whole bunch of different edge cases and all the different kinds of human languages. And so here are some specific edge cases that are relevant when you're implementing a terminal. So a terminal assumes that you have a monospace font. You know, you have cells that form a perfect grid but Unicode specifies that some characters are supposed to be double wide even in a monospace font. So here we have some Korean characters above some Latin characters and Latin characters each take up one cell but the Korean characters take up two. And so we need to be able to handle that in the state of the grid to make sure we don't accidentally have characters overriding one another. And some characters aren't actually supposed to be displayed in their own cell at all, right? So there's like modifying and extending characters which are code points that are supposed to be applied to the previous character. So you have an N and you type this character you're supposed to get an N yet out of it instead of this weird N followed by a till day thing. And lastly, I said that Naughty supports features that other terminals currently don't support. And so one feature that Naughty supports is writing images into the grid instead of only writing Unicode characters. And so if you want to display an image with your terminal output it can be really difficult. You can't really do that in a cross-platform way because you can't handle like the windowing systems of all these different, you know, wherever your terminal you're running. And in Naughty you just kind of write an image and it'll be in line with the text output of your program. And so sort of the intuitive solution to solving all of this complexity was just to define an enum. And so there's the char data enum and it has a variant for each of the different characters or not for each of the different kinds of things that you could write which could be characters or you know, there could be images. And write instead of taking a char takes this enum and then the first thing it does is it matches over that data and breaks it under each of its variants and then each variant contains the algorithm for implementing writing that particular kind of data into the grid. And here under the char variant we have the, this was the exact same code as the original method only now it's nested way deep into this match and it's next to all of these much longer and more complex variants for all of the different other kinds of data. And so this works but it has some problems and sort of the most obvious problem is that the method quickly became enormous. So as you're dealing with all the edge cases when you have like, you have a wide character but you're in the far right hand of the grid and so you need to wrap around before you write and things like that. You know, you have this like multiple, like it was in three digits the number of lines in this method and there's no separation between the logic for each of the variants. So if you want to understand, you know, you're trying to read this code and you're trying to understand how one of these variants is going to behave. You know, you have to read this entire method and there's a sort of more subtle but related problem which is that there's no contract defining how writing each of these variants is allowed to access the char grid. So they just take a mutable reference to the entire char grid and there could be like other fields in that struct and you don't actually know how they're going to be mutating them. And when the method is really short, that's not so much of a problem. You know, it's two lines, you can kind of like look through it and see how it's going to mutate the different fields of char grid. But as the method grows longer and you know, maybe you're trying to debug it and it's changing the state in a way you don't expect, you have to read through this really long like thicket of conditionals to figure out what's happening. And so I did some exploratory programming and ended up coming up with this refactor where I used a trait instead of an enum. And so instead of having the char data enum, there's now a writer trait, which defines the behavior of writing something into a char grid. And so it takes a mutable reference to the two-dimensional array, the underlying grid, and also to the, it takes the cursor, the cursor is a copy type, so it takes it by value. And then it returns the position the cursor should be at when it's done writing. And now the right method is only one line long. It just takes a writer and calls that writer's method, passing in the correct arguments, and then saving the state of the cursor when you're done to the cursor. And what's really great, I think about the three factors, the way that this method now only deals with the state change component of this problem, right? You're only dealing with getting this current state and then saving the state when you're done and you're not dealing with any of the conditional logic. And those have been partially isolated from one another. And then, so I implemented a writer for char and this is very similar to the original method of write. You know, it assigns a char into the grid at the cursor position and then moves the cursor right. And there's also an implementation for, you know, each of the other variants are now their own types and they all have their own implementation. And so somewhere in the code, you get each of these variants get constructed and then passed and char grid calls write on them and such. And so I think this was sort of a straightforward refactor in that, you know, I just had an enum that had become too long and so I converted into a trait and maybe we might like differ on like when exactly an enum is too complicated and it's worth the trait. And you know, there are some performance concerns about like possibly this might introduce dynamic dispatch. In this case, it didn't introduce any dynamic dispatch but, you know, it could. But something I think that I do that is a little bit more uncommon is that I will often write a trait when I only have one implementation. And so there's nothing really polymorphic about that, right? The trait is only going to be implemented by one type but I'm still creating this generic that's always going to be the same type. And the reason I do that is that the other aspect of traits is abstraction. And so I have another quote. This is from the introduction to computer and computation and programming using Python but as you can tell from my inability to read the title, I actually just found a normal compedia also. The essence of abstraction is preserving information that is relevant in a given context and forgetting information that is irrelevant in that context. And so abstraction is, you know, about determining what matters for implementing this particular algorithm and only presenting that information and hiding the other information. And so I think it's important to remember that abstraction is not only about code reuse, right? Like these ideas sometimes get completed when we're talking about abstraction but code reuse is an advantage of writing abstraction, right? So you write and iterate, you have the iterator trait and you get to reuse all of those adapters. But the real reason you want to write abstraction is separation of concerns. So you want to isolate your components from each other and only present them, you know, the information about one another they actually need to know to implement their problem, to implement the solution to their problem. And so just another case study where I, you know, wrote a trait when I only actually had one type so let's say you're using a text editor like Vim for the other one and you've got two text buffers open. And so the terminal doesn't know anything about, like, these buffers are supposed to behave like separate panels, right, but the terminal doesn't actually know that. The terminal just has a single char grid. And so your editor is doing all this wizard of Oz man behind the curtain stuff to make it look to you like you have these two separate buffers that can scroll independently and then where you edit one and you wrap around and you stay within the buffer and things like that but the terminal is not giving your program any help at all. And so you can see here in this application there's the two text buffers and there's sort of another panel between them is like a gutter and then there's a status bar below them and such. And so Naughty provides a feature where you can like present the terminal an abstract description of how it should be dividing up the screen and then it will create separate char grids that fill each of those sections of the screen. And so essentially it starts with a single panel for the whole screen and you tell it, you know split it at this column or split it at this row and you just keep splitting it up into separate columns. And the implementation of this is it's basically a tree. So it's an enum where one variant contains a char grid and the other variant contains a fork between a left panel and a right panel and then some data about where exactly you should draw the split. And so the root of the tree is gonna be the whole screen and then each branch is each section down to the leaf nodes which contain actual grids. And importantly, this enum only is coupled to char grid in its grid variant. The split variant doesn't care anything about what the leaf grids, the leaf nodes of the panel actually contain. And I wanted them to be as decoupled as possible. So I actually defined it in a generic way. So this is now a panel of T and the grid contains any type T and I constrained that type T by a trait that I called fillPanel. And so this is a panel of any type that implements the behavior of filling a panel. And what this means is that, you know, panel can't just arbitrarily call any method it wants on char grid. If I wanna call a method on char grid from within panel, I have to think about, you know, is this appropriate for it to have this knowledge of char grid? You know, I have to, it's like this decision point where I have to think about whether or not I'm coupling my modules too closely together. And when I was finished implementing panel, it actually only contained two items. In the fillPanel trait only contained two items. Panel has a whole bunch of methods. And the first is a constructor that just takes a dimension pair. And the second is resize, which takes, you know, it's a method that you already have a panel and you wanna resize it to fit into a new section of the screen. And so both of these methods have like a very clear connection to the logic in panel, right? So if you wanna split a panel in half, you're going to need to resize the current grid and then construct a new grid to fill the other half of the panel. And so both the brevity of this interface and the relevance of it made me feel pretty comfortable that I had to find a really good abstraction between these two components of my system. But there's another advantage to providing these traits. And so I said that it only had one implementation and that's true in like a debug build or a release build, but in testing build, there are actually multiple implementations because what providing a trait like this can do is it makes it much easier to write mocks. So, you know, in Rust, it's very hard to mock something because it has, if you have a concrete type, it has to actually be that type. You know, it's not like Ruby or something where you can just pass a dynamic type in. But if your code is generic, you can just create a new type which implements the traits that are required and then pass that type in instead. So this mock grid type isn't a real grid. It just contains the dimension pair of a width and a height. And that makes it, you know, so it only cares about, it only contains the information that panel cares about and not any of that irrelevant information about having a grid of characters and writing to create and all those kinds of things. And so in my test, I just had to, you know, create a panel that can hit a mock grid instead of containing a char grid and then it splits it or whatever operation I want to test. And then at the end, a search that the grids that it had have been resized to be, you know, the correct dimensions. And so I think this is an effective way of writing isolated unit tests in Rust. I really don't know a better way to mock. There are like some weird things you can do involving like config attributes on modules and like fake paths and stuff like that. But I think this is much cleaner and like a really good way to, if you want to isolate your unit tests. But more broadly, so how will you know that, you know, you should use a technique that I just used, right? Like how do you know when an enum should be a trait and how do you know when you have a boundary between modules that you want to keep decoupled. And you know, obviously I don't actually know like in your specific use case when you should do that. But I do think I know how we can find the answer together and it's patterns. This is a really good book, by the way. It's about architecture, like buildings and stuff, but you should totally read it. So I see patterns come up a lot, few times in Rust and outside of Rust. And I always, whenever patterns come up, someone always says something like this, that, you know, patterns are a sign that your language isn't powerful enough. You know, you just need a better type system or you need macros or you need some form of expressivity that your language just doesn't have. And I really think that this is wrong. I don't want to say wrong because it's not, I think it's off the mark. I think that it's a really stultified way of looking at patterns. So patterns don't have to be templates for classes. Like that's the really common way that, you know, patterns have been presented as being like abstract factory being and so on. But like, that's a really stultified way of looking at what a pattern is. A pattern is a way of recognizing the good solution to the problem that you have. So languages can just, on their own, they can give you tools for creating abstractions. They can give you different ways of expressing yourself. But they can't give you the insight into how you should express yourself in this context. They can't tell you, you know, what the right way to solve this problem is. And they can give you some guidance but really what we need is like a community consensus around, you know, what the patterns of our language are and what's idiomatic and what's not idiomatic. And so I think that, you know, we really should be having more conversations about design and Rust. I think that as Rust gains greater adoption and we're no longer implementing, you know, just these libraries like a standard library or serialization but, you know, actual application code, we need to be having more thoughtful conversations about how Rust ought to be used and, you know, try to come to some consensus about what the idioms of the language are, about what might be a code smell, about what patterns you can use and things like that. And, you know, we have some movement in this direction. I think it's Nick Cameron has a repo about patterns but it's, you know, most entries have to do next to them. And I think that we should be, you know, having this conversation in a thoughtful way. And I do think that traits are a really good way of finding what the patterns in our language can be. I think that, you know, playing around with how you can use traits to better abstract your problem can bring out a lot of patterns that might be surprising and not obvious about how this feature can be used. And so on that note, I have one more slide. I'm sorry.