Search
Matthias Endler - SE Radio guest

SE Radio 670: Matthias Endler on Prototype in Rust

Matthias Endler, Rust developer, open-source maintainer, and consultant through his company Corrode, speaks with SE Radio host Gavin Henry about prototyping in Rust. They discuss prototyping and why Rust is excellent for prototyping, and Matthias recommends a workflow for it, including what parts of Rust to use, and what parts to avoid at this stage. He describes the key components that Rust provides to help us validate ideas via prototypes, as well as tips and tricks to reach for. In addition, the conversation explores type inference, unwrap(), expect(), anyhow crate, bacon crate, cargo-script, Rust macros to use, generics, lifetimes, best practices, project layout styles, and how to design through types.

Brought to you by IEEE Computer Society and IEEE Software magazine.



Show Notes

Related Episodes

Other References


Transcript

Transcript brought to you by IEEE Software magazine.
This transcript was automatically generated. To suggest improvements in the text, please contact [email protected] and include the episode number and URL.

Gavin Henry 00:00:18 Welcome to Software Engineering Radio. I’m your host Gavin Henry. And today my guest is Matthias Endler. Matthias is a Rust developer and open-source maintainer with 20 years of experience who provides training and consulting through his company called Corrode. Beyond writing clean code, he prioritizes creating supportive environments where teams can grow their rough skills together. Matthias welcome to Software Engineering Radio. Is there anything I missed in your bio that you’d like to add?

Matthias Endler 00:00:45 No, that pretty much sums it up. Thanks for having me, Gavin.

Gavin Henry 00:00:48 Perfect, my pleasure. So I got you on the show because I saw your blog post, really enjoyed it and it was called Prototyping and Rust.

Matthias Endler 00:00:57 Yes.

Gavin Henry 00:00:58 It helped me understand how to take my idea and try to validate it in Rust, which isn’t something you usually hear. So I thought I’d get you on to chat over your techniques and go through some of the things that might help other people get into Rust for the first time or reach for it when they want to do that prototype. So let’s lay down some foundations. Could you give me an overview of what a prototype is?

Matthias Endler 00:01:23 Sure. Well, I like to compare it with art. When you try to paint a picture, you don’t really have to start from the top and go all the way to the bottom. Usually you try to capture the main idea as quickly as possible before it goes away. And so maybe you can start with a sketch and a prototype is like a sketch. It turns out that programming itself is a very iterative process. We do believe that when we read the program in the end the ideas are there and we thought about these ideas from the get-go, which is not true. In reality we also sketch out certain parts of our application as we go, and this is what a prototype is. It starts as a quick draft of what we have in mind and then we iterate on it.

Gavin Henry 00:02:15 Thanks. Do we keep it or do we throw it away? Because I’ve heard other explanations. I think it’s in the pragmatic programmer book where they say a prototype is something you’ve been, but I don’t know. What do you think?

Matthias Endler 00:02:28 That’s a good point. I think a lot of people when they think about prototypes, they have this idea of a throwaway product or project in mind. We will throw it away anyway, but I think it’s an orthogonal question that is besides the question of whether to prototype or not or what a prototype looks like, because in reality it doesn’t really matter if you’re happy with the result, you can keep it, you can iterate on it. But the main point is that you’re trying to get an idea from your brain into some sort of text format. And this is the main core idea. It helps you find the best approach before committing to a design. Whether you keep it or not in the end is completely up to you, completely up to the complexity of the project, the team you work with and all of these things that are maybe even outside of your control. Maybe your manager will say, we will go forward with it. And I would say that’s a positive thing even because you start it with the right idea, but prototyping is like hatching your risks because if you start with the wrong idea, you can happily throw it away and you didn’t lose a lot of time.

Gavin Henry 00:03:41 I like that explanation. Also in my experience, it gives you a different mindset because you’re thinking this is a prototype, I don’t need to care too much about it. I can, you know, whereas if you’re starting the real thing than you’re thinking, oh, I need to get this right, I need to do this, I need to do that. So maybe it’s a bit more freeing because it’s got that label on it. So should a prototype be in the same programming language that we think the final or production version’s going to be? Or should it just be something that gives us that freedom or what’s your thoughts?

Matthias Endler 00:04:14 One huge advantage of using the same language and the same tool set, especially for the prototype and the final version is that you don’t have to go through the rewrite. And the rewrite in quotes is the process of going from your decent idea to quote unquote production code. Now if you have to change the language, then you might make mistakes or maybe the patterns that you use in one language, they don’t translate to another language. So you kind of end up in a weird situation where maybe you try to bite off too much or you probably end up with two problems. One would be the translation from one language to the other, and the other would be making it idiomatic again in the other language that you chose for production. So I would say if you can, keep it in the same language, ideally you would want to use the same language.

Matthias Endler 00:05:11 And I think the other part is the tool set. If you have a certain stack for writing, say a Python prototype or a Golang prototype, then this translates very well into a Python production application or a Golang production application. Same for Rust. The tooling is what makes developers fast and what makes them efficient. And if you have to switch the language, then you also have to switch to tooling and the entire ecosystem that goes around with how do I put that into production for example, how do I containerize my language or what’s the ICD frameworks can I use and whatever. So there’s an advantage to using the same language. Ideally, it’s not always possible in every language, but I would strive for it.

Gavin Henry 00:05:59 Before I move us on to our example application in the next section, I see quite a few places where people say that Rust isnít a good fit for this type of process how youíre prototyping. Why do they say that?

Matthias Endler 00:06:14 Yes, I fully agree that this is a very common trope that I see being repeated on public social media, on YouTube, in various blog posts and so on. The notion that Rust is not a good language for writing prototypes in. And this is kind of what prompted me to write the blog post because what I see in practice is not what people say on the internet about this topic. And I wanted to write some wrongs here if you want. The reality is that my clients and me, we are very effective with writing prototypes in Rust. But to your question, why do people think Rust is not a good fit? I would say there’s a few misconceptions out there. First would be that the Rust type system, which is very strict, pushes back when you change your mind. So it tries to keep you in track.

Matthias Endler 00:07:14 And when people think about prototyping, they think about running free, letting their ideas flow, but in reality, they also want guardrails even in this early process. Another misconception is that memory safety and prototyping are incompatible. Rust is a very safe language. It needs you to know how to handle memory and it forces you to use ownership in borrowing. And that takes the fun out of prototyping and it’s also incompatible with what you want to build in the long run. And I don’t believe that is true necessarily because you will have to deal with that anyway and you might as well just deal with it in the beginning when you have the most control over it. Another misconception is that Rust requires getting all the details right from the beginning. And I think that’s not entirely true. I think it wants you to get the important details right?

Matthias Endler 00:08:13 For example, how do you structure your structs and how do you manage ownership of these objects that you can create from these structs who’s owning what, at what point? What are the lifetimes of your objects in your system? And these are things that are very important even for a prototype, but especially for production because otherwise he would introduce now pointers. And I think the combination of all of these things would be that Rust requires you to handle errors and that gets in the way of prototyping. Well, that’s not entirely true. There are escape hatches for handling errors. Even in Rust you can use unwrapped, you can use expect, and you don’t really have to handle all of the errors right away. It’s just that Rust will kind of panic in case it runs into an error. And that’s a good thing even for a prototype. It means in reality you can avoid all of these pitfalls while getting the most value from Rust.

Gavin Henry 00:09:16 Thank you. Going back to your earlier point in the answer about borrowing and ownership and the fact that Rust pushes forward things that you’d need to deal with earlier. If you’re doing this in a scripting language or a dynamic language, say you might be just saving debugging for later when you’ve saved things to the same variable twice or things like that. So you could argue on the flip side that Rust helps you out faster than these other languages because it’s telling you these problems straight away and you shouldn’t think about it getting in the way it’s actually helping you.

Matthias Endler 00:09:50 Rust is very much a day two language, and I think that’s at the core of the problem here where people mostly start with their clean, pristine, vanilla idea in their head and then they have to face reality in which some of their ideas don’t make any sense or some of the concepts they came up with, they don’t really work well together. And with many other languages like Python, you defer those issues until later and later is usually when rubber hits the road. And when you make the prototype into a production system, Rust does not allow you to do that. So the initial ramp up phase is much more involved, but on day two you can reap the benefits because all of these conceptual issues, all of these integration issues are already solved. You cannot take this burden away from your future self. But what I see in other languages is that people take on a loan of their own future and it will haunt them later on, but then later on is the painful time that they don’t have to think about right now. Rust is very much against that and tries to start from a clean slate and tries to put the right abstractions in place that you know will work in the future.

Gavin Henry 00:11:21 That’s a much better answer than I just gave. That’s cool. Did you just make up day two or is that a common term?

Matthias Endler 00:11:27 I didn’t invent it. Some other people might say Rust shifts complexity to the left and by left they mean to earlier stages of development. For example, the development phase or the ideation phase and prototyping is somewhere in between, I would say.

Gavin Henry 00:11:45 Oh, like on a time graph left being the start. Yeah.

Matthias Endler 00:11:49 And these are all, I would say day one problems. So how do I set up the project? How do I get from my idea to something that I can play around with? And many other languages they excel in this area, especially the scripting languages, they allow you to run free, they allow you to make mistakes, Rust does not allow you to. And then later on day two, which is in production when later on you have a no pointer issue or you have a race condition, these languages tend to fall apart. It depends on what you build of course, but that’s what I commonly see that services become hard to maintain, they become brittle. Refactoring becomes very tricky to do. You might be afraid to make too many changes because you might break things, whereas in Rust it’s pretty much easygoing then because all of these things were clarified upfront and really what you end up with is mostly business problems or logic problems maybe, but the core semantics of the language keep you from going astray and keep you from drawing yourself into corner where your only escape might be a rewrite.

Gavin Henry 00:13:05 Yeah, I mean also you can have a program that is correct and compiles and runs, but it doesn’t do the right thing. So Rust does help with that as well. Right. I’m going to move us on to our next section. So we’ve got an idea where I’ve had an idea for prototype. I donít know how applicable it is to the blog post, but why don’t we think about a weather station that takes various real-world feeds and displays them initially on a command line and then maybe a display in production. Do you think that’s a good fit for prototype?

Matthias Endler 00:13:38 Anything can be a good fit for a prototype, but yeah, this one specifically I like because it has a couple of components.

Gavin Henry 00:13:45 Excellent. So in your blog post, obviously the listeners can’t see the article just now and the images, but I took a screenshot of what you’ve called Rust Prototyping Workflow, which is a four-step workflow. Number one being define the requirements, number two being add your types. Number three you’ve called borrow check, which we’ll explore. And number four is fix Clippy lints, which comes with Rust. That helps you tidy up things. So would you like to take us through that workflow?

Matthias Endler 00:14:18 Sure. So first step would be to define your requirements. By the way, this is just my workflow. It’s not a canonical version of any workflow. I don’t impose that on anyone else. I just try to explain what works for me in practice and how I think about prototyping.

Gavin Henry 00:14:37 No, that’s cool. That’s cool. Sure. I just thought it helped describe things nicely.

Matthias Endler 00:14:42 Yeah, yeah, totally. In the first stage I try to find my requirements and I don’t really think about the types as much as I think about the components or how they interact. I might not even write a single line of code in that stage. I might just draw something on a piece of paper or use Skelly draw to draw a couple boxes in lines and then just see how it feels, how it feels in my head, how I could imagine things going. I do think a lot about control flow or data flow rather than objects because I think you can always model proper objects around your data, but it’s very hard if you do it the other way around. And in this stage, I usually just think about the larger parts and how they would interact and how they would communicate with each other. And then I go to stage number two, which is adding types. In Rust of course you have a lot of types. For example, we have I think like 20 different string types and most people are just aware of maybe two.

Gavin Henry 00:15:50 Yeah, I’m only aware of string, new and borrowed string.

Matthias Endler 00:15:56 Yeah, it all boils down to the guarantees that you want to give about your string. Is it UTF-8? Is it on the heap or the stack and so on. But in reality, you don’t really need to know about all of these different string types. What you can do is just use the simplest type that would possibly work. And since Rust is so type heavy, it allows you to build these abstractions from simple abstractions and you can always add more guarantees later on. With some experience you might even start with the lowest of guarantees that you can possibly give. But let’s assume in the beginning you have a message, don’t even over complicate it, just use a string. Whether it lifts on the stack or the heap, doesn’t really matter whether you allocate or not doesn’t really matter. At this point, you just know a message is a string, so you just use the own string type with a capital S for example.

Matthias Endler 00:16:48 Other examples are you don’t use a slice if you can use a vector or if you don’t know which integer type to use, just use an I32 for example. Don’t think too hard about the very specifics of the implications of your types at this stage because in the end you can replace them with finer or more refined types so to say. Now once you build up your little system of types, you try to talk to the compiler about it and there’s this notion of fighting with the borrow checker. I think that’s a misconception as well. In reality you discuss with the borrow checker or you have a conversation with the borrow checker, this is how I see it nowadays. So it might tell you, okay look this doesn’t work because this place in memory does not live long enough. You probably want to use a different type or you have a smaller scope for that or maybe you want to add a lifetime if needed.

Matthias Endler 00:17:52 But most of the time it would just tell you this goes out of scope. Try to make the scope larger so that the variable lives for longer. Now after this stage you know that you have two things. You have types which model what you want and you know that it will work in production because the borrowed checker tells you if there are any null pointer issues or any memory issues, safety issues. And now the last part, part four would be to refine and to improve and to fix some of the code. And I use Clippy for that a lot as to many other people. And Clippy gives you a lot of hints about what to improve in your code. Just set it to the highest level possible. Even in prototyping, it’s fine. And then it will point out things that you can improve and maybe idioms that you did not know about, but also with experience you will see how you can probably shape it up yourself during that stage. Fixing Clippy links doesn’t only mean that you fix the clip links, but also that you fix everything and prepare yourself for the next iteration cycle and start over again with defining more requirements. This is the cycle.

Gavin Henry 00:19:11 Yeah, because I might not have explained the image. It’s a one to four item list, but then it loops back to number one. That’s your workflow process. Okay, let’s go through that again. So we’ve defined the requirements for the weather station. An input might be the level of rain that’s happened. We think about a type for that. To keep it simple, we’ve chosen a string. If we need to do a list of things, we’re going to reach for a vector and not think too hard about that at this point. We’ll have some variables we’ve moving stuff about and we might compile the project and get some complaints from the borrow checker saying that we are using a variable in another place and we haven’t moved it properly or we need to create something else. Is that a good summary from one to three so far?

Matthias Endler 00:19:57 Yes. And there will be errors which we haven’t handled at this stage.

Gavin Henry 00:20:01 Yeah. Will it compile at 0.3?

Matthias Endler 00:20:03 It will compile it on level three, but to make it compile, we might still need to add some escape hatches here and there. Okay. For example, we could add a little to do and there’s a macro for this, which actually is called to do exclamation mark (ToDo!) and then you can specify whatever you need to do in this line. And this is what I do a lot. I say, oh yeah, we need to flesh out this part or here’s a little bit that’s missing or this is unimplemented and that is completely fine.

Gavin Henry 00:20:32 Who are you telling that to yourself or to?

Matthias Endler 00:20:35 Oh well this is in fact an instruction in Rust. So this is a language primitive that you can use wherever you need to fill in gaps later on. And the compiler will turn away at this line and say, okay, if I hit this line it will just panic. And that is completely fine because you get the message which says this needs to be done in order for this to work.

Gavin Henry 00:20:57 So it’s not something that’s just printed on the screen for you to remember that you need to do. It’s actually,

Matthias Endler 00:21:02 It’s like an executable comment. It’s like an executable to do. Yeah,

Gavin Henry 00:21:07 I’ve not used that much at all.

Matthias Endler 00:21:09 And the cool thing about this also is that all of these primitives are graphable, you can search for to do exclamation mark and we will show you all of the places where you use that or you can search for unwrapped or you can search for expect and it will show you all the places that you need to fix up for this to go from prototype to production. You see where the power is now because in Python there is no such thing. Every single instruction could throw an exception and usually the exceptions appear very deep in the call chain and this makes it super tricky to do later on. But since Rust is so explicit, it will kind of force you to at least at this line and it just helps you keep track so that you don’t forget you don’t have to fix it right away. A lot of people say it needs to be perfect or it doesn’t compile and that is not true.

Gavin Henry 00:22:02 Nice. So stage three, the borrow checker’s helping us out already. You mentioned Lifetime, so can you just one sentence remind the listeners of what that is if they’re not familiar with Rust and the Borrow checker?

Matthias Endler 00:22:15 First off, don’t worry about lifetimes. I even wrote an entire article about this.

Gavin Henry 00:22:20 Okay. I don’t think I’ve actually ever used the lifetime syntax myself yet in any Rust I’ve read. Yeah. So I donít know if I’m doing it right because I haven’t done that.

Matthias Endler 00:22:29 The way I think about lifetimes is it’s like a label. It’s another set of variables that you can use. So for example, you say you have a file handle and this file handle points to a certain file that you can read from. And then you have a reader which uses that file handle. Now the file handle needs to be alive for as long as the reader because otherwise if the reader is trying to read from the file handle and it’s no longer there, then well that’s a memory safety issue. That’s a null pointer essentially. And so you just define, you guarantee to the compiler. You say this fight handle will always be around for as long as the reader is around and you can in comparison to all the other languages, spelled it out is text in the Rust programming language you can say tick A, which is just a shortcut, but it can also be tick reader and that means this is the lifetime of a reader that I’m referring to here. And you give the fight handle the lifetime of the reader for example. And I guess that’s the entire metric here.

Gavin Henry 00:23:39 That’s something you’re in control of that you have to remember to make sure it doesn’t go out of scope.

Matthias Endler 00:23:44 Yes. But 99% of the cases the compiler will infer it for you. It’s just in the cases where it’s not sure about which lifetime you mean specifically say there’s more than one option that it will ask you to edit yourself. But there are lifetime initial rules which allow you to skip most of the work as long as it’s clear what you’re referring to. If there’s just a single lifetime in scope, then you don’t really need to specify that.

Gavin Henry 00:24:14 Excellent. So now we’re at number four. We’re going to use, I presume, Cargo Clippy links to help us clean up the codes. Now we don’t have to do this, do we?

Matthias Endler 00:24:25 No, but it’s a bit like cleaning up the kitchen. So technically you don’t have to clean the kitchen after every time you make dinner or so, but like the next day or the day after, there’s a couple smells and you probably want to avoid that situation. It’s probably much better if you do a little bit of work on a regular basis instead of doing a lot of work all at once. I’m not sure about the audience, but I’m really bad at getting myself a huge chunk of time to do household course. And this is very similar. I have a much easier time fixing things as I go. I’m not sure if this is true because I’m not a good cook, but what I imagine good cooks to do is keep the workplace clean while they cook. So they kind of do that more or less automatically. It’s second nature to them. Someone please correct me if this is wrong, but in my perfect imagination of a good cook, this is how I think about it. And I would rather clean up after myself while I’m coding and I just fix those little Clippy links or whatever. A lot of people that work a lot with Rust, they love Clippy for pointing out issues. You get addicted to it.

Gavin Henry 00:25:41 Yeah, I like it too. It’s one of the first continuous integration workflows I put in my GitHub repositories. So it cleans it up. Cool. We’ve got about five minutes left in this section. We’ve skipped some of the questions I wanted to ask but we’ll do them now. I think that was a good overview of the workflow. We’ve gone through some of the key components that Rust gives us. So some of the in-bill macros a massive part of the tool set, which is what I love about the Rust ecosystem. We’ve had a WeChat about type inference that is mentioned in your blog post.

Matthias Endler 00:26:14 It is, actually.

Gavin Henry 00:26:15 Yeah. And how we can skip some of our error check-in by using the expect and unwrapped functions. Do we need to think about heap and stack stuff just now? I think we decided that we’re just going to stick with strings and vectors in our prototype.

Matthias Endler 00:26:30 Yeah, absolutely.

Gavin Henry 00:26:32 Cool. So one last question before we move on or two. When I got exposed to Rust in a previous life, I remember there was an issue in production which I was explained to that the default stack size of two meg wasn’t big enough. Now I know we said we’re going to skip heap and stack from memory, but what does that mean? Because I haven’t had a chance to ask anyone that the default two meg size of the Rust stack wasn’t big enough.

Matthias Endler 00:27:00 Yeah, so we would have to look at the specifics of this error, but in general, Rust, like any other language, has a limitation on the things that you can put into the stack. And the stack is a certain section in memory that just keeps growing until it reaches a certain threshold. It’s very fast essentially you don’t really deallocate memory, you just move a program counter around and it always points at the latest thing that you put on the stack. So you can think of it as like a stack of cards and you just can put things on top and then you can take a thing from the top and this is what a stack looks like in memory. Now if you run out of stack, that means the stack of cards is exhausted. You cannot put any more cards on the stack because well there are no cards anymore.

Matthias Endler 00:27:55 Now how this usually happens is there’s a very complex operation which puts a lot of things on the stack. Of course two megabytes is kind of a lot of memory. If you only have for example, simple integer types or so you can put a lot of integers on a two megabyte stack, but at some point you will run out of it. And sometimes this happens when for example, you reach a recursion limit when you call a function over and over again and it puts more things on the stack until eventually you are exhausted. And what it means is when you get the message run out of stack or so ran out of memory, usually it points at a bigger problem with the logic of your application. Maybe you can restructure your code such that it doesn’t put that many things on the stack or vice versa. You could put things on the heap instead, which is pretty much unlimited in size.

Gavin Henry 00:28:53 And how do you think we could trigger this issue in our weather station prototype? Would that be too many inputs or?

Matthias Endler 00:29:01 It would have to be a lot of inputs. But for example, one potential way to trigger this would be if you wrote a function which does calculations on a lot of weather data and it’s recursive in a sense that the result of one calculation depends on calling this calculation again with maybe a reduced set of inputs. And then over time you kind of add things to the stack until you run out of memory. But then again, I also want to point out that for every function call you kind of create a new stack frame. So it’s not as if there was a single stack. In fact every function gets its own stack, and it will be cleaned up after the function returns. So it would have to be a thing that puts stuff on the same stack over and over again. Maybe weather information and doing some computation in a loop or so and then keeping the stuff around for way too long and not accumulating a sum but trying to keep all of the individual measurements on the stack for too long. Maybe that will be one way, but yeah, I agree that it’s kind of a constructed example.

Gavin Henry 00:30:16 Yeah, thanks for the explanation. It’s not something I’d come across in other languages. I don’t know if that’s just because I’ve not hit that type of thing.

Matthias Endler 00:30:23 Well it can happen in any language really. Yeah. So Rust isn’t special in that regard.

Gavin Henry 00:30:28 I suppose that stack overflow is it? Yeah, especially in the stack and all those types of things.

Matthias Endler 00:30:32 Exactly. That’s a stack overflow. Now the reason why a lot of people don’t run into that in dynamic languages like Python is that a lot of things end up on the heap instead of the stack. And most people don’t really think about the stack as a place where they can put stuff. But in reality, it’s probably a very fast and convenient option and it’s an order of magnitude, maybe two, maybe three orders of magnitude at times faster than the heap. A heap allocation is very expensive and if performance matters, maybe you do want to use the stack more and Rust allows you to do that. Whereas in other languages like Python, that’s harder to do.

Gavin Henry 00:31:10 Perfect. So I’m going to move us on now to our next section and I want to go over some of the libraries or third-party things that aren’t in core Rust that can help us with our prototype. And so we’re going to park the app and just go through three crates that you mentioned. So the first one would be Anyhow, now I’ve spoken a little bit about this with Tim McNamara when we did the four levels of errors in Rust, which I’ll put a link in the show notes for listeners. But could you just take me through what Anyhow does for us and how it allows us to get on with the idea of our prototype?

Matthias Endler 00:31:46 Yes, Anyhow is a bit of the next stage after you are done with your first initial prototype, you have all your code in place but you use unwrapped and expect in many places and you kind of want to get rid of it but you are working say on a CLI application like your weather app and you don’t really have a consumer of the errors, you just want to have cleaner error messages and you want to handle them properly inside of your CLI application so that you can eventually print a string and say this went wrong and this is where Anyhow comes in. Anyhow, itself is just a wrapper around whatever the Rust Standard Library provides around error handling, like the error trade. But it’s nice because it adds some conveniences like the context method which allows you to add context to any error that implements the error trade.

Matthias Endler 00:32:42 And this is extremely powerful because instead of panicking when you hit an unwrapped, it will bubble up the error to the caller and all you need to do is change the function signature from no return value to an Anyhow result value and returning an okay at the end of the function. And then you can use the context macro and the bio macro that it provides to convey that there was an error without panicking. And in a central place you can then print the error for example and exit the program cleanly. This is a very effective way if you are writing a command item application or a binary that doesn’t have any consumers on an API side like a library does.

Gavin Henry 00:33:26 So if we left the unwrapped function call or the expect, that would just crash the binary and it would panic.

Matthias Endler 00:33:34 Yes.

Gavin Henry 00:33:35 And to create production version of our application or idea, we don’t want any of that because it looks terrible, and it doesn’t tell us what we need to know.

Matthias Endler 00:33:42 Yes. And the step from unwrapped to Anyhow is extremely small. You can do that with a simple surgery replace of unwrapped with context and then you return the result type. So you change the function header, you return a result from your function and suddenly you converted that into proper error handling. You do that in leave notes in the functions of your application and then in a central place where the error bubbles up, you can handle it and print it and exit the program cleanly. And again, this is kind of the powerful part that people forget about prototyping in Rust. We started with a thing that was crude on purpose because we focused on other things and now we end up in a place where things are relatively smooth already after this Anyhow stage. I would say this is kind of on the level of a decent error handling situation in many other languages like GoLinks for example, with the added benefit that we started with a way dirtier version in the beginning we didn’t really have to litter our code with if error not equals nil like in Go or we didn’t really be scared about exceptions like in Python we just have it there explicitly in our code there wasn’t on Rep and now we replace it with context or with Veil and suddenly we end up with much better, more robust application

Gavin Henry 00:35:03 And also, we know exactly where to look to make this change because we’re replacing potential things unwrapped and expect. So it’s a lot easier to push that out of your head and move on to the next step.

Matthias Endler 00:35:14 A lot of the critical parts in Rusts are keywords.

Gavin Henry 00:35:18 So the next crate in the list of three we’ve got, so we’ve done Anyhow would be Bacon. That’s not something I’ve, well I like Bacon, but it’s not something I’ve heard of in Rust. Can you take me through that one?

Matthias Endler 00:35:28 In languages like Node you have a Watcher which allows you to restart the application when you make a change. And this is what Bacon does, it’s sort of the official successor of Cargo Watch, which I love to use, but it’s deprecated by now and Bacon does a similar job. It just watches for changes and the moment you save a file it would run whatever command you decide to run. For example Cargo Run, that’s kind of the default, I guess. So you save the file, it will trigger an event that Bacon listens to and then it restarts your app, and it has some additional conveniences. For example, it has this nice two E-app, the text user interface application which shows you everything that’s going on from the errors that get thrown from the compiler messages. Yeah, I think it has more functionality and it’s kind of a nice copilot or companion while you code, it runs in a terminal, and it just sits there, and you can iterate on your code while you prototype. You don’t really have to Command T, Cntrl C and up and enter all the time to restart the application. Instead it has got your back. It always shows you the latest version. In our case when we built a weather app, we might have a CLI application and maybe we run one specific command over and over and over again. Well Bacon can do this for us. We just make the changes in the code, compiles, it runs to command, we see the output right away. We don’t have to wait.

Gavin Henry 00:36:58 It’s more than what you’d get in an IDE like Rust Rover or Zed or something where it’s constantly building when it sees a change.

Matthias Endler 00:37:06 Yeah, IDEs are all about reducing the feedback cycle time and Bacon takes us one more step further because an IDE does not know what to do after the program compiles. You kind of have to run the application yourself but Bacon fills this gap, it runs the application in the end and it shows you the output. And so it’s again about reducing the feedback cycle, which is kind of the core part of having a great prototyping experience.

Gavin Henry 00:37:34 Yeah, for us we might decide that the weather station takes all the data around the command line, but it also has an API Restful API built in Web API and we’ve decided to have a library and a binary and the binary calls that API. So Bacon could keep calling the Rest endpoint that we’re trying to pass JSON for or something like that.

Matthias Endler 00:37:56 Yeah, yeah, a good example. It’s all about getting this Ripple like experience that you know from other languages.

Gavin Henry 00:38:03 The third one we’ve got, so we’ve done Anyhow and Bacon. Third one I liked was called Cargo Script. What is that?

Matthias Endler 00:38:10 I like to share code with other people and for that to work it needs to be self-contained. Some people might know the Rust Playground, it’s a web application, you can write some code and then you get a link that you can share with other people. Cargo script is similar, but you can run it locally, it just runs scripts. You can add dependencies at the top of your script. You can say this depends on Anyhow for example. And then it will be a crate that you depend on like a normal dependency and then you can take this script, copy it, send it to a friend or a colleague and ask them to run it with a specific command Cargo script itself and it will produce the exact same output as it did on your machine. And this is extremely helpful for prototyping and tossing ideas around.

Matthias Endler 00:39:02 So I kind of like to use that a lot. It’s still a nightly feature. You don’t always have to use a nightly compiler to use the nightly feature. You can just say Cargo plus nightly to temporarily use the nightly compiler but then the experience is kind of great. Another thing that I use it for, which is kind of besides the prototyping part, but I wanted to mention it, is for blog posts and book chapters, for example, all of your code since it’s self-contained in this script can be some sort of unit test for your article. So you just put the code next to your document and then you might run it just to check that it still compiles. And so you make sure that the code that you have in your article is always valid. And I kind of like it, it’s very hard to keep code working while you iterate on a blog post. Just like you iterate on the prototype. I used it for both cases for prototyping and for writing nowadays.

Gavin Henry 00:40:03 And why couldn’t you just build a binary and give that to your person because they can’t see the code I suppose.

Matthias Endler 00:40:09 Yeah, because they can’t see the code and if you were to show them the code then you would have to send them a zip file because Rust Project consists of many files, not only a single source file but also a source folder and a Cargo Tomo at least.

Gavin Henry 00:40:24 That’s a good point because it might never grow beyond a Rust script either. And just, before I move on to the final section, you mentioned the word nightly. So for those that aren’t too familiar with the different builds of Rust, could you just summarize that for me?

Matthias Endler 00:40:42 Rust has three main versions that you can use. The most common version is stable Rust. That is what most people, I would say 90% of people typically use on a daily basis. Then you have the nightly version of Rust, which is a, as the name says nightly built of the Rust competitor. They have a CI/CD workflow which always runs at night and produces a version of the Rust compiler that you can use which has the latest features enabled so that you can test them. It’s a bit cutting edge so if you don’t want to go all the way, you can sign up for beta and you don’t have to really sign up, you just tell Rust up for example to download it. And then you have features that are about to be stabilized in there and you can also try them right away. So these are the three, let’s say releases of Rust that are continuously maintained.

Gavin Henry 00:41:37 Thanks. And when you say nightly, who’s night on the planet? Is that?

Matthias Endler 00:41:42 That’s a good point.

Gavin Henry 00:41:44 American night, European or.

Matthias Endler 00:41:45 I don’t know, I would assume that its wherever AWS servers are, but nightly is a bit of a term that just tries to express the fact that it’s a bill that runs on a daily basis. It can also run during the day of course. Now why is it a nightly bill? Why do we call it this way? I’m assuming, I don’t know, but I’m assuming that it comes from this old notion of batch processing which also ran through the night. So developers would end their day and then the batch processing thing would run through the night and then in the morning they would have the results. So it’s a bit like this.

Gavin Henry 00:42:22 Yeah, where your backups would run overnight and things like that as well.

Matthias Endler 00:42:25 Yeah, yeah.

Gavin Henry 00:42:26 Perfect. Thank you. So our last section I’ve called debugging and error handling. Now we’ve touched a little bit on how we handle errors already where we could mark somewhere in our prototype, let’s call it prototype script since we mentioned Cargo script now. So just to summarize that last section we did Anyhow, Bacon and Cargo script Anyhow was for errors, Bacon was for detecting changes in the code and then Cargo script is to put everything in one file to share. So if we’re thinking about our one file to share, we might, we’ve already mentioned the ToDo! macro where we know we need to do something in the script tool, sort of crash there because we haven’t done it. What other errors could we get in our prototype apart from not having implemented something bad data or maybe this is just a nothing to do with our prototyping, just errors in general. Do we need to think about that type of stuff now in our prototype? What would you recommend?

Matthias Endler 00:43:19 Yeah, and this is the core idea of prototyping to really try and squeeze out all the error conditions that we could possibly get hit with as early as possible. We kind of take whatever would await us in production and we try to go through it now because it’s way less costly to handle all these situations now. Let’s talk about the weather station example again. So what can go wrong? Well we need to read those feeds from somewhere and that somewhere might not be available right now. That might be a normal network error. How do we handle that then if the server is available and it sends us some weather information, can we read it? Is it in the correct format? What is in there? Can we transform it into a Rust type, maybe de serialize it with 30 and what if not, how do we handle that case? Do we need the data from every single feed all the time or do we handle it some other way? That’s a business decision to be made.

Gavin Henry 00:44:31 And third is a crate, isn’t it a library?

Matthias Endler 00:44:33 Yeah. 30 stands for serialization, deserialization and it’s the core crate for that job in Rust. Now I wanted to go forward and say, let’s say we were able to aggregate all the data from our feeds. Now we want to display that information somehow. Well where do we display that information? In what format do we display that information? Can we even format the information in a way we want and maybe is the output available if we print for example to the terminal, well can we lock standard out, so print to it even. All of these things might happen in a real-world system. What if we run out of memory? What if the processing of data takes too long or whatever? These are things that will keep us from deploying this application to production and that’s why we need to handle it right then and there. And so I would say error handling at this stage now is the very central part of our job.

Gavin Henry 00:45:32 What macros can help us to achieve this or even experiment to make sure we’ve covered everything.

Matthias Endler 00:45:38 Yeah. Well let’s start with the simplest example print line. I use print line a lot. It’s a macro because it takes variable amount of arguments and you can litter print lines wherever you want at this stage. Just to understand the program logic, you don’t really need a full-blown debugger at this stage. The Type system will guide you and the rest you can just print. Now what if you want to be more expressive? Maybe you don’t really want to just print stuff, you also want to show the file name, the line number, what can you do? Well in this case there’s macro code debug and it shows just that. It shows you where exactly in your code this message was sent. It shows you the expression that it evaluated and the value that it returned. Then there’s the ToDo! macro, which we already talked about.

Matthias Endler 00:46:35 This is amazing for scaffolding functions and marking incomplete parts. Then you have the unreachable macro, which is similar but also a little different in comparison to ToDo!. It says this part shouldn’t be reached. I am aware that this is not done, but we should never get into this point. Whereas in ToDo! you say we will get to this point, we just didn’t get around to fixing this yet. And then for testing you have two macros, which are, I would say kind of relevant. One would be assert, which is like a normal assertion in other languages it documents in variance. And then you have debunked assert, which the first expensive checks and it’s only available in debug builds, which sometimes is nice, especially if you have a prototype, you write some code, you add some assertions right then and there just so that you’re clear about your in variance. But you don’t want to publish that in production and you might forget to remove these assertions, just use debug assert and then yeah, it won’t end up in the release build, but it’s still going to be there for debugging purpose later on.

Gavin Henry 00:47:44 So two points I’d like to focus on and ask there. So there’s a debug build of your Rust application and there’s a release, isn’t there?

Matthias Endler 00:47:55 Yes,

Gavin Henry 00:47:56 Debug is bigger, potentially slower. I’ve got lots more stuff at it. So you can debug it and release is as fast as you can get. Everything’s slick, streamlined, and unreachable. Can you give me a use case when we would use that unreachable macro? I kind of get it, but could you give me another example?

Matthias Endler 00:48:12 Yeah, I did write a Moss 6502 emulator at some point.

Gavin Henry 00:48:19 You must’ve been bored.

Matthias Endler 00:48:21 Yeah. My original goal was to build a Nintendo entertainment system emulator, but in reality, the Moss 6502 was the more interesting part and I wanted to get this right. So I started to write out what an emulator does. So it takes an instruction and then it transforms that into machine code or in the case of an emulator, it just modifies the state of the CPU. And what I found was that there were undocumented things in the CPU and these instructions were there, but they were not supposed to be executed. Those were kind of maybe box in the hardware or at least undocumented in the Moss 6502 documentation. But I just wanted to express the fact that this section in the code should be unreachable for any normal usage. And if someone reached that point, that would definitely be unusual, and I would not want to care about this situation because you kind of enter the realm of undefined behavior and I wanted to stay clear from that. But this was a really nice use case for the unreachable macro to say, yeah, this CPU instruction might exist but it should be unreachable. I wanted to market it.

Gavin Henry 00:49:41 Do you have the that code or the feature in your application to satisfy something else and then you just mark it not usable? I don’t understand why you have that code around in the first place if it’s never going to get used.

Matthias Endler 00:49:53 Yeah, because Rust kind of forces you to tell it what to do in specific cases like pattern matching for example, you have an Enum variant and it has four different variants and one of them you kind of don’t expect to handle at this stage. You can add a ToDo! if you say, I don’t get around to doing this right now. Or you can add unreachable to say this should never happen and it should never be reached. But Rust, the compiler itself kind of wants an answer from you. It doesn’t accept no as an answer or returning a null pointer or just going into undefined behavior here. In reality, what you say to the compiler is, look, if we reach a spot, please panic and then I need to handle this in the future, but right now I don’t want to deal with this case.

Gavin Henry 00:50:46 Yeah. So you’re satisfying its requirement for understanding what to do rather than just saying unwrapped or something. because that’s not acceptable.

Matthias Endler 00:50:52 Yes. Pattern matching in Rust is exhaustive in the best sense and it needs you to handle all of the variants that can possibly or occur.

Gavin Henry 00:51:03 That’s why Rust is great for error handling, isn’t it?

Matthias Endler 00:51:05 Yeah, that’s part of the reason for sure.

Gavin Henry 00:51:08 Okay. So we’ve gone through some great macros, some that I wasn’t aware of either. So are there any normal Rust things as in what you’d expect to see in every production grade Rust application that we should avoid at this point?

Matthias Endler 00:51:21 I would say three things. The first one will be avoid generics. Just use concrete types until it’s really necessary. Generics are, maybe some people don’t know a form to say this function can take a set of different inputs. For example, it can take a string, or it can take an integer. And sometimes this is really helpful where you say, this can take any type that implements this trade. So it’s like an interface which says this for example, takes anything that can be converted into a vector and there’s various things that can be converted into vector. And then you have to write this function once, but this gets in the way of prototyping. I would say use concrete types wherever possible. You can also copy paste the function, make your modifications, because more often than not, these two functions that you created are different in nature.

Matthias Endler 00:52:17 And as the prototype evolves, you will find that they look similar, but they are different so they diverged. And if you edit a generic too early on in your program, you might not have the patience or the insight to see that right away. And then you’re bound with whatever the design was at this point. So only introduce generics when clear patterns emerge. And also just in general avoid being fancy. Don’t add those generic type signatures like T AsRef or so if it’s not necessary. Now the second part would be avoiding lifetimes. We also covered that already. Lifetimes just sidestep them with cloning things. And yeah, the borrow checker will be happy and you can later grab for clone and improve your code again. Also in case where you think about multi-threading, maybe an arc mutex T is all you need really, you don’t really need to make your code thread safe right away. You can just put it into an arc mutex. So focus on the logic first.

Gavin Henry 00:53:28 What is cloning? Sorry, before we do Version 3?

Matthias Endler 00:53:30 Yeah. So cloning creates an actual clone of a memory block on the heap. So you have a string on the heap, it owns some memory, you clone it, then you have another string that points somewhere else on the heap. But it has the same input. It has the same nature. It’s also a string, it has the same length, it has the same contents, but it’s like an actual clone of your value.

Gavin Henry 00:54:01 And the ARC is a way to have a copy of a variable that’s unique, isn’t it?

Matthias Endler 00:54:07 Yes. An ARC is an Atomic Reference Counted value. And an atomic is a way for the CPU to enforce exclusive access to memory. And with an ARC and especially the combination of ARC Mutex, you can sidestep a lot of situations where you would have to add lifetimes to your variables. And in this case, you just say, well it is behind an Arc Mutex, so it will be locked, it will be exclusive to one owner that modifies the memory at one point in time. But there cannot be two writers at the same time in that region in memory.

Gavin Henry 00:54:49 So we want to avoid generics lifetimes. And there was a third one wasn’t there?

Matthias Endler 00:54:53 Yes. The third one that I like to do is to keep my hierarchy flat. Earlier we talked about Cargo script, and we can take this one step further. I keep everything in my main ORs and when I need a module, I just use the mod keyword, which is another keyword in Rust and I can add the module right in the same file in my main ORs. Because modules are not bound to files as they are. For example, in GO, they are a separate concept. You can have multiple modules in the same file. And I used it very, very often. So there’s no need for complex organization. You probably don’t know the names of things right away and it will be hard later on to go from a very nested hierarchy to a flat hierarchy. So I keep it flat and I experiment with the structure in the same file. Once I feel confident that this is the structure I want to go for, I can always move modules into separate files when the structure stabilizes.

Gavin Henry 00:55:56 Yeah, it’s a good indicator like you’re saying, where you search the source code to replace unwrapped with Anyhow and some context or et cetera, but you just don’t have to think about it now.

Matthias Endler 00:56:07 Yeah, yeah.

Gavin Henry 00:56:08 And do you have a rule of thumb for when you would want to put something in a new file?

Matthias Endler 00:56:12 Usually when I try to transition from prototype to production, so this is when I gradually replace the unwrapped with proper error handling and then I think about structuring modules, then I think about encapsulation, I think about composition instead of inheritance. How do I make these parts talk to one another? What are the guarantees? What’s the minimal interface that I can provide and how can I put the rest into a module and make it private? So this is the stage from prototype to production where I refine the type of structure as I improve my understanding.

Gavin Henry 00:56:53 Yeah. I also have the tasks located within that file potentially as well. So it makes helps you switch context completely to focus on.

Matthias Endler 00:57:00 Yeah, exactly. And also adding documentation.

Gavin Henry 00:57:03 Yeah.

Matthias Endler 00:57:04 And replacing owned types with references where appropriate now is the time to do all of this extra work to really understand, okay, how should my hierarchy look like?

Gavin Henry 00:57:16 And that would be a good use case for that Bacon crate as well. If you’re writing documentation in that file that you’ve just moved away from main RS. Because you could refresh the browser to make sure it’s looking good.

Matthias Endler 00:57:27 Yeah, you wouldn’t see the documentation in Bacon, but there is a Cargo dock command. And this is also the stage where I look at this and see how does my documentation look like? Is it clear? Do I need to be more specific to any Ö

Gavin Henry 00:57:42 Does that open a browser for you?

Matthias Endler 00:57:44 Yes.

Gavin Henry 00:57:44 Oh, I didn’t know that. Well, actually I did. I remember now. Yeah, I was just thinking it might refresh the browser for you. Like the tools.

Matthias Endler 00:57:50 Yeah, you can say Cargo doc open, it opens the browser.

Gavin Henry 00:57:54 Yeah, I’ve actually written about that as well. I just forgot.

Matthias Endler 00:57:57 Super helpful.

Gavin Henry 00:57:58 Okay, well let’s start wrapping up. I think that was a really good walkthrough, picking what to leave and what to take, what to focus on, and I certainly learned a lot. So I’m really glad we did this show and thanks for coming on. I hope it helps others understand that you can actually prototype in Rust. You can have an idea, you can play around, you can share the code. You don’t have to think too big or too hard, too early. Was there anything that we missed you think would be a good time to talk about?

Matthias Endler 00:58:27 Yeah, I think for all the people that have a tendency to optimize prematurely, you must try really, really hard to write slow Rust code. Rust is an excellent prototyping language despite all the wrong perceptions. So don’t really think about performance too much when you think about prototypes. Let the type system guide you. You can always go in and make things faster with profiling in the long run, but you can never get back the right abstractions once you go overboard and use now pointers in other languages or you do all these anti-patterns. So yeah, let the Type system force better design on you upfront. And I would assume you need fewer iterations from prototype to production with Rust. At least I would encourage everyone to give it a try.

Gavin Henry 00:59:19 Excellent. So how could people get in touch or reach out if they want to work with you or just play around with some of your ideas or have a chat?

Matthias Endler 00:59:27 Yeah, people can go to Corrode.dev. This is where they can learn more about the services that I provide. You can look through the blog posts. We also have a podcast about Rust usage in production. It’s actually called Rust in Production. A very fitting name.

Gavin Henry 00:59:46 Yeah, it’s really good. I like it a lot.

Matthias Endler 00:59:47 There’s my email address on there and yeah, feel free to reach out even if you don’t want to have a very long running project. It’s sometimes good to have another pair of eyes, especially when you go from prototype to production. Just see if everything is in place and we keep it very lean and take it from there.

Gavin Henry 01:00:07 Matthias, thank you for coming on the show. It’s been a real pleasure. This is Gavin Henry for Software Engineering Radio. Thank you for listening.

[End of Audio]

Join the discussion

More from this show