Evolving API design in Rust
CC Attribution 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Heath, Simon Nicholas
As Rust is a young language containing many innovative features, questions about how to structure Rust libraries and API's are common. Heavy use of metaprogramming and trait constraints can make libraries hard to understand and use, but also bring great power to reason about programs at compile-time. How do you design a library that exploits the power of Rust without making new users say 'This is way too complicated to bother with'? We will discuss these issues using an existing Rust crate as a case study in both designing an API and how it is influenced by the API decisions of the crates it uses as dependencies.
 As she said, I'm Simon Heath. I am talking about evolving API design in Rust. I've been using Rust pretty much since 1.0, and I'm interested in programming languages, and I'm interested in compilers, and I'm building infrastructure, and making video games. So I took these things and put them together, and made a game engine because it's more fun than making a game, called GGEZ. And this is the first major Rust project that I had been working on, and it's also the first major open source project that I've worked on. And the goal is to make it easy to make 2D games, because this was something I wanted to do. It's a good way to learn Rust. And whenever someone says, I need an idea for a project to learn Rust with, I can say, you should make a game. It is based on a Lua game framework called Love 2D, which also has a similar goal of making 2D games easily, which is to say that I went through the Love 2D API docs, function by function, and wrote everything in Rust. Because I didn't really have a great plan for how to make a good 2D game engine. I just wanted something that was simple and would work and was easy to use. So I knew Love was simple and worked, and I'd used it before, so there we go. GGEZ is actually used in a few games made by real people who are not me, which is awesome. It's nothing so far. There's anything super huge or complicated besides Xemaroth, which is this one which has been worked on since before I started working on Rust. But hopefully someday, I'll actually get to write games in GGEZ as well. That's my goal for 2019. So to do all this stuff, GGEZ brings together a lot of other crates from the Rust ecosystem and has to take all of these libraries and make them play nice with each other and convince the ones that don't have cool logos to make cool logos, so I can put them on the next slide. It has to take these crates and have them interoperate. It has to take whatever API they expose and be able to wrap it up in a consistent way and make it easy to use for Rust newbies. And it has to actually be able to use all of these crates successfully. And so I have gotten to be very good friends with some of the maintainers of these crates because I would keep submitting bug reports or I would keep saying, I need to be able to do x. How do I do x? And usually they tell me, and life is good. But I wanted to do this talk because I also hang out on Reddit and IRC a lot and probably too much. And I keep seeing things like this. So everyone who first learns Rust and writes something big has to ask for advice on it. And so it was weird because I don't see this a lot in Python or C sharp or whatever. Maybe the world would be a better place if people did do this in those languages. But either way, people who learn Rust seem to have trouble figuring out how to write Rust APIs. Or at least they have anxiety about writing Rust APIs. They keep asking, how do I write idiomatic Rust? And so that's what I want to talk about. So how do we design a good API in Rust? In my case, I didn't have to design an API. I just copied an API and made it Rusty. So that was, let's take a look at the API I copied it a little bit. But I'm going to start with some GGEZ examples. And then I'm going to look at some of the other crates that GGEZ uses and how those sort of look from an end user perspective and what's good about them and what's bad about them. So here is a very simple love 2D game. It's all in Lua. I don't know how many people out there know Lua. But we have some functions. We have load, update, and draw, which are the sort of fundamental parts of your game. We have a global player, which is just a table, dict essentially. That's my Python showing through. And we have update, detects if you are pressing buttons and changes the world state if you are, and draw, draw stuff. And great. And these are basically callbacks that are loaded by the Lua interpreter. And love basically has a version of the interpreter that is built with a bunch of libraries and looks for these callbacks and loads them and runs your game. So this is pretty different from how Rust works. I mean, there's no curly braces at all. But we have these sort of magic callbacks that the interpreter looks for. It's dynamically typed. There's mutable state everywhere. So it's kind of, I didn't even know if it was going to be possible to make this in Rust. I was like, well, maybe. I mean, look at just the draw, the love2D draw function. We have four different overloads for the draw function. I like your expression responding to this. We have some drawable object, or you can replace the drawable object with a texture and a quad, saying which part of it to draw. You can have a transform, which is a structure that basically bundles up all of the drawing parameters. Or you can just list all the drawing parameters for that are possible individually. And it also turns out that you can actually omit the ones at the end. And they'll just default to 0 or 1 or whatever is appropriate. And so you can just leave all of those off and just have x and y and r. And it works fine. So I was kind of like looking at this and saying, well, I just want to make something work. So worst case, I'll just make a separate function for each of these variants. And so I started with that, and I sort of squished it together and got something like this. So we have a struct that has all the draw parameters in it that you can have. And we have a function that takes a drawable object and drawparams and a context which just holds on to the graphics context state and appears everywhere. And it draws it based on whatever parameters you give it. And then, well, OK, we have a simplified function that just takes a destination point under a rotation. And you can use that if that's all you need. And if you need the full power function, then you can use that instead. It was like, OK, fine. Eventually, I discovered that the default trait and the struct update syntax exist. And you can do something like this, which actually is halfway decent. It's not great. It's not terrible. But it works. It's not too pretty. I never really liked it. But something that I realized as time went on is that GGEZ is an opinionated framework. And so lots of people have opinions about it whenever they try to use it. It's actually fairly low level. It doesn't provide animations. It doesn't provide a physics engine. But, and so everyone says, oh, why don't you do it this way? Or why don't you add this? Or why don't you not add that? But nobody in the last two and a half years or whatever has actually complained about this horrible hack. It's not a problem. It actually has quite a few advantages. It's simple. Even the most basic Rust programmer can understand it. It's completely obvious what's going on and where the data is coming from, where it's going, and where it's used. And with this nice syntax, it's even not too terrible. So it works. So the harder case was dealing with Love2D's sort of callback structure. And I ended up starting with something like this, where we had a trait called GameState. And I should have cut out all of the inconvenient code. But it provides load, update, and draw methods that are just like Lua's or Love2D's load, update, and draw methods. And then down here, you have this game struct that is generic on the type that you implemented the GameState trait for. And it just creates your GameState by calling the load method. And then it has an event loop inside it that calls update and draw and takes keyboard events and all that stuff. And so this is the closest I could get to something that looked like Love2D, where it just had these magic callbacks that did everything for you. And it sucked. Nobody hated it. Or nobody liked it. I didn't like it. I got tons of questions like, oh, how does the GameState actually get created? Like, where do I put the new method? How do I? Like, who owns it? It's owned by the game type. How does it know what type to load? Well, you have to annotate it with that generic parameter. And like, where does the context get created? Well, it gets created in the game. And it was just complicated and nasty. And eventually, I wanted to be able to even take apart the event loop and let the user sort of write their own with their own update functionality if they really wanted to, because Love2D does allow you to do that. So eventually, I ended up with this, which is kind of similar. We have an event handler trait. It defines update and draw methods. But the GameState is just a struct you create. And there's nothing special about it. You create a context, which is sort of the handle to all the GTEZ library functions. Like, it handles the sound state, the window state. It talks to the operating system, et cetera. And you have this event.run, or colon run function, which is literally what you would just write. It's a wow loop that pulls the operating system for events, calls event handler update, and then calls event handler draw. And it just does that. And now, by trying to do less magic, everything becomes way better. And so, and I didn't like, it was like, this doesn't really look rusty. This is kind of dumb as a pile of bricks. But how do we design a good API and Rust? Everything, a good API is still a good API and Rust. That was the sort of what I figured out. Love2D started off with a pretty good API. I turned it into Rust, and it's still a good API. Rust doesn't add or remove anything too magical from it. And when it down, keep it simple. I like the term complexity budget that the last talk used. Because putting my complex budget into trying to make it look exactly like a Lua API wasn't worth the extra complexity. So another problem that I deal, often when people are trying to get into Rust game dev, is that some popular crates are very hairy. So they say, well, what are my options for drawing graphics? And someone mentions GFXRS, which is awesome, and which Love2D uses. And they go to the docs, and they see this. Now, I don't know about you. Usually I call it quits after only seven or eight associated types. Here we have 12. Or they say, OK, well, how do I do matrix math? Well, look at an algebra. And they see this. This is the matrix type. This is what's even going on here. It's almost impossible to read. When I see stuff like this, I call it trait salad. Because it's just a pile of different stuff all mixed up, and you can't make heads or tails of it. But if you're trying to learn Rust, and you come at this, and this is what you see, then obviously the creator is this guy, who either is just a mentat, who is one with the computer, and knows everything there is to know about everything, or he's just a sadist, and he likes torturing the users of his crates. I mean, let's look at, we saw his math framework. Let's look at his physics engine. This actually looks kind of reasonable. I mean, it's just a bunch of methods. This is part of the collider type, I think. And it's pretty obvious what's supposed to connect. So it's not that he's a sadist, and it's not that he is operating on some plane beyond human comprehension. So what's actually going on here? We have a bunch of traits. They all have really complicated bounds. We have one here called abstract magma. I usually think of this when I see that, or maybe this. However, it turns out that what I should have been thinking of is this. It's a math term. And so what an algebra is doing is teaching the Rust compiler how to do fundamental math by encoding it in the type system. And that's not something that would have ever occurred to me to do, but it also means that it completely rules out a lot of math errors at compile time. If you have a transform vector and a scale vector, you can't add them together. The type checker catches it. And GFX is similar. It's doing something kind of like that, trying to encode the state of a graphics card in the type system, which is really complicated and low level, but it ends up catching a lot of bugs. So these aren't bad APIs. They're just very sophisticated and specific and low level and are geared towards a certain type of use case. I've talked to people who have a formal math background and use an algebra, and they love it. They don't see this magma, but all of this pops out at them and they're like, oh, I know how everything fits together. So the next lesson is know your audience. Who do you want to be making these crates for? What do they want to be doing, and how do you make what they want to do easy? If you're making something for yourself and you have a background in math, then you end up with an algebra. And if you don't, then you end up with something else. So also, as an audience, know your tools. It's a lot easier to understand why an API is the way it is and where to look when you need some functionality when you know who the writer is making it for. Maybe it's not for you. Maybe it's something you can learn, but either way. So we've discovered that API design in Rust in hard is hard. It's hard in any language. Rust seems to make it trickier, though. Why do people get such anxiety about it? And what can we do to make it better? Well, in Rust, API misfeatures are actually really nasty. This is true in any language, like Java or Python or whatever. But Rust is good at making things subtly terrible in ways that aren't obvious to people who don't know Rust very well. For instance, GGEZ has a submodule for loading resources from file paths without keeping the files in some platform-specific location that's different on Windows, on Mac OS X, on whatever. And so it uses a create to just ask the operating system what paths it should use. And the create is called appters, and it works basically like this. You create an app infostruct, which has the name of the app in author. And the operating system has some specific location based off of this information that it uses to store images or fonts or whatever. And you just say, OK, get me the user config directory for whatever operating system I'm running on. And it gives you a path. And that's it. So the API is this. There's more than this, but this is the part that GGEZ uses the most. And so we have a struct, and we have a method, and the struct has some static strings, and everything is good. And so I wanted at one point to write a program that loaded these paths from a config file or something. And so I had something like this. We have author and app name, which are our own strings. And we feed them into app info, which has static string slices, except these are own strings, but these are static string slices. You can't get there from here. How do you do it? Well, you can probably figure it like you go on IRC and you ask, how do you turn a own string into a static string slice? And someone will say, well, you can probably do it with unsafe, but we don't like doing that, because the point of unsafe is that you never need to use it. It's there in case it's like the shotgun on the wall in your dad's room. It never leaves the wall. It's there in case you need it. You just never need it. OK, so we'll fix that somehow. Let's just get on with the file system code. I want to be able to write something like this, where I have several virtual file systems of which one type will be able to load things from the disk, just from the normal file system. And one will load things from a zip file and pretend it's a file system that's been overlaid on top of it. And this is handy for games, because especially if you want a game that's moddable or something, you can have all the formal game resources in one zip file, and then you can have a mod that just replaces a few of them, either in a zip file or in a directory. And it's nice. And I wanted to just have the file system be a trait object that exposed a few methods. And we just have a vec, and we just look through each of them in turn and use the first one that has a file. Great. So I found a crate that looks like it does this. And it has a trait like this. And it's called VFS. And so we have the path which this crate is. The path uses the path as the entry point kind of, like you use path, and then you open a file, and it's very object oriented. But it works OK. And then we have an associated file and metadata type, and then you create a path through this method on the trait. And it takes something that can be turned into a string and gives you the path type for that trait. Well, hang on. I wanted trait objects. And this has a generic parameter with a trait bound, but you can't turn that into trait objects because a trait object needs a v table. And the compiler will make one version of the path method for each type that you specialize it with. And so you can't make a trait object from that. The compiler would have to be able to look into the future and see what types of T you would use it for and compile those as well. So you end up with these situations where perfectly reasonable explanations or perfectly reasonable design decisions just get into weird places. And it's impossible to do anything about it. And so these are the easy cases. If you try to use GFXRS or Tokyo and you end up in one of these weird corner cases, then even Rusty can't figure out what's going on and tell you what you're trying to do incorrectly. And so you just end up with these weird situations that aren't obvious, or at least aren't obvious if you're not looking for them, where you want to do something that the create author didn't think of you trying to do, and Rust isn't allowing you to do it. So they made some design decision that had unexpected consequences. Unfortunately, I don't really have a great answer to this one, besides iterate, because people will always come up with interesting use cases that you never thought of. I've spent the last two weeks with some trying to get GGEZ to work on iOS because someone really wanted it to, and I never really considered doing that before. So your users will always come up with something that you didn't expect, and the nice thing is it's not a terrible thing to redesign an API in Rust, at least for small things, definitely, because it's Rust. If it compiles, it will probably work. And it's very hard to break things by accident. So have people actually use your crates, use your own dog food, but also make sure you share your dog food around and have other people taste it and see if it is to their palette. Rust is great because you have cargo. Everyone tends to use semantic versioning, and it has good encapsulation. And so if you make an update to your crate, like, future or something, and people don't like it, that's fine. They just use the old version, and it's no problem. So everything you know about API design still works in Rust. If you can make a function that takes borrowed types and returns the result when necessary, then you're writing idiomatic Rust code. It doesn't matter how non-fancy it is. You don't need the fancieness. The fancieness is there when you need it, but most of the time you don't. Always make sure that you have some idea of who you're writing this crate for, just so when the next interesting feature pops into your head, and you say, oh, I wonder if I could do it, if it could do x, you stop the second and say, well, does x really serve my purpose or not? And more importantly, if I write x, how do I explain it to someone who's trying to use it? And iterate and keep working and keep writing code. So thank you. Thanks.
 As she said, I'm Simon Heath.  I am talking about evolving API design in Rust.  I've been using Rust pretty much since 1.0,  and I'm interested in programming languages,  and I'm interested in compilers,  and I'm building infrastructure, and making video games.  So I took these things and put them together,  and made a game engine because it's more fun than making a game,  called GGEZ.  And this is the first major Rust project  that I had been working on, and it's also  the first major open source project that I've worked on.  And the goal is to make it easy to make 2D games,  because this was something I wanted to do.  It's a good way to learn Rust.  And whenever someone says, I need  an idea for a project to learn Rust with,  I can say, you should make a game.  It is based on a Lua game framework called Love 2D, which  also has a similar goal of making 2D games easily, which  is to say that I went through the Love 2D API docs,  function by function, and wrote everything in Rust.  Because I didn't really have a great plan for how  to make a good 2D game engine.  I just wanted something that was simple and would work  and was easy to use.  So I knew Love was simple and worked,  and I'd used it before, so there we go.  GGEZ is actually used in a few games  made by real people who are not me, which is awesome.  It's nothing so far.  There's anything super huge or complicated  besides Xemaroth, which is this one which has been worked on  since before I started working on Rust.  But hopefully someday, I'll actually  get to write games in GGEZ as well.  That's my goal for 2019.  So to do all this stuff, GGEZ brings together  a lot of other crates from the Rust ecosystem  and has to take all of these libraries  and make them play nice with each other  and convince the ones that don't have cool logos to make  cool logos, so I can put them on the next slide.  It has to take these crates and have them interoperate.  It has to take whatever API they expose  and be able to wrap it up in a consistent way  and make it easy to use for Rust newbies.  And it has to actually be able to use all of these crates  successfully.  And so I have gotten to be very good friends  with some of the maintainers of these crates  because I would keep submitting bug reports  or I would keep saying, I need to be able to do x.  How do I do x?  And usually they tell me, and life is good.  But I wanted to do this talk because I also  hang out on Reddit and IRC a lot and probably too much.  And I keep seeing things like this.  So everyone who first learns Rust and writes something big  has to ask for advice on it.  And so it was weird because I don't see this a lot in Python  or C sharp or whatever.  Maybe the world would be a better place  if people did do this in those languages.  But either way, people who learn Rust  seem to have trouble figuring out how to write Rust APIs.  Or at least they have anxiety about writing Rust APIs.  They keep asking, how do I write idiomatic Rust?  And so that's what I want to talk about.  So how do we design a good API in Rust?  In my case, I didn't have to design an API.  I just copied an API and made it Rusty.  So that was, let's take a look at the API I copied it  a little bit.  But I'm going to start with some GGEZ examples.  And then I'm going to look at some of the other crates  that GGEZ uses and how those sort of look from an end user  perspective and what's good about them  and what's bad about them.  So here is a very simple love 2D game.  It's all in Lua.  I don't know how many people out there know Lua.  But we have some functions.  We have load, update, and draw, which  are the sort of fundamental parts of your game.  We have a global player, which is just a table,  dict essentially.  That's my Python showing through.  And we have update, detects if you are pressing buttons  and changes the world state if you are, and draw, draw stuff.  And great.  And these are basically callbacks that  are loaded by the Lua interpreter.  And love basically has a version of the interpreter  that is built with a bunch of libraries  and looks for these callbacks and loads them and runs your game.  So this is pretty different from how Rust works.  I mean, there's no curly braces at all.  But we have these sort of magic callbacks  that the interpreter looks for.  It's dynamically typed.  There's mutable state everywhere.  So it's kind of, I didn't even know  if it was going to be possible to make this in Rust.  I was like, well, maybe.  I mean, look at just the draw, the love2D draw function.  We have four different overloads for the draw function.  I like your expression responding to this.  We have some drawable object, or you  can replace the drawable object with a texture and a quad,  saying which part of it to draw.  You can have a transform, which is a structure that basically  bundles up all of the drawing parameters.  Or you can just list all the drawing parameters  for that are possible individually.  And it also turns out that you can actually omit the ones  at the end.  And they'll just default to 0 or 1 or whatever is appropriate.  And so you can just leave all of those off  and just have x and y and r.  And it works fine.  So I was kind of like looking at this and saying, well,  I just want to make something work.  So worst case, I'll just make a separate function  for each of these variants.  And so I started with that, and I sort of squished it together  and got something like this.  So we have a struct that has all the draw parameters in it  that you can have.  And we have a function that takes a drawable object  and drawparams and a context which just holds on  to the graphics context state and appears everywhere.  And it draws it based on whatever parameters you give it.  And then, well, OK, we have a simplified function  that just takes a destination point under a rotation.  And you can use that if that's all you need.  And if you need the full power function,  then you can use that instead.  It was like, OK, fine.  Eventually, I discovered that the default trait  and the struct update syntax exist.  And you can do something like this, which actually  is halfway decent.  It's not great.  It's not terrible.  But it works.  It's not too pretty.  I never really liked it.  But something that I realized as time went on  is that GGEZ is an opinionated framework.  And so lots of people have opinions about it  whenever they try to use it.  It's actually fairly low level.  It doesn't provide animations.  It doesn't provide a physics engine.  But, and so everyone says, oh, why don't you do it this way?  Or why don't you add this?  Or why don't you not add that?  But nobody in the last two and a half years or whatever  has actually complained about this horrible hack.  It's not a problem.  It actually has quite a few advantages.  It's simple.  Even the most basic Rust programmer can understand it.  It's completely obvious what's going on  and where the data is coming from,  where it's going, and where it's used.  And with this nice syntax, it's even not too terrible.  So it works.  So the harder case was dealing with Love2D's sort  of callback structure.  And I ended up starting with something like this,  where we had a trait called GameState.  And I should have cut out all of the inconvenient code.  But it provides load, update, and draw methods  that are just like Lua's or Love2D's load, update, and draw  methods.  And then down here, you have this game struct  that is generic on the type that you implemented the GameState  trait for.  And it just creates your GameState  by calling the load method.  And then it has an event loop inside it  that calls update and draw and takes keyboard events  and all that stuff.  And so this is the closest I could get to something  that looked like Love2D, where it just  had these magic callbacks that did everything for you.  And it sucked.  Nobody hated it.  Or nobody liked it.  I didn't like it.  I got tons of questions like, oh, how does the GameState actually  get created?  Like, where do I put the new method?  How do I?  Like, who owns it?  It's owned by the game type.  How does it know what type to load?  Well, you have to annotate it with that generic parameter.  And like, where does the context get created?  Well, it gets created in the game.  And it was just complicated and nasty.  And eventually, I wanted to be able to even take apart  the event loop and let the user sort of write their own  with their own update functionality  if they really wanted to, because Love2D  does allow you to do that.  So eventually, I ended up with this, which is kind of similar.  We have an event handler trait.  It defines update and draw methods.  But the GameState is just a struct you create.  And there's nothing special about it.  You create a context, which is sort of the handle  to all the GTEZ library functions.  Like, it handles the sound state, the window state.  It talks to the operating system, et cetera.  And you have this event.run, or colon run function, which  is literally what you would just write.  It's a wow loop that pulls the operating system for events,  calls event handler update, and then calls event handler draw.  And it just does that.  And now, by trying to do less magic,  everything becomes way better.  And so, and I didn't like, it was like, this doesn't really  look rusty.  This is kind of dumb as a pile of bricks.  But how do we design a good API and Rust?  Everything, a good API is still a good API and Rust.  That was the sort of what I figured out.  Love2D started off with a pretty good API.  I turned it into Rust, and it's still a good API.  Rust doesn't add or remove anything too magical from it.  And when it down, keep it simple.  I like the term complexity budget that the last talk used.  Because putting my complex budget into trying to make it  look exactly like a Lua API wasn't worth the extra complexity.  So another problem that I deal, often when people are trying  to get into Rust game dev, is that some popular crates are  very hairy.  So they say, well, what are my options for drawing graphics?  And someone mentions GFXRS, which is awesome,  and which Love2D uses.  And they go to the docs, and they see this.  Now, I don't know about you.  Usually I call it quits after only seven or eight  associated types.  Here we have 12.  Or they say, OK, well, how do I do matrix math?  Well, look at an algebra.  And they see this.  This is the matrix type.  This is what's even going on here.  It's almost impossible to read.  When I see stuff like this, I call it trait salad.  Because it's just a pile of different stuff all mixed up,  and you can't make heads or tails of it.  But if you're trying to learn Rust, and you come at this,  and this is what you see, then obviously the creator is  this guy, who either is just a mentat, who is one with the  computer, and knows everything there is to know about  everything, or he's just a sadist, and he likes  torturing the users of his crates.  I mean, let's look at, we saw his math framework.  Let's look at his physics engine.  This actually looks kind of reasonable.  I mean, it's just a bunch of methods.  This is part of the collider type, I think.  And it's pretty obvious what's supposed to connect.  So it's not that he's a sadist, and it's not that he is  operating on some plane beyond human comprehension.  So what's actually going on here?  We have a bunch of traits.  They all have really complicated bounds.  We have one here called abstract magma.  I usually think of this when I see that, or maybe this.  However, it turns out that what I should have been thinking  of is this.  It's a math term.  And so what an algebra is doing is teaching the Rust  compiler how to do fundamental math by encoding it in the  type system.  And that's not something that would have ever occurred to  me to do, but it also means that it completely rules out a  lot of math errors at compile time.  If you have a transform vector and a scale vector, you  can't add them together.  The type checker catches it.  And GFX is similar.  It's doing something kind of like that, trying to encode the  state of a graphics card in the type system, which is  really complicated and low level, but it ends up  catching a lot of bugs.  So these aren't bad APIs.  They're just very sophisticated and specific and low  level and are geared towards a certain type of use case.  I've talked to people who have a formal math background and  use an algebra, and they love it.  They don't see this magma, but all of this pops out at them  and they're like, oh, I know how everything fits together.  So the next lesson is know your audience.  Who do you want to be making these crates for?  What do they want to be doing, and how do you make what  they want to do easy?  If you're making something for yourself and you have a  background in math, then you end up with an algebra.  And if you don't, then you end up with something else.  So also, as an audience, know your tools.  It's a lot easier to understand why an API is the way it is  and where to look when you need some functionality when you  know who the writer is making it for.  Maybe it's not for you.  Maybe it's something you can learn, but either way.  So we've discovered that API design in Rust in hard is  hard.  It's hard in any language.  Rust seems to make it trickier, though.  Why do people get such anxiety about it?  And what can we do to make it better?  Well, in Rust, API misfeatures are actually really nasty.  This is true in any language, like Java or Python or  whatever.  But Rust is good at making things subtly terrible in ways  that aren't obvious to people who don't know Rust very well.  For instance, GGEZ has a submodule for loading  resources from file paths without keeping the files in  some platform-specific location that's different on Windows,  on Mac OS X, on whatever.  And so it uses a create to just ask the operating system  what paths it should use.  And the create is called appters, and it  works basically like this.  You create an app infostruct, which has the name of the app  in author.  And the operating system has some specific location based  off of this information that it uses to store images or  fonts or whatever.  And you just say, OK, get me the user config directory for  whatever operating system I'm running on.  And it gives you a path.  And that's it.  So the API is this.  There's more than this, but this is the part that GGEZ uses  the most.  And so we have a struct, and we have a method, and the  struct has some static strings, and everything is good.  And so I wanted at one point to write a program that loaded  these paths from a config file or something.  And so I had something like this.  We have author and app name, which are our own strings.  And we feed them into app info, which has static string  slices, except these are own strings, but these are static  string slices.  You can't get there from here.  How do you do it?  Well, you can probably figure it like you go on IRC and you  ask, how do you turn a own string into a static string  slice?  And someone will say, well, you can probably do it with  unsafe, but we don't like doing that, because the point of  unsafe is that you never need to use it.  It's there in case it's like the shotgun on the wall in your  dad's room.  It never leaves the wall.  It's there in case you need it.  You just never need it.  OK, so we'll fix that somehow.  Let's just get on with the file system code.  I want to be able to write something like this, where I  have several virtual file systems of which one type  will be able to load things from the disk, just from the  normal file system.  And one will load things from a zip file and pretend it's a  file system that's been overlaid on top of it.  And this is handy for games, because especially if you want  a game that's moddable or something, you can have all  the formal game resources in one zip file, and then you can  have a mod that just replaces a few of them, either in a zip  file or in a directory.  And it's nice.  And I wanted to just have the file system be a trait object  that exposed a few methods.  And we just have a vec, and we just look through each of  them in turn and use the first one that has a file.  Great.  So I found a crate that looks like it does this.  And it has a trait like this.  And it's called VFS.  And so we have the path which this crate is.  The path uses the path as the entry point kind of, like you  use path, and then you open a file, and it's very object  oriented.  But it works OK.  And then we have an associated file and metadata type, and  then you create a path through this method on the trait.  And it takes something that can be turned into a string and  gives you the path type for that trait.  Well, hang on.  I wanted trait objects.  And this has a generic parameter with a trait  bound, but you can't turn that into trait objects because a  trait object needs a v table.  And the compiler will make one version of the path method for  each type that you specialize it with.  And so you can't make a trait object from that.  The compiler would have to be able to look into the future  and see what types of T you would use it for and compile  those as well.  So you end up with these situations where perfectly  reasonable explanations or perfectly reasonable design  decisions just get into weird places.  And it's impossible to do anything about it.  And so these are the easy cases.  If you try to use GFXRS or Tokyo and you end up in one of  these weird corner cases, then even Rusty can't figure out  what's going on and tell you what you're trying to do  incorrectly.  And so you just end up with these weird situations that  aren't obvious, or at least aren't obvious if you're not  looking for them, where you want to do something that the  create author didn't think of you trying to do, and Rust  isn't allowing you to do it.  So they made some design decision that had  unexpected consequences.  Unfortunately, I don't really have a great answer to this  one, besides iterate, because people will always come up  with interesting use cases that you never thought of.  I've spent the last two weeks with some trying to get  GGEZ to work on iOS because someone really wanted it to,  and I never really considered doing that before.  So your users will always come up with something that you  didn't expect, and the nice thing is it's not a terrible  thing to redesign an API in Rust, at least for small  things, definitely, because it's Rust.  If it compiles, it will probably work.  And it's very hard to break things by accident.  So have people actually use your crates, use your own dog  food, but also make sure you share your dog food around  and have other people taste it and see if it is to their palette.  Rust is great because you have cargo.  Everyone tends to use semantic versioning, and it has good  encapsulation.  And so if you make an update to your crate, like, future  or something, and people don't like it, that's fine.  They just use the old version, and it's no problem.  So everything you know about API design still works in Rust.  If you can make a function that takes borrowed types and  returns the result when necessary, then you're  writing idiomatic Rust code.  It doesn't matter how non-fancy it is.  You don't need the fancieness.  The fancieness is there when you need it, but most of the  time you don't.  Always make sure that you have some idea of who you're  writing this crate for, just so when the next interesting  feature pops into your head, and you say, oh, I wonder if I  could do it, if it could do x, you stop the second and say,  well, does x really serve my purpose or not?  And more importantly, if I write x, how do I explain it to  someone who's trying to use it?  And iterate and keep working and keep writing code.  So thank you.  Thanks.
Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_0_59.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_60_191.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_192_323.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_324_371.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_372_419.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_420_551.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_552_647.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_648_707.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_708_3335.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_3336_4739.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_4740_5603.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_5604_6335.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_6336_8267.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_8268_9659.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_9660_10631.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_10632_16811.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_16812_17447.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_17448_18131.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_18132_18707.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_18708_19199.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_19200_19991.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_19992_20063.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_20064_21935.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_21936_29723.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_29724_31199.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_31200_34391.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34392_34451.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34452_34511.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34512_34559.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34560_34631.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34632_34691.png Evolving_API_design_in_Rust___Simon_Nicholas_Heath_Xt1JOVeQ5hw_34692_34746.png