Video details

"Type-Driven API Design in Rust" by Will Crichton

Rust
10.11.2021
English

Rust is a unique programming language that blends imperative and functional concepts to make low-level systems safer. However, compared to other modern languages, designing APIs in Rust requires a fundamentally different mindset -- for example, designing without classes or inheritance. In this talk, I will live-code the design of a simple Rust API. Through the evolution of the API, I will demonstrate how Rust’s type system (especially traits) can be used to design interfaces that cleanly compose with existing code, and that help API clients catch mistakes at compile-time.
Will Crichton Stanford University @wcrichton
Will Crichton is a 6th year CS Ph.D. student at Stanford University advised by Profs. Pat Hanrahan and Maneesh Agrawala. His research is about applying cognitive psychology and programming language theory to understand programmers and to design better programming tools.
Recorded at Strange Loop 2021 https://thestrangeloop.com

Transcript

My name is Will Creighton, and I'm a PhD student at Stanford. I do research about using cognitive science to make better tools for programmers. And as a part of that research, I think a lot about the tools that we use and the languages that we develop in and how that shapes the way that we program. And today, one of the most foundational concepts in programming is the API or application programming interface. Because programming today is about weaving together dozens of tools and languages and libraries, all written by other people. And the API defines how you work with these tools. And so API design is a really critical problem. Now, I've personally been programming for about 15 years, and I've seen a lot of APIs in that time. Apis in object oriented languages, functional languages, systems languages, and dynamically Typed and statically Typed contexts. And so when I picked up Rust about seven years ago, I found that Rust APIs were this really interesting fusion of ideas from all of these different languages, as well as totally new ones that I'd never seen before. And so my goal in this talk is to synthesize some observations about what makes API design in Rust different from other languages. So Rust, if you're not familiar, is a relatively new language for systems programming. It fills the same niche as C Plus Plus, and Rust has this really powerful type system that's used to make programs safer. So, for example, it is much harder to have a null pointer dereference in Rust than in C. But Rust also has a very different flavor of software architecture. So, for example, it doesn't have a concept of classes or inheritance, and instead, the core mechanism for organizing APIs is the trait, and that's where we're going to focus today. So the way this talk is going to go, I'm going to create a Rust API for you live on stage. And my goal is that in the process of iterating on this API, you can really see how each construct of the language is used to accomplish some aspect of API design, and also by live coding, it'll pace the talk a little bit. So even if you don't know that much Rust or none at all, you can still keep up and understand what's going on. So let's get started. All right. The example we are going to use today comes from some of my travails in Python. A common issue I have is, let's say I have a long running for loop. So from time import sleep for N in range 100, sleep for 1 second. When I'm running this for loop, I have no idea how long it's taken or how far I'm in, how far I have left to go. And that's a very frustrating experience. So one of my favorite utilities is called tqdm. All I have to do is say, four N in tqdm range 100 sweep for one, and then I get this awesome progress bar that shows me of nice visual representation, the percentage through the number of iterations, how far I have left to go. And this is just a stupendously handy little utility, and it's a quite nice API. All I have to do is wrap the Iterable thing, the range and then otherwise leave my code unchanged. And I get this nice little progress bar. Some other nice things about this API are that it works with other kinds of Iterable objects. So for example, let's say I have the count Iterator which goes from zero to Infinity, and I say four n in tqdm count sweep one. Then I still get some measure of progress. It doesn't have a bar because this is an unbounded Iterator. There's no technical end to this, but it still tells me, at least, for example, the number of iterations, which is quite nice. However, it's a Python API and so it has some drawbacks. For example, if you use this in an ill Typed way, like four n in TTDM of one, then I'll get a runtime type error. Once I execute this code saying that Int object is not Iterable, and I can only see that error once this actually executes, but at least I get an error. Sometimes there are things that are a little bit more subtle. So if I say four n in tpdm range 100 comma bar format equals bar and then sleep for 1 second, then this changes the visual display of the bar. So now I just see the bar and nothing else if I'm a minimalist and that's all that I need out of my progress bar. But what happens if we try this with the other Iterator. So I replace range with count. I don't see anything, and that's very confusing to me as a user to call this API and then just observe nothing happening. I'm wondering, is this broken? Is it hanging? What have I done? And the answer is that bar doesn't exist for these unbounded iterators. It just doesn't display anything in this case. And so this is a really common issue in API design. You have a combination of options that don't make sense together that each one individually makes sense. It's nice to work for unbounded iterators. It's nice to customize my bar format, but if I use them in the wrong way, I get not even an error, but just something that seems to not work. And so these are examples of both good and bad things that arise in API design within Python, and we're going to design a progress bar in Rust and see how we can address some of these issues along the way. So let's get to rust. First we need a Rust project Cargo new Progress. We will open up the Temp progress sourcemain, so this is going to be our main, and then I'll keep the console over on the right where if I say cargo run Q, I'll get out. But Hello world. So the default application is a single file that contains a function main, the entry point of the application that just prints out Hello World. So when I start designing APIs, I like to begin with a very concrete example of the functionality that I want, and then generalize that. So let's do that. First we need a vector V, one, two, three. Then we need a for loop four n in V ITER, and then we'll have some kind of expensive calculation of N. So we need to define that function function expensive calculation. And that's just going to take as input a pointer to a 32 bit integer I 32, and it won't do anything with it. It'll just sleep. So we'll add some imports, duration, thread sleep, and then we will sleep for 1 second. So now if I run this, we wait for 3 seconds and then the program completes. So we have a basic example of a long running for loop. And now we want to add some kind of a progress bar, so it won't be as fancy as tqdm, but it'll be nice enough. So we'll say let mute I is equal to zero. This is the state of the progress bar. The number of iterations mute means that we can change the value of I and then we'll print out star repeated I times, and then we'll increment I by one. So if we run this, we'll get nothing. One star, two stars, and we'll start I at one, because that at least gives us one star to begin with. I think that makes a little more sense. And now we have a basic progress bar, but it's kind of ugly puts the things on a new line so we can do a little bit of a hack. Here, let me just copy my mystic string from my sticky note, since I didn't want to commit this to memory clear and string equals this Anzie terminal escape code. Don't worry about it not important to the talk, but now if I run this, I get a nice little progress bar. Cool. So this is the start of the functionality of our API, but it's not an API yet. This is a very concrete piece of code. So what I like to do is look at things in this API that are not essential to the way that it works. So for example, the choice of vector V shouldn't be just one, two, and three. We should be able to work for any vector, so we can make a function fun. Progress takes a vector V of I 32s. We'll take this code down here, copy it up here, and then add the function call. So each time we change the code, we're not changing the functionality or the sort of observable behavior just changing the way that a user would call into this functionality. And now I want you to take a look at this progress function and just think to yourself for a second. What are other aspects of this API? They're not essential to the progress bar. I realize it's after lunch, so we'll try and wake everybody up a bit. Okay. So one thing is the type of elements of this vector. So a progress bar should of course, work for vectors of strings as well as vectors of numbers or other types. So we can generalize that by adding a type parameter. So progress of T. So T capital T is a type, and then this takes us input any vector of type T. So if we try and compile this, what happens? We get an error mismatched types expected when we called expensive calculation a pointer to a number, but we got a pointer to a random type. So this correctly caught the error that we need to simultaneously generalize the function that we're calling inside of here along with the parameter of the vector. Now one cool thing to note here for those of you who know more C, if you try to do this with templates in C, you would only get this kind of error when somebody actually calls the function a big difference with Generics and Rust and other languages that support this. Similarly is the definition of the function is what gets type checked. And if that patches type checking, it'll always be fine whenever you call it, which is a nice handy feature to have, especially from an API perspective. So the client never has to deal with, like weird type checking errors that they use your API incorrectly like that. So we'll make this a function parameter. F is a function from pointers to T to nothing. Then take this and add it down here. And then we overrun this again back where we started with a functioning progress bar, and with a little higher order function. Now. Still, we can keep generalizing. The choice of vector here doesn't seem quite right. This should work for anything we can put into a for loop. For instance, let's say I have a hash set. H equals hash set new, which we will need to import, and then we insert an element in there, and what we want to do is call progress of H, and then again run our expensive calculation. But what's common between a vector and a hash set? What does it mean for something to be put into a for loop? Well, this is where traits come into play. So I'm going to go to Google and search for Rustiterator. Click on the first result, and in the standard library documentation, we see what we need. So this is a trait. Traits in Rust are a lot like interfaces in other languages. They define a set of requirements that a type should implement, and so the Iterator trait defines what makes things Iterable such that you can put them into a for loop, and the requirements of the Iterator trait in Rust are these two things. First, we have this function next. The way that this works. I'll show you the example in the documentation. You have an array a with elements one, two, and three. Then you can create an Iterator on that array by calling a dot ITER, and that itself is a mutable structure. And then each time we call next, we get out a pointer to one, then a pointer to two, and then a pointer to three, and that is wrapped in sum, if we get back an item or we get a nun if we've reached the end of the stream. So that's the behavior of an Iterator in rust. And the way we formalize that at the type system level is that function. Next takes its input a mutable pointer to self, which is the object that implements this trait, and then it returns option. So some or none of self colon colon item. And that is a reference to this thing. Up here the associated type, which is the type of elements being iterated over. So if you have this array here one, two, and three, then the type of elements coming from this Iterator are pointers to numbers. And generally if you have a back of T, you get a pointer to T's is what comes out of your Iterator. This is a little bit different if you have an Iterable interface in other languages. Usually this type might be like a parameter to the interface or something like that. So this is one of the more subtle parts of traits and rust, but the main thing is just it's another part of the interface. It's another requirement that an Iterator has to declare what the type of this item is. So the way we can use this going back to our code is to add another type parameter. So progress now takes an ITER type, and that's going to be the first argument. Iter of type ITER. But in order to put this into a for loop, we need to be able to say what makes this Iterable. And we do that with a where clause where the type capital I ITER implements Iterator. And we also want to constrain that the thing that comes out of this Iterator is an item of type T. And now this function just takes type T. So with this now we can change our progress call from v to V. Iter because a vector itself is not Iterable, but an Iterator on a vector is and the same thing for the hash set. We'll run this and we should see two progress bars, one for the first vector and then one for the hash set. Awesome. So we've used the concept of a trait and a type parameter that is bounded on the trait with this where clause to make this progress function generic over all kinds of things that are Iterable. And this is actually a little too verbose. We can get rid of this parameter T and just say that this function takes its input either colon colon item. I think that's a cleaner and clearer way to describe what's going on here. And again, the program type checks and runs exactly the same way. So this is pretty good. We have a decently generic API, but it's kind of gross. Still like this higher order function thing. It's very intrusive. I would say I liked the tqdm style where you just wrapped the Iterable and everything else was left unchanged. So we're going to move to that style of API, which requires a bit more of a change. We need. What we want is something like this four n in progress new of Viter. Then we call the expensive calculation. So this is what we want. How are we going to get there? Let's delete the rest of the example code. We need some data structure that represents this Iterator. So we'll make a new struct progress that contains the Iterator that it's given as a type parameter. So ITER of type ITER, and then the state of the Iterator I of type U size, which is an unsigned integer where the number of bits depends on your architecture. It's an unwieldy name, but that's what they picked. And then we need two things for this progress data type. First is a constructor like the colon new, and then something that defines this as an Iterator. So we'll say imple ITER progress ITER pub function new takes as input a concrete Iterator of type capital I ITER returns self, and that object is progress of ITER comma I colon zero. Okay, there's a lot of new syntax here, so let's walk through this. The Impul block is a really key concept. It associates a method with a type. So this function new is now associated with the type progress ITER. It's a static method because it doesn't take self as input. And so the way that we call it is with the double colon progress colon colon new. Another thing is that this implement block is quantified. So we say for all types, ITER implements progress of ITER. So that means anything we could put here is now a valid call to this particular progress. And lastly, this function new the capital Sstel type means whatever is being implemented. So it's just progress of ITER. And because rust has a functional flavor, we don't need to use a return keyword. We could just put an expression and that's returned directly from the function. So a lot of syntax, but it makes the code a little bit shorter. And next we need to turn this progress structure into an Iterator. So we'll say imple ITER Iterator for progress of ITER. And then we also need this bound where ITER is an Iterator. This needs two things a type item and a function. Next, going from mute self to option of self item. These are just the requirements of the Iterator interface, and the default behavior of our progress bar is just to pass items along. It doesn't change them, so we can call self ITER next. But before we do that, we want to display the state of the Iterator. So I will again copy this code up here, and this time we will replace I with self I because we have a data structure that represents the state. And lastly, the type item is whatever is returned from the inner Iterator. So we'll say ITER colon colon item again, this is a fair amount of syntax, but what's going on here is we are implementing a trait for a type. So now the compiler understands that the progress data structure is an Iterator and can be given to a for loop, and we have to satisfy the requirements of the interface, which are the type item and a function. Next. That doesn't do much but just really copy over the code we were doing before with a little bit of glue, and now we get rid of this. We have once again no change to the behavior of our code, but still a much nicer API in my perspective. And that's really the essentials of traits. You define an interface. You use these imple blocks to either directly attach a method to a type like New or to implement a trait for a type so it can be used in certain ways. All right, we're at about the halfway point in the Mark, and I think these are the essentials, but now I'm going to dive deep into some more advanced use cases for typedriven API design and how we can use traits in some more creative ways because this is similar to tqdm. I would say this has a pretty similar flavor minus some of the features. But for the first thing I want to show you is I don't like this style of API that's very inside out. You have vot ITER and then progress new. And so the flow of control goes from the innermost argument to all the functions you wrap it with. There's a reason why we have the dot operator or the pipe operator or all these things where the left to right reading order of the code matches the control flow, the execution order of that code. And so what I'd like to do is write this v ITER progress. Now think for a second about how you might implement this in any other programming language. V. Iter the type of iterators over vectors or hash sets or whatever. Is a type implemented in the standard library. I don't own it. I can't call up the rest developers and ask them to implement that method and just toss it in the standard library that'll take a little bit of time more than we have here. And in general, when you have a class based system, it is very hard to extend types that you don't own, and so you have wrappers and adapters and all this really ugly crud that's needed to compose cleanly with existing code, but in Rust with traits, it actually becomes possible. I'll show you how it's like this. I make a new trait progress. Iterator extension or that requires a function progress that takes an Iterator and returns progress of that Iterator. Then we will implement for all iterators progress. Iterator extension extra is just a convention in Rust for ITER. Function progress, self, arrow capital P progress self. And this won't do anything complicated. It's just calling the constructor, but that's it. It's almost it. You actually have to add this sized bound, which is a bit of a small memory thing. It's not super critical for this talk, but now it's actually it. That's pretty cool. I just attached a method to a type that I don't own, so that's one basic use case for traits and rust is these imple blocks are a lot more powerful than at first glance. And let's actually take a really close look at this line. It says for all types, ITER implement the trait progress. Iterator extension for that quantified type. So I me on this stage, I just singlehandedly added a method to every single type in the Rust universe that has ever existed or will ever exist. You don't believe me? I can say, watch this. Let X equals one progress. Let y equals blah progress. All of these are valid, well Typed calls that are unused warnings, but it's actually a well Typed program now. Okay, you're probably this point thinking that doesn't seem sane at all, and you will get an error if you try and iterate over these things. So if I say four blah in Y or X, then I will get an error. Expand this out that says in this very large blob of text, essentially that a number is not Iterable, and therefore you can't do that. Oh yeah. An integer is not an Iterator. That's the text up here. But the cool thing here is that you have the capacity to extend all of these types with new methods if you want. And that allows you to get this really seamless dot based notation. And for those of you who are sweating a little bit, you're like, oh, my God, I don't want people randomly attaching methods to my types. That sounds terrifying. There are ways of controlling this within the language. So, for example, a user has to import this trait into their module in order to access this method. It's not just there by default. And also you can't directly attach methods to types that you don't own. You have to do it through traits, and there are methods for resolving ambiguities and stuff like that. There are details that I'm not covering, but the high level up point is that the trait combined with this symbol block allows you to do this by attaching these methods. And if you might say, I want to disallow one dot progress, you shouldn't be able to do this on types that aren't Iterable then you could add a where clause. You could say where ITER is an Iterator, and that would disable. This would only attach this method to Iterable types. That's probably what I would do in progress, but just as more text for the purposes of this demonstration. So I won't do that. All right, so that's pretty neat. That's cool thing number one. Cool thing number two is I want to talk about how we deal with splits in the API. A common issue is I have, for example, for iterators. Sometimes I have iterators that are bounded. I know how long they are. I know this vector has three elements, and so I want to draw a progress bar with bounds on it, but sometimes they're unbounded. So in rust that would look like four n in zero the Iterator from zero to Infinity. Progress also doing our expensive calculation. So one of these iterators is bounded and one is not, and we want to have different functionality in both cases. How do we accomplish this in the Rust type system? Well, first, we need to understand how does Rust represent the concept of an Iterator that may or may not have a length. We go back and there is a trait for that exact size. Iterator so exact size Iterator. Let's read through this definition. First, it requires that anything that implements this trait must also implement Iterator. That's what this little colon means. So this might seem a little bit like inheritance. It should remind you of inheritance because you're saying anything that this is like a class that extends the Iterator superclass. But the big difference and what's different from traditional objectoriented languages is you are inheriting a specification, a set of requirements. You are not inheriting an implementation of methods, and so you don't have to have any convoluted runtime mechanisms for following an inheritance chain, or worse, linearizing, a multiple inheritance chain, or any complicated dynamic dispatch. So it's always clear at the time you compile the code which method you're referring to. If you say call a method, instead, you use an inheritance style thing solely to express requirements, and that ends up, I think, being a lot cleaner in practice. And so the idea here is an exact size. Iterator is an Iterator that also has these two methods where the relevant one is function length. That tells us the length of this Iterator. So let's implement that. First we need something that represents the possibility of having a bound. So bound is option of U size, where option is some or none. And by default this will be none. We'll assume by default we have an unbounded Iterator, and then we want to implement a method progress ITER. In the case where ITER is an exact size Iterator. We'll have pub function with bound that takes the immutable owned version of the Iterator return self. This is kind of like a factory feel self bound equals sum self. Iter Len. And then return self. So what this imple block is doing? Is it's saying add this method to the progress data structure? But again, this is the power of the imple block. Only add this method where the type on the inside implements a trait, so you can only call with bound when you're iterating over something that has a bound, and hence we are allowed to access the dot Len function inside of the implementation with bound, then we need to have something that prints out the bound, so we'll make a quick display. So match self bound. There's a match expression. If we have a bound, then we're going to print out little brackets, and then we'll do some fun. We'll repeat this self I times, and then we'll add a space for bound minus self itimes. And then if we don't have the bound, we will print out exactly what we had before. Except I'm going to move this clear logic up to the top. So now we get that shared between both of the iterators all right. And this is almost a syntax error. Good. So now if we compile this, we are back at the same place we were before. Let me get rid of this one up here. If we iterate over the vector and say progress withbound then now we have our nice little bounce star. And importantly, thank you to my point about catching helping API clients catch mistakes if you try and call that on an unbounded Iterator. So if I say progress with bound where this is over the dot dot which doesn't have a bound, and then I try and compile this, I will get a nice big error which starts with the method with bound exists for struct progress, but its trait bounds were not satisfied and read out range from doesn't implement exact size Iterator. That was the bound that wasn't specified, so that's pretty cool. We've now used the type system to help the user catch an error. This is a method they can only call in certain cases, and the error message, albeit a little bit verbose, does ultimately tell us why. This is the case that the Iterator doesn't implement this certain Iterator. Now as we're getting into more and more advanced territory. Huge caveat. This still isn't a great error message. There's a lot of text in here that takes someone time to parse through. And a big issue with type driven API design is you rely on the compiler's generic error messaging system to help users understand their mistakes. It will catch their mistakes, but it's often up to the user with the error message to understand them. And that is in itself often quite a challenge, especially when error messages fall off a cliff in certain more advanced use cases. So that's a trade off that you, as the designer of an API, have to decide. Maybe it's better to have a beautifully crafted runtime error than to have an extremely obtuse compiled time error, but at the very least it does make sure that the error is caught. You're guaranteed not to encounter this problem at runtime that's part of the tradeoff you're making. All right. And so for my last trick, I would like to talk about a really key problem. So let's say we have extend this with the ability to customize the delimiters. So we're going to add a pair of characters that by default will be the square brackets, and then we'll print out those square brackets down here. At this point in the talk. I also apologize. These are cross cutting changes that will require going across the code. So this is going to get a little complicated, but I'll try and review the code after I make each big change to make sure we understand what's going on. So I'll add those delimiters to the print. Oh, right. I still have this compile failure. Now I have this basic square brackets thing, and then we want to add a method that's going to let us change these delimiters so imple ITER progress. Iter pub function with delimiters that takes a self and delimiters as a pair of characters. Return self and then sets self Dilims equals to delimbs just like the bound thing, and then return self. So if we call this, let's make some delimiters. We can say let brackets equals angle brackets and then progress withbound with the limbs brackets. If we call this now we get our customized progress bar with a nice little different visual display. But if we try and call this on our unbounded Iterator. So if I say zero progress with the limbs brackets, what happens? I don't see any change in the output again calling back to the error we saw tqdm, and so this might be confusing for a user. I tried to configure something I thought would make a difference, but I didn't see any difference in the output. Why? And the core issue is that progress is now a stateful object based on whether or not it is in the bounded or unbounded state. And these methods the width of limbs method is allowed to be called regardless of which state you're in. So let's change that. I'm going to show you a particularly advanced technique called type state, where we encode the state of the progress bar in the type system to do that. First we need each state, so we have an unbounded state and a bounded state that has a bound as a U size or a set of delimiters as a pair of characters. Then we are going to add a type parameter to progress of the bound, and that's what's going to sit in this bound field. Excuse me, then we need something that ties these two data structures together. Of course, it is a trait progress display. We will call it that will print out something to the screen function display takes its input a pointer to self, and then the progress bar. I'm going to lightening speed here. So apologize for any mistakes. And then we're going to implement this trait imple progress display for both types unbounded, which is going to have this type signature. And then we'll also implement this for the bounded type. And the implementation is just going to move the code that we had in our match statement down here. We're going to move that up here. So now if it's bounded, we'll call this print statement and just update our fields as necessary. So instead of self I this becomes progress I and becomes selfbound. And then this print gets put up here, which again becomes progress to I make sure we add the appropriate markers so we don't get a syntax error. All right. So now we have this trait implemented for both of our States, the common functionality. Now we have to refactor the rest of our code. So instead of referring to progress, we have to refer now to the additional state we added to progress. So initially progress will be unbounded so that's the initial state bound is unbounded. And then the other key thing is when we call with bound, this will change the type of the progress bar. That's the key aspect of type state. When we are in the unbounded state and you have an exact size, Iterator you can call with bound that does not return self, but it returns progress of ITER comma bounded. So we'll say let bound equals to bounded of bound is self. Iter Len. And then the delms are by default our pair of brackets, and then we will return a distinct structure, a new type I self I ITER self ITER, and then the bound. And then finally, the width delims method is only implemented for the bounded state. And so we can only call this self bound to limbs when we know that bounded is the type. Bounded is the thing inside of our progress data structure. All right, we got to keep going. A lot of changes to make. Next, we have to change our Iterator, which works for both bounds. So that becomes a type parameter. And then we'll say bound implements progress display and then self bound displayandself. Good. Last thing is the trait we added also returns the constructor, which is unbounded by default. Let's see if it compiles. It may not compile. Oh, I almost forgot one thing. This also needs to be sized. Don't worry about why clear cargo run Q. Oh, no, I missed something. All right, we're going to figure this out. I define unbounded twice. You mean implement display for unbounded unbounded. I see. Thank you. Wow. I have such a good audience. Oh, my God. Wow. Okay, props to you. Thank you. Thank you. In my moment of need, the audience came through. All right, well, clearly you got it. Great props to all of you. So that's pretty cool. Wow. We did it. And importantly, the API down here didn't change. But instead, now what happens? Let me change the code first, if I change this for loop, and instead of if I try to say progress with the limbs. Previously, this was a valid API call. But now if I try and compile this, I'll get out an error that says no method named with the limbs found for struct progress unbounded. And then down here in the note it says this method was found for progress it or comma bounded. So that's cool. That's type state we've taken whether or not we're in a bounded state, turned that into a type, these two distinct structs and then made it a type parameter so that we can change the type as we transition through our state machine. This is actually a really cool technique that's used in other APIs. If you've ever heard of the Rocket Web server, they have something similar for the different stages of starting up a web server. Embedded computing libraries will use this to represent, like the state of your pins on your Arduino or something like that, your Raspberry Pi. And so this is a neat way to again help the user catch mistakes with a state machine. So, for example, with bound must be called before with delimiters. And then rather than being a comment in your documentation, this is enforced by the compiler. That's what it means to have typedriven API design. Okay, that's all the cool stuff I have time for. I'm sure you have other questions, or maybe comparisons to other languages, and I'm happy to take your questions offline, but just to reiterate what we saw today. So the main idea is traits. Traits are the foundational unit by which you organize an API. They define an interface, and then you can implement those traits for types, but in rust. And this is also true for Haskell and other type class or traitbased languages. You have this flexibility to implement traits for types that you don't own. You have this flexibility to be parametric over these interfaces, and you can combine these where clauses in really creative ways to help users avoid mistakes to make sure you can only call a method when certain capabilities exist or when your object is in a certain state. For example, that's what we saw with type state. And so these are all instances of API design that I've seen in different rust APIs that I've used. And hopefully this has distilled them down into a sort slightly simpler, more pedagogic form where you can really get a sense of how these evolve rather than just seeing the individual instances of APIs. Yeah, that's about it. Thank you for listening.