 I like Steve said, I'm Brandon. I'm going to talk about translating everything that the software industry has learned over the last 80 years into Rust. So this is sort of design and architecture in Rust. We're all very professional people, I assume. None of us like writing spaghetti code. So, so, wait a minute, this is good. So, yeah, I wrote a demo and I called it lasagna. Lasagna is, I think, a really good metaphor for good code. When you think about the kinds of things we try to avoid in spaghetti code, we don't like untraceable paths, we don't like a lack of separation, we don't like the core architecture getting just smushed up with everything. Contrary wise, lasagna is awesome. It has layers that are big and flat and easy to see. There are tools in place, i.e. lasagna pasta, to keep the layer separate and the layers still end up interacting, otherwise it would be just boring food. Interestingly, as I was writing this talk, this slide in particular, I discovered that lasagna code is a term of art which means code that is so poorly separated in its layers that you can't change anything without changing everything. But by that point, I had integrated the metaphor into my talk so thoroughly that I couldn't change it. So what am I gonna talk about? I'm gonna talk about leaning into strictness. How, the fact that Rust is difficult, everybody knows. But having to think about our APIs actually gives us the ability to really clarify our thought. I'm gonna talk about using modules and privacy to keep our code simple and to keep our API simple. I'm going to talk about using encapsulation and ergonomics and that's separate to modules and privacy. I'm gonna talk about abstraction and dynamism because abstraction is the last rung of the like totem pole, you're very senior when you're being very abstract. Yeah, that's what it is. To start, I'm going to introduce you to my dominant metaphor. It is an onion, just like this talk, we can't actually, you don't know what's in the onion yet. Here's the code, I'm starting with a lot of code. When you, oh, actually a very important part of this is that my through line metaphor, in addition to being an onion, is a cache. I love caches, they are super simple to start with. Every time you start implementing a cache, you realize that you have contrasting requirements, you have people who want to be fast, you want people to want to be like correct for some reason. And so you always, like there's a lot of things that need to happen, but it's a very simple metaphor to keep going through it. So we're gonna implement a cache and what's the first thing that you do when you implement a cache, you just stick stuff in a map. And that's like, in Rust, this looks pretty similar to the way it does in every other language, you've got a bunch of code, you stick stuff in a map. The downside to just doing this is that there is absolutely no encapsulation. If you want to change the cache mechanics, then you are changing the code that is using the cache. In this case, like we want to do something, if we want to change how efficiently we do something, we've got to modify our cache code. If we have a lot of different things that operate on the same data, then we've got to change the code everywhere. That's rough. So we are just like blending our business policy throughout our code if we do it this way. Ideally, we would avoid that. Even here though, we have Rust guarantees telling us something, the refmute here is saying, hey, this function might be modifying our cache, it's not just accessing it. So even in the simplest possible API, you should be thinking about, is this telling me something about the way that I'm using it? And maybe actually Rust is getting us a little bit more defined. Maybe it looks a little bit more like this. It's not so blurry, it's a little bit blurry. Our business policy, it has a clear boundary. But how do we take it to the next level? This is lasagna inside of a tray. Just a little bit. We use functions, separate functions. And this code uses a few more features to sort of climb the pillars of abstraction. Everyone knows what a module is probably. Modules are Rust's smallest unit of privacy. They are the thing that you use to keep your code private. In that sense, they serve the same purpose as files in C, classes in Java, capitalization in Go and underscores in Python. Modules also the smallest unit that can be broken out into their own file. You just extract the curly braces into their own file, name it kaffirrs and replace the whole block with a semicolon, that's awesome. I often realize that something that I'm doing should actually have a stronger privacy barrier. Just start writing module in my current file and that makes it sort of trivial to copy paste, cut around and revert stuff without it being super complicated. Pubcrate means that anything inside of the current crate can see this crate public item. This is a feature that's fairly unique to Rust. There aren't very many other languages that provide a compilation unit privacy barrier. So we've established a module, that's the bright orange part in the middle. It now has a boundary that is the privacy. It has something inside of it and something outside of it and the pubcrate is the tiny little hole inside of it. There are no problems with the implementation that we've got so far. Ownership of the hash map is sort of spread throughout. This code is circular. The cache itself doesn't have full control over its own destiny, specifically because do something knows about the implementation details. So what can we do? Or what's actually still good about this? We do have a little abstraction boundary in the form of generics. So I note 1977 is the earliest time I can find of like static polymorphism coming around in ADA. KNV here are just generic generics. They are similar to but better than C++ templates or Java generics. They're not always better than C++ templates depending on what you're trying to do. The where cause AKA trace constraint is what distinguishes rust generics from C++ templates or Java. This particular use case here, this where K equals hash plus X plus clone is a demonstration of the composition over inheritance philosophy that runs through Rust. Instead of being required to create a cache into hash map, can insert into hash map interface like you would in Java. We just say that things that go into hash maps require hash plus X. So you can sort of generate your own folksonomy of small composable parts. Additionally, traits act as a type level privacy barrier the same way that module privacy acts as a code level privacy barrier. Types prevent you from knowing what's go, traits prevent you from knowing the actual type that's going into your code. Or being operated upon. Modules give us the ability to hide items because they're not important to users. For example, I've got this pretty nifty syntax of just using dots instead of types. That's not legal. I just didn't fit everything on the slide. Sticking an item inside of a module and not marking public means that we can depend on any invariance being guaranteed by a public API. That's just basic privacy. In this case, that means that get or insert is in significantly more control over its destiny than it would be if interest was public. So why would we not just use pub for everything? The only thing that is keeping you from making the mistake of interpreting of just providing internal access to everything that you do is speed bumps that you place for yourself. Discipline is hard and making a conscious decision to publicize your internals is very important. The smaller the holes in your API, the easier it is to think through adding another one. Additionally, modules are items too. So a module, even though it is accessible, even a private module is accessible from inside module in which it is defined, but items inside of it are not public. So here we have to find this insure exists as a public item, but because it is not re-exported from its internal module, it's still private to that module. So if we hadn't marked insure exists here as public, then get or insert would not be able to access to it. But because we've made mod internal private, we haven't marked it as public, it's not accessible to the outside world. If we wanted to mark it accessible to the outside world, we would have to re-export it some way. This is a super common pattern in Rust. It is, I believe, called a facade pattern or just facade-ing stuff. It makes it easy for you to organize your code as complicatedly as you would like, make yourself feel very, very, very mature, and still expose a simple and easy to understand API. So this is what we just wrote. It's a ton of syntax. An important, the thing that I'm really trying to emphasize is that even when you're writing the simplest possible thing, Rust really wants you to be explicit about what it requires. What's the next step? Another layer of encapsulation. In this case, we are, it's very similar code. There's a little bit new here. There's a struct. We now have an object. What else is going on here? We are implementing stuff on this struct. Impulse are the way that you access method call syntax. They are not fundamentally different, except methods aren't different from functions, except that they're more creative, ergonomic to use. This is an actual design decision, though. The internal properties of this struct are public. That means that anyone inside of your crate can use this cache in any way that they would like. Up to and including initializing it separately or accessing its internal items after it's been created. I would argue that this is a completely reasonable choice for an internal or otherwise unimportant object. If you expose the public parts of your objects to things that you trust, that means that there's a whole lot of code that you don't need to write. And that means that you can have a more ergonomic API for your users if you can trust them. Something else that's new here is that we've moved the trait constraints onto the object itself. That allows you to move, even though it's possible for you to add trait constraints onto functions inside of impulse, pushing the requirements up to the object itself makes for better error messages and it means that your objects are just sort of more straightforward to use and you can describe what is actually important about the objects that you're defining. A subtle but awesome part of Rust's impulse blocks is that it's actually possible to define, require different constraints in implementation than on the object itself. In this case, the cache that is being implemented requires that the case be hash and ec. But the specific implementation of the cache requires that the objects be cloned, so I'm just moving it there. Let's talk about the error messages that I just described. Here I'm defining a wrapper type that does everything necessary to be inserted into a hash map but does not implement cache clone, which is required for the cache to work. So if I try to insert it, I get an error message that traits center clone clone is not implemented for wrapper. This is interesting because in most other languages, the error would actually come on the instantiation line because it would be impossible to construct a cache of what you want. Here the error is at actually trying to call method. There are lots of times where what you wanna do, where you have either a more ergonomic or more efficient implementation that you can provide for a specific operation but it only holds if some specific trait constraints apply. That's pretty much the only time you would want to do this. Every other time, if you can move trait constraints up to the object, you will end up with a sort of more pleasant API because you don't want people to be using your object for months or years, as long as Rust has been around before they realized that they can't actually call the method that they expected to be able to call. Only one more thing that I wanted to talk about at this level of abstraction. Even here, the module is still the only unit of privacy. The getter insert is implemented on the cache object but it still can refer to other methods internally. So it's a simple rule, modules or what privacy is implementations or never don't affect privacy rules. Another point though, is that even though the hash map itself is now working floating around out in main, we've sort of tied this down a little bit. It's a little bit less of a loop structure. There's a little, the cache is in much more control of its own destiny and knows what's going on and we've improved things a bit. So everything so far has had the caveat of you have to trust your users which is obviously crazy. So let's add an invariant that we need to maintain in order for us to sort of demonstrate some more features. In this case it's a cache statistics. Obviously in order for our cache to be accepted by our CEO, the hits and misses need to be correct. We can't trust other developers. So we don't mark them public. That means that we know that we are the only people who can modify these attributes but it also means that we need to implement a ton more functionality. An example here is that before we didn't need to implement a new method but now we do. So since we've discovered that there was one method that we weren't implementing before to make sure that we are catching every possible method that would allow for our cache to be correctly usable or pleasant to use, let's clean up some of this API. Fundamentally I was talking about how caches, like we're tying business logic to itself, hits and misses don't actually have anything to do with caching. Metrics are sort of a orthogonal concern. We want to, if we want to change which metrics we capture or the reason that we capture them it's going to be different to changing the cache implementation. So let's move the hash map out. Here is a, we've just separated, we're using two separate modules next to each other in this case. Okay, that gives us a fairly clean separation of concerns. Cache module is going to be changing because we're changing our cache implementation when you're doing code reviews, when you're handling stuff it will, you're much less likely to make a mistake if every object is responsible only for its own things. This also actually demonstrates, rust's famous commitment to like pay for what you need and hopefully zero cost abstractions. In this case, because of zero cost abstractions this hash map in this implementation is likely to be inlined and functions are going to be inlined and you're not actually going to have any kind of runtime cost for splitting things out this way unlike in most other languages or, yeah. It is also, yeah. So because we split the logic up into their own smaller units we're going to, we have like this notion of a core caching policy section inside of which we have our own privacy barriers. This is starting to look a little bit more like a shallot than an onion but you do what you can. We're still controlling access, we still have these small holes in the public API which means that we have a clear public API no matter how complex the implementation gets. Speaking of public API as though what is this insert thing that we've imported? We, inside of the cache module we've created an insert enum. It, the name is a little bit, the fields are a little bit suggestive of use. Where's it being used? It's in insert if missing. So, okay, so that's you doing it. Insert of missing has been around since basically the beginning but all of a sudden now it's public and previously it returned nothing and now it's returning insert. Get or insert though hasn't changed at all. Why did we change insert of missing? It's a mystery, what happened? It's a bad mystery. Cache metrics obviously need to be able to determine whether or not the item it's interacting with has already contains the item or if it's a fresh insert. So we're using the needs of consumers of the API to define what the API is. This is sort of in contradiction to what I said before where I suggested that you really want separation and you want items to not change unless their business policy changes. But this actually makes a lot of sense and is required. The infrastructural code which actually is responsible for interacting with the real world is going to have needs. And as long as it, as long as the API pressures are always going from the real world, from the things that change voluntarily inwards to things that don't change that often, so the implementation is going to change much faster than the definition, then you're in a pretty good state. If the cache was requiring changes on the cache metrics and the cache metrics were requiring changes from the cache, then you're in a situation where your code is in spaghetti and it's time to evaluate whether or not you've defined your layers correctly. Once again, coming back to this design, we just want the use arrows to only be pointing in one direction. As soon as you've got a loop, you've got a problem and things get more complex. Okay, so at this point, we've done enough work that we know the wrong greetings that will allow us to create a nice cache. At this point, and usually only when there's motivation is when I start thinking about defining traits for my objects. One common motivation that would actually get me to define traits before I start thinking about re-use or common ecosystem concerns is testing. Traits are the best way to create mocks and rust, and in general, I just try to stick to the philosophy of use traits as early as necessary, but no earlier, unlike some ecosystems where you really want to create an interface before you even have an idea of what the code is going to look like. Coming to rust from pretty much any language, defining traits looks a lot like defining abstract base classes, you've got a function, and you've got other functions that can depend upon those functions. That's pretty straightforward, but there are a couple more interesting properties. In this one, there are no constraints placed upon KRV at the top level, but we are saying requiring, placing trait constraints on a couple of methods. In this particular case, there's absolutely no reason to do that, it's a bad idea. When I extracted this trait, this trait, the specific implementation that I was working on required clones, so I initially extracted the clone requirement as well. The good news is that in Rust, removing a trait constraint is not a backwards, it's a backwards compatible change, so we can just trash them and call it a day. There's only one more fairly obvious property. Visibility modifiers go on trait definitions. Traits are interfaces, there is no such thing as a private method inside of a trait. So that's it. Also, implementing a cache, once you've got this, we have just moved the impo block from existing on the cache itself to an impo of a trait for the cache. As promised, because, again, our insert was defined in terms of insert of missing, it doesn't need to be defined. One slightly interesting property here is that you're not allowed to, while you are allowed to define new where constraints on implementations, you're not allowed to, unlike an impo block, a raw impo block or an inherent impo, you're not allowed to add trait constraints onto methods themselves. The entire trait implementation needs to be, needs the where clause or the trait constraint. Now that we've got a trait, we can create a cache metric type that can handle any type of cache. We've only got one so far, but that's okay, except that this does not work. Traits aren't types. Even though here we are saying that the V is, the compiler is telling us that the V is required. We're using the V, it shows up in there, but it's not present. The issue is that traits are not types. Rust only considers traits used when they actually show up inside of the implementation. Luckily the error message in this case tells us that it gives us a pointer to what we need. So we can get around this with the phantom data collage, give the compiler a little bit of hints and work around it. I'm going to ignore phantom data. This is just like a reminder that traits are type constraints, they are not types. Implementing cache still looks very similar to the way we did before. Now we've got access to the default getter method, getter insert method, so we're continuing to ignore it. One other sort of interesting property here is with the implementation delegation pattern that shows up in Rust all the time. Our cache has a get method, so when we implement a trait for a cache, for something that where the only thing we know about is that it's a cache, we just call that. And here we are tying it all together with the core implementation, the inherent impulse on the cache metrics block. The coolest thing here is we finally have some dependency injection. If you come from Java, this should make you feel a little bit more comfortable. Something else that's sort of interesting is that the Rust community has decided that functions like getters just get the same name as the fields that they get, which I love. Lots of other languages support this, but it's not actually common in their communities. And that's it. The overall implementation of cache metrics is straightforward. It just has a few design decisions that you are allowed to make. Here we are tying everything together. Is there anything interesting here? We created one new line, that was six minutes of talking, and this line is more complex than the previous code, so what have we gained? A single direction of implementation. The cache metrics knows about the cache trait, but it no longer knows about the specific implementation. The black users line is completely isolated from the implementation line, which is a demonstration of traits performing as a code type-level privacy barrier. Now the actual work that we're doing doesn't know what it's interacting with any more than it needs to. Okay, now that we've got a trait, what can we do with it? She seems so happy. A cache that falls back, this will really abstract our business policy. I really like that. We've got the type aliasing. This pretty much just allows us to abstract this fairly complex thing into something that's easier to say. Any caches, any cache. Focusing on the actual implementation, we've got a little bit of an odd shape to this struct. It's got an any cache in first position and a vac of any cache in rest position. This is an example of using the type system to avoid having to write some tests. We want every cascade cache to have at least one cache. If we had just a vac of any cache, then we'd have to write a bunch of tests to verify that this cache always has at least one cache. Here, the type system will guarantee that for us. The only other thing here is that we're using the builder pattern. We just return self and that makes it like we can do with and and and it'll be kind of nice. This is a reasonable implementation, assuming a reasonable implementation of cache for any cache, we can now comfortably double our memory usage for absolutely no gain. Which I like. Okay, so let's talk about what it takes to build an ecosystem. The most important property in a large and complex ecosystem is the ability for users to work together without ever having to speak to each other in any way. In this case, things that can contribute to that is small, genuinely public items. In general, your code will just be much simpler to understand and use if it is simple. Making items public means that it's possible for developers to use the work that you've done without communicating with you. Building your own implementations in separate crates is a great way to guarantee that what you think is actually usable is actually usable. Means that other people can come in and build things for you and you won't have to worry about it. But you still have ideally a clear definition of what the core of your code is doing. Everything is going in one direction and that is the direction that changes the least. So what have I done? I talked about leaning in the strictness, trying to encode business rules into the type and lifetime system. Keeping API small is really something that is hard to do in general and the best way to make sure that you succeeded that is to just make it as small as possible and give yourself speed bumps. You can read the rest of this, I'm out of time. That was my talk.