Rust Programming Techniques

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

This is an excellent Tutorial session for newer rust programmers and some of the whys are wherefores of rust idioms and syntax.

It has a couple of exercises as well that are well thought out with questions by the attendees answered live.

๐Ÿ‘๏ธŽ︎ 5 ๐Ÿ‘ค๏ธŽ︎ u/rebo ๐Ÿ“…๏ธŽ︎ Jan 28 2018 ๐Ÿ—ซ︎ replies

I really dug this talk - especially Nicolas' notion of "error modules" as another kind of architecture boundary.

I'm a new rust programmer, who has read the book and has started on the other book and I definitely got something out of it. Recommended viewing for others in a boat the same make as mine.

๐Ÿ‘๏ธŽ︎ 2 ๐Ÿ‘ค๏ธŽ︎ u/asciiascetic_ ๐Ÿ“…๏ธŽ︎ Jan 28 2018 ๐Ÿ—ซ︎ replies
Captions
good afternoon everybody how we all doing so coming up in here we now have a workshop presented by Nick Cameron on Rus programming techniques please make him feel welcome hey thanks everybody and thanks for coming for the tutorial so I'm Nick Cameron I'm from Mozilla I'm quite involved in rust I'm I lead the tools team I'm on the compiler team the language team and the core team and today this is meant to be kind of like an intermediate level tutorial hopefully you'll still be able to get something out of it even if you're kind of a very advanced programmer in rust or you've never touched rust before I'm not going to kind of introduce the syntax but hopefully if you can program in something you'll be able to follow along I what an alternate title I played around with this talk was kind of thinking in rust and why I want you to get to what I want to help you do is to really get into kind of like the mindset of an experienced rust programmer so you're not kind of like fighting the rust compiler anymore you're kind of like working with it and winning so the it's it's mostly going to be kind of like a chalk and talk kind of tutorial I didn't realize we'd have this awesome room where you could like actually get some really proper work done but there are going to be like a few exercises but don't worry if you don't have like a rust kind of environment set up like that you can do them on pen and paper if you like and maybe even use them the compilers gonna be cheating for some of them ah and I'll stop every now and again to ask questions but please interrupt if if you don't understand something or I can clear something up for anyone don't wait until the end cuz it's quite a long tutorial and I'll have forgotten what I was talking about okay so the tutorials going to be in kind of two halves the first half is gonna be kind of programming in the small so like how to write better function bodies how to write better kind of like code the micro kind of level so gonna cover a few of the really key fundamental data types that every Russ program I'd like users will have you Russ program uses a lot I'm gonna talk about some control flow that's maybe not so familiar from other languages and then I'm gonna tell you how to like avoid doing that and write better control flow and by using kind of other constructs and some methods on these types the second half of the tutorial is gonna be kind of like programming in the large although it's more kind of like the medium are not so much gonna touch on kind of architectural issues so much is kind of like design issues how to design the kind of like data types and abstractions in your programming how to do error handling in your in your program Before we jump into all of that though I just want to kind of like talk about some principles that have gone into kind of like the design of rust and its libraries and that will kind of give a themed this tutorial safe and fast this is like the kind of the foundation of everything and rust we we want to be like a systems programming language and that means it has to be fast like as fast as C or C++ or even like hundred and assembly and a lot of cases but it has to be safe memory safety is like a big selling point and rust in this respect we kind of as safe as like a higher-level garbage-collected language but also safe in other ways we want to like avoid giving you so many options for shooting yourself in the foot as you have in some programming languages but really this is just like this I mean this would be quite cool in a language if you just have safe and fast write but actually this is just like the starting point this is like what we consider like table stakes or any kind of like new feature in the language and what we want kind of like move towards is we want kind of the the language as a whole to be kind of economic we wants it to be pleasurable to write Russ code we want it to be easy to to stand when you're reading Ross code or debugging rust code and so that's kind of like what we we want everything in rust to be where we want to end up and that's kind of like a theme for like a lot of the parts of the tutorial is I'm gonna start by showing you a kind of a basic maybe naive way of doing something in rust and sure it'll be safe and it'll be fast because hopefully everything is but then I'm gonna show you a better way to do it which will give you kind of more concise code and easier to read code and faster to write code okay so let's start throwing in the small so just to let you know what's coming up the data types are going to talk about option results and iterator and I reckon that in any given like Russ program maybe 50 percent of the types that you ever use are these three types basically so these are really kind of like fundamental to rust and in terms of control flow we're going to touch on the match statement which is a kind of pattern matching construct and iteration in just kind of it in in general I know we're not quite gonna go in this order we're gonna kind of zigzag between kind of data types and control flow okay so I'm gonna start with some C code so this is fairly idiomatic C code although it's not actually doing very much or C++ sorry we're gonna take her to stern objects of type foo and of course we're good programmers so we're going to do a null check because there could be a null pointer and if it's not then we're going to call a method on this object so what could go wrong there's actually quite a lot I mean it's pretty easy to forget the null check I mean hopefully we're all excellent programmers no one here would ever forget doing a null check but it's a pretty easy mistake to make if you're a beginner and actually much more common is you don't need the null check because there is some invariant established somewhere else but of course it's code and code evolves and that invariant changes and all of a sudden you need a null check we didn't need it before but like there's no indication in the code that you need one and so nobody adds it and so you're missing the null check the this pointer variable could get mutated I mean probably not in the function as it's written at the moment but you know if this were a more complex function and especially if that variable is aliased and/or it gets passed to some other function it's actually pretty easy for a variable to get mutated without it being obvious from the from the code and pointer could be like it could be non null but it might be like otherwise invalid so I mean it could it could just be a random punch in numbers right or it could point to memory that's already been freed or to memory that's never been initialized and your null check isn't gonna catch that so here's what the same code looks like in rust and you don't have any of those problems which is nice so let me kind of explain what's going on here as you say I'm not really going to like explore in this tutorial why you're not getting all of those problems just kind of like some of them you have to set my word for it on the others so the really interesting things that are happening here let's start with the type of the points who are passing in so the amps and foo is a is a reference to foo and references are pointers but they are they can never be known okay if you want to represent a pointer that might be null you have to wrap it in this option type and an option says either you have something or you have nothing okay so it's just like a null pointer like ether it like we've got something and we're pointing all we've got zero and we're pointing at nothing except that it's explicit in the type and rust okay so you have to opt in you know that it's there and unlike a null pointer you are forced by the type system to check whether you have something on nothing if you try to just run this G method on the the PTR variable it would be a type error instead we have to use this match statement and a match does a pattern match and there are two patterns that this option can be it can either be sum or it can be none and in the son case you can go ahead and use the value that's there and you know that it's there and in the none case we do nothing and we return exactly like we would in the null check and because of the scoping in this statements it's impossible to mutate the the PTR variable once you've done the equivalent of the null check so these these two two these two key concepts option and match and it's gonna come out drill into into these a little bit so match is an exhaustive pattern matching it's a little bit like a switch statement except that we force it to be the compiler kind of like checks that will be exhaustive so here's a really simple example we've got an enum type with three variants a B and C and assume that X has type foo we're then going to match that variable and we've got the three cases either it's a either it's or it's B or at C and in each case we have a code block and we execute the you know obvious one at runtime you can use the an underscore here to say like you know we're interested in what happens if it's a and we want kind of like we interested in anything else happening and so you don't have to kind of like repeat yourself all the time and unlike a switch statement the match statement can be used as an expression so oh and also it binds variables in the pattern matching so we've changed the enum a little so now the a variant takes a value with it so either it so if we have a foo like X is meant to be a foo here then we've got three options either it's an A or it's a B or it's a C and if it's an A then it's carrying with it a 32-bit integer and so when we pattern match using the match statement in the a case we can bind this new variable n to the value and we can use that value in the code block and we can or we can kind of return it as the value of the match statement and in this case like all the branches have to have the same type so in the default case we use 0 and then we assign all that into the the variable little case food so that's a very very brief introduction to match there's a lot more that match can do you you can have quite fancy patterns you can it does deep pattern matching if you have kind of like nested types that you want to match into there are kind of if guards that you can have on the the match arms and so forth but that's if your interests in that you can can look it up the option type so we've seen can await the option type in action before the option type is not magic it's not built into the language it's just a part of the standard library and it's a perfectly ordinary enum it has two variants some none but it is generic so in the some case we have whatever we instantiate it to so before we saw we had like an integer as the type parameter and we can have any any type we want in the some case it's very easy to use because it's so common it's part of the Russ prelude that means like you don't need to import the name or quad or qualify the name when you use it you can directly use kind of the option or some or none the names of the variants directly in your Russ program and also in arms are kind of optimized cleverly by the vampire so even though you have like an option of a pointer and so this seems like it's a little bit more heavyweight than just having a pointer that might be nullable the compiler will optimize that to the same thing okay so if you know that a pointer will never be 0 which you do or sorry the compiler knows then it will optimize the the option representation in memory so that it's just a pointer and the non case is represented by the null pointer so it's some you know that works out to be just as fast and very efficient to see when you at runtime the result type is very similar to the option type it's just another generic enum the difference being that whereas with option you either have like something or you have like nothing with the result with the result type you either have something or you have an explanation of why you have nothing okay so you have like the the value type which is kind of like the success value and you have like an error type in case the an error happened so I'll just show that in action here we've got a very small slightly silly function it takes an integer and it returns a result where the success value is an integer and the error value the error type is a string in this case it's just gonna be a message about what went wrong and we're going to use the pattern matching construct match again on the input and if it's a positive integer we're going to return success and we're going to add 10 to the input and if it's negative then we're going to give you an error message and then here you see how we would use this kind of function so we call on the second line in the main function we're going to call that function and we're going to get back either a success with an integer all we're going to get back the strength and we're going to use match and we're going to give a message to the user that's appropriate in in each case so just look is it clear to everyone what's happening in this code does anyone do I need to explain bits no reason like I mean success is kind of the sorry I should repeat the question the question is why why are the type parameters in the order te and not ET that is success type failure type and not failure type success type and the order of type parameters in Rus is not really important doesn't signify anything like we don't have any kind of currying or anything so like it basically doesn't matter what order the type parameters are in and given that like success is more common than like error hopefully and so that's the order it's in any other yep so the question is is there any significance to the exclamation mark yes so an exclamation mark means that this is a macro usage rather than a function call so you so print line is a macro not a function and whenever you use a macro you have to use an exclamation mark the reason for that is that there are things that a macro can do that a function call can't do for example a macro can hide a return so you know print line prop isn't gonna return out to your function but like other macros might do so it's to make it clear when you're reading like that the invariants around you know what you can assume this is going to do of are different any other questions for move them excellent okay so the result type is absolutely central to rust error handling essentially it is rust error handling story we're going to go into a little bit of detail right now about how you can use it ergonomically and then we're going to see a bit later in the talk about kind of like designing error handling but its result that's really important okay and so what you often see a function signature which looks like this like it returns a result of T in and arrow type and the kind of way the way to read that is that it returns a team the success case or it can throw this kind of like error this is not valid rust syntax but maybe will evolve to something similar to this soon and I think it's worth making kind of like or comparing to kind of like error handling mechanisms in other languages if you're kind of like a die-hard C program out then you're probably used to kind of like return codes and result basically works like that except that you're forced to check it in the same way that you're forced to check for null pointers so you can imagine you know the these are the error codes like in when you get like your arrow type but like the you know if you have success then you get the the success value passed back at the same time you can also see this kind of like analogous elite exception types and Java or C++ except that there's when we kind of throw an error there's kind of nothing special is happening in the language there's no stack unwinding whatever else it's just a a regular return from a function so I want to introduce a few bits of control flomax that make it easier to use types like result so it's pretty common to see idioms like this with the match statement where we call some function or we do some work and if it's if there's success then we do something with the value that was that we got back and otherwise we're just gonna do nothing actually this is probably more common with option than with result you probably want to handle your error and not just do nothing although that's a strategy but this is this is common nevertheless and so we have a special construct which does essentially kind of non-exhaustive pattern matching which is a flat so if let matches just a single pattern so here like the we've got like a result type and so we're gonna match like the okay pattern and so if you've got kind of okay we're gonna do something with the value we got and it binds the the variable I in the same way as match does and if it's not that then we're just going to carry on here's another somewhat common idiom in rust which is actually more common with result type we're going to call a function again like we had H before and if it's okay we're gonna do something with the variable I and if there's an error we're not going to try and handle it we're just gonna throw it again and assume that I caller can handle it in some way I'm assuming the return type of the function here is is result otherwise this wouldn't work ah this one or this one okay okay so like the if flat is kind of a single keyword and then okay I as a pattern and then equals just as an equals and then we've got an expression which will be executed and then we try and match the result of that expression against the pattern and if it matches then we do the block so it's it has exactly the same semantics as this okay so here's here's this other idiom where we we are we the take the success or we throw the error and actually it's more common to write this like like like this and rust where we're using the kind of like expression or inside version of the match statement and we either binding into a local variable if we had success or we're throwing the error to be dealt with elsewhere and this is such a common idiom in rust that we have some extremely sweet syntax to deal with it which is this so when you see like this postfix question mark operator that's basically doing exactly that match right if we it was successful then we keep the we keep that variable bind it into I and then we can do something with it and if there was an error we throw that for our caller to deal with okay so this question mark sure and this becomes this yeah okay so repeat the question what happens if the return types are different so it doesn't matter what it will do type adjustment yes is the short answer it will can it doesn't matter what the tears and actually the ease can be different as well as long as the compiler can find a way to convert one into the other and it's kind of beyond this tour to talk about how we do that but it can be done yes yes I mean I I think it's probably easier to kind of like look lit like this in that like the [Music] yeah like if the error case is still kind of like wrapped in the result and will be be thrown because the return type for the function has to be result not with the arrow type whereas if we want to use the value then there's no point about keeping it in side the result because we know it must be like okay at this one okay result-type yes so we're not so the unwrapping kind of happens at the patent level so it's the the unwrapping is not part of the the whole match statement the unwrapping is like because we're matching the okay pattern then we're unlike by by matching it we're kind of doing the unwrapping and we're left with the I was on the earth case so I probably should have used the better that's that's a local variable called not capital e of which is the name of the variant and so there's no unwrapping happening on that case there's a question over here somewhere so the question is this looks a little bit like a macro shouldn't there be like an exclamation right so the the history of this is it used to be a macro it used to be called tri exclamation mark and was was a macro and we're on earth question mark for reasons I'll discuss kind of on the next slide and basically it like it's not a macro and so it doesn't need it and if you're looking for like all the places a function can return while you look for the return keyword you look for an exclamation mark and you look for a question mark and if you see these bits of punctuation then you know it could return and if not it can't Oh what so no we do not have that videos I will talk about unwrapped a little bit later the short story is you should never use it okay so this is the simple kind of syntax for question mark and you often see it in chains of method calls or field lookups like this where you know if you did the matching explicitly this would be like I don't know ten lines of code and it'd be really hard to follow what's actually happening here but in this case you can actually see quite clearly what's the you know what's the kind of control flow what's happening here it's just a chain of method calls but if you want to know what the exceptional control flow looks like it's explicit here like exactly where you might be kind of throwing out of this function and so the question marks this really nice compromise in that it's extremely lightweight so you can quite easily see what the kind of common case control flow is doing but it is explicit and so if you need to know like every possible path of control you can see it right so having kind of talked about various kind of like control flow constructs I want to now show you ways you can avoid using them at all okay so there is a lot of methods on the option and result types or not don't have anywhere near enough time to cover them all here but you should look at the documentation I think kind of like mastering these things is a really important step to kind of like writing kind of clean elegant code and rust I'm gonna that there's kind of like a symmetry between the option and result types so I'm only going to cover option but pretty much everything I cover here exist for results as well either exactly the same or in some variation so here's kind of a common thing that you often want to do so sorry look at the second function this kind of maybe add four so we're gonna pass in either an integer or not an integer like nothing at all and if we get the integer then we want to add 4 to it and then return that and if there's nothing there we're just gonna return nothing okay so this is kind of like a really common way it's kind of like unpack do something repack and we can express this really simply and rust with the map function method so this is a method on the option type and so it can it's if there's something that then it applies the function that we passed to it and if it's not if there's nothing there we just pass none out of it and actually it's much more common to see kind of this closure form where the the function we're going to apply is written in line as a closure and even if this syntax is a little bit kind of funky if you're coming from C then it should be kind of clear what's going on here X is the what we've we've got an option if we indeed have anything at all and then we're going to return X plus 4 so is it clear to everyone what's going on yeah yes yes so I'll show this on the next slide yep it does sorry the question is why doesn't the question mark work for the option type and it does and you could use that here but it would actually be a little bit less sin for doing it this way because you would have to do the repacking yourself so you have to you know put the the sum in there whereas that's done as part of the map here yeah so the question is given that you have liked this map function why have the question mark operator as well it just leads to like better control flow like when you when you have this really common case of like the chains like this with the question mark it's really easy to read like what's going on here like a set of kind of like nested map calls is just not as clear no it's a short answer we don't have high kind of tight so the question was is there like some kind of syntax for doing this in general I guess like do syntax in Haskell and the answer is no we don't have higher kind of types and rust we can't really express like the the concepts of kind of like do in the more general case so we have stuff like the question mark operator and functions as a replacement okay I lost my place where do we get up to okay yeah I was gonna show you the function signature form app which I had a question about just now so looking at the the function signature hopefully it's kind of clear what's going on here even though it's not the clearest function signature if we look we see the first argument itself we have to be explicit in rust about taking a cell for this argument so that just basically shows that it's a method with a receiver we see that we take a function and if you look at the where clause you see the type of that function don't worry about the details but like the the essential bit is we're mapping T to you where T is the type parameter for the receiver and you is the type parameter of the option that we return so in the example here T and u are the same they're both i-32 but there's no reason they have to be the same in general okay so I want to look at some other methods and I'm going to go over there's a bit more quickly and I'm just going to show you the type signatures so one kind of intuitive way you can think of optional result types is like a like a boolean where if you've got saw more okay it's true and if you've got nothing or error then it's false with that interpretation then it makes sense to want to and or all these things together and indeed we have these functions so and takes another option and we can look at the two and if both of those are true like some then we're going to return the the second one that we passed in and if either of them is none like false then we return none similarly or does exactly what you would expect these are actually kind of like more useful if you think about these well like maybe a more kind of practical programming terms so and is kind of like map except where what you're sorry I have skipped ahead I should before getting to this I should introduce that they're like the and and the or are kind of eager versions of and and or and that there are also kind of like lazy versions of and and or so and then or else you can think of the boolean operations and and all but the short-circuiting kind so and then takes function and if the the receiver is none we never execute that function but if the receiver is some then we unwrap the receiver pass it to this function and if that function gives you some then that's the results of the hole and then function similarly like or else takes a function and if the receiver is something then it never executes s-- whereas if it's nothing then we ask you the function and then you can think like and then is exactly like mapping but where the function that we're mapping can actually return a nun right rather than always succeeding where we would wrap it back up in the map case and then lets you execute a function that might return something I might return nothing but handles like the nun case to start with similarly or else you can think oh well or as well you can think of being like about having a default value so it's saying if if this is something then great I'll just you keep using it if it's if there's nothing there then I'm going to execute this function or use this value and and that's kind of like the default I'm going to going to use so these are both like really common really useful functions for on the option and result types and there's this pattern that often you have they're kind of like eager and lazy versions where the eager version takes of value and the lazy version takes the function that'll be executed if necessary okay okay before I it so this is probably another good point to ask if people have questions on on these methods know the type parameter is on the option so it all requires the argument that's passed I have the same type parameter as the the receiver was that's not the case with hand okay cool so another couple of methods so I said before option and results are fairly similar and it's often extremely common that you will want to convert between them and so there are methods to convert from an option to a result and vice versa so okay or turns an option into a result and you the the all bit is because you have to supply like the error case if it was if there was nothing there and there's a there's like a lazy version okay or else of this it sir is a way to convert an option into an iterator so if boolean is one intuition you can have an option type you can also think of an option type as a collection it's kind of like a list which always have zero or one elements in it so given this intuition of like a list of zero one elements then you might want to iterate over those elements and so it gives you an iterator back and we'll see kind of like more details of the iterator next okay so time for an exercise basically here are two functions these are written kind of badly so write them really well is the exercise there's a link in the top right corner for the documentation you might find it handy to look up the the docs for for option in order to do this we can figure figure this out I'm just going to give you a couple of minutes to do and if you have any questions that you along just stick up your hand but I'm gonna interrupt because I'm already slightly over time sorry but I want to go through what we're what we've talked about so I'm just going to go through the first function first so this if input it's none returned none and then unwrap the input well we can do this much quicker using a try operator sorry the question mark operator so that this is exactly what's happened in this version but if we keep reading a little bit further down then we see that like in we're then doing like an if test and we're gonna return none and otherwise we're gonna wrap it all back up in a sum so that suggests that we can do even better we can use the and then function that I talked about before rather than like the the try operator so this is doing like an implicit check and whether it's some or none and then we're returning none if we're we're less than zero and actually if you're using nightly rust you can do even better than that because there's a function that hasn't been stabilized in the standard library yet called filter which does what and then was doing like with some or none but then applies a predicate to the value if there was something there and effectively kind of like those and then returns like some if the predicate is true and none if it's if it's false okay so the second function is a bit simpler it just has a match statement where and it calls the previous function which returns an option and it gives you an okay result or it gives you an error and this can quite easily be mapped onto the okay or function that we talked about earlier this is just converting from the option type into the result type so does anyone have any questions about the exercise before we move on okay great so on to iteration and iterators so I want to start again with an example in C we've got a ping all function this is going to take an array of some type foo it also needs to take the length of that array it's gonna iterate over that arrays and standard C for loop and then it's gonna index into that array and call some ping function on every element of the array what could go wrong so that length might just be wrong right who knows who's calling this function right there's nothing that says they're gonna get this right the array could actually be mutated inside the loop we could be adding something on to the end or taking something away or changing things or what have you the counter variable could be mutated I mean not in it that would be an obvious error in a simple loop like this but in more complicated loops especially if you're mutating the thing you're iterating over then it's kind of common to mutate the counter you could have some logic errors so a really simple one would be you know starting at one rather than zero or using less than or equal to rather than less than these are probably pretty obvious hopefully they'd get caught in code of view but who knows and certainly once you start having like nested iteration or you're iterating over multiple arrays or you're going backwards rather than forwards or you're stepping over multiple elements at a time it gets much much easier to make this kind of logic error finally this array is just a pointer and so it could be null and we'd into a null check and it could be otherwise invalid in the same way that a pointer could be so there's a lot that could go wrong here's the version in rust so hopefully the type looks kind of similar unlike see the type includes the length and so we don't need to separately pass the length so that's one thing that can't go wrong iterating over it is like a much simpler expression it's a for-loop but it's not like a c4 loop we're just gonna this is going to bind the variable F to each elements in Fu's in sequence and then we're gonna call this ping function on it so this is already like much better hopefully you all agree but actually it's kind of uncommon to even see like four loops for this kind of single simple case because there's a whole bunch of functions on iterators just like there's a whole bunch of functions on option and result that let you write kind of like much more succinct code so here we explicitly have to ask for an iterate over the array using the inter function and then there's a for each function which just executes the function for each element so having got this kind of like taste what's going on I want to kind of like oh yeah sorry question so the sorry the question is what's the benefit of using for each rather than for its so the I can I want to make the general point that when we see like other functions that I'm going to introduce next then it gives you kind of like a nicer kind of control flow it's more explicit about exactly what's going on with for each there's not a lot of benefit over using for like it's a it's a more secend construction if you're not doing if you if you know if this was gonna be like a 10 line closure I would say use a for loop if it's just like a real simple thing like this then it's easier to read the code if you use for each it's just on one line okay let's look at some methods on the iterator type so option and result were concrete types I showed you the the definitions that are in the standard library iterator as a trait and we're going to cover traits like at the end of this talk but what the important thing is that like iterator is kind of like an interface of which there are many many concrete implementations you may even write your own it's common to write your own in rust code and that for various reasons that means that actually looking at the signatures these functions is not very useful so I'm going to do this all by example rather than by showing you the signature okay it's so a vac and rust is just a resizable array so here we're just going to create our back we're going to call it iran it's got an iterator and then we're going to use some functions so map just like map on option applies this function to every elements of the it's razor and then gives you a new iterator filter applies the predicate and if it's true it keeps it and if it's false dumps it so this gives you a iterator that's the same size or shorter and then for each we've already seen what for each does it just execute this function on all of these so what's the output going to be here you can have a think or shout it out if you're feeling brave yeah so we just going to prints out two three and four okay another couple of functions this time we're going to use a for loop as well so it's actually pretty common to kind of like gas an iterator and manipulate the iterator in some way and then use a for loop to iterate over the iterator if you like so we're going to use the chain function so chain takes two iterators and change them together so you got a new iterator that iterates over all the first element all the elements in the first iterator and then all the elements in the second iterator enumerate well sometimes I mean so in the for loop we saw earlier there's no counter variable sometimes you want a counter variable right it's useful so enumerate gives you a way to give that back so enumerate takes an iterator and then gives you back an iterator which is an iterator over pairs of the counter variable and the value from the original iterator and so that's when we see the for loop doing some kind of basic pattern matching on the left hand side here so I is the count value and V is the the value from that we're iterating over and then again we're going to like print stuff out so what do we think it's gonna get printed out this time yes just pop up what's gonna get printed out okay finally the collect methods like we've gone from a collection like a vector into an iterator but often you want to go back you've done your iteration and you want to collect back down into a collection so in the first example here we're going to get our iterator we're gonna run map we've seen map just now and then we're going to get a new vector which is kind of all the results that we've tucked that we've got and then in the second example we're going to do map again but then we're going to run in numerate and so this is going to give us an iterator of counter variables and value pairs and then we're going to collect into a hash map so we're going to get a map from the counter variables to the values and the fact that these are two quite different things gives you a hint that collect is quite a smart little function and exactly how it behaves depends on the the type that you're expecting and that's why we need to put some explicit types on the variable declarations even though usually in rust we can just infer these and the the output is going to be like a vector and the hash map with these values question a hat so question is are there any lazier taters and all these raters are lazy or most of the iterators are lazy yes and there's kind of like there's functions for taking like a certain segment of that yeah so when I say like this takes one iterator and gives you another one that's actually not what happens it happens lazily when you kind of like call one of the terminating functions like collapse or 4-h okay well I have a question so we have this back right and we've seen two ways we can iterate over it and I think intuitively these are very similar you can see why these might correspond but it's kind of odd that these are maybe not as close as you might be expecting right like the in the first case we have to explicitly get an iterator and in the second case we done but we do have an extra ampersand in there so what exactly is the for loop expecting like a for loop is built into the language is a is it expecting a vector but we saw an array earlier and we saw kind of some iterator stuff as well so what's what's going on here okay so let's start at the beginning like this this vak macro this creating a vector for us the type of that is capital v vac this is not kind of an iterator which is why in the first expression you have to call it sir to actually get an iterator but the vac type does implement into iterator which is a trait and we'll see what that gives you in a second but actually there are multiple implementations so there's actually an implementation for just vac which gives you an iterator over the values in fact and as an iterator for a reference to vac which - an iterator over a reference to each value in the vac and seeing us in our for loop we we don't want to kind of use up that vac we won't have access to it afterwards it's it's pretty common that you want to get the the references and so that ampersand before the vac ensures that we get the the implementation of into iterator which is going to give an iterator over the references rather than values and what is into iterator do into iterator house like a single function which just turns the receiver ah I forgot the South bit sorry that should be in to iterate sort of intuitive self and so it turns that receiver into an iterator and then the iterator trait it has a lot of functions I've been talking about some of them there's an awful lot more but the one that you actually the really kind of like key function is this next function and it returns an option we've seen option already and what happens is if the iterator has been used up we return none because there's nothing left to give otherwise we return the next element as a Sun so you might have an inkling of how this is gonna fit together but let's kind of like explore a little bit more so I talked about if lat there's a control flow statement before so it's just pattern matching and if we match the pattern and we execute the block there's also while lat so while light does exactly like if flat except it keeps doing it so while lat says while I can match this pattern execute the block and keep going and then when I can no longer match that pattern then I stop so this is essentially what the for code for loop boils down to we run into it so to get ourselves an iterator and then we repeatedly call it a dot next and whilst that returns a some variant we execute the body of the the loop and if it returns none then we stop but that's not the end of the story as we before with flats then you can rewrite that with match so similarly like with violet we can reduce this to even more primitive constructs but you need to know that their exists loop which takes no arguments and just loops forever it's an infinite loop and also the rust has a break statement and so I'm going to get you all to do this as an exercise which is can you write the this while lat loop that we had before to do iteration using these more basic constructs so just give you a couple of minutes to do that okay so hopefully you had some fun with this I want to kind of like just show you what the solution looks like quickly so we still need to kind of run the into iterate it into is earth function to get ourselves an iterator then we've all this infinite loop and we've got a match that will break out if there's nothing left on the iterator otherwise we're going to like a sign into a local variable and this should hopefully remind you a little bit of where we saw the question mark it operated earlier and the idiom that led to that so just to summarize like where we've we've come from here like we're looking at the for loop and I've shown you kind of how you can think of the for loop like getting rid of this implicit so a for loop works over an iterator it takes any any iterator and then by using kind of wild lap I've shown you how explicitly we're kind of getting that iterator and then iterating over it and then shown how like while that itself is can be thought of in even lower level terms the second part of it is probably not too useful but it is often useful in your Rusco to be able to use while lat for kind of various complex kinds of iterations so that's really nice things kind of having you in your toolbox okay so that's the end of the the first half of the the too-tall it's been slightly more than half the time but never mind and in the next section we're going to talk about programming in the large ish so we're going to talk about error handling we're gonna talk about ownership primarily like as a design principle and we're gonna talk about traits if we have enough time so error handling so my my my first and most important point I think is that you should think of error handling as an architectural concern in your program from earlier that means you have to think about it really early in the design process so when you're thinking you know should this app run on a server or should it have a GUI or should it have like a restful api like when you're making that level of architectural decisions you also want to be thinking about you know what is the error handling story gonna be for this piece of software and in this section we're gonna go over the kind of decisions you'd be making as part of that so we talked about results earlier I told you result was kind of like the fundamental part of the a fundamental part of rusts error handling story so we've basically kind of I've been showing you all this code that uses a result but whenever like we've got the error type who's basically kind of like you know ignored that so what should you do when you get this when you get the error case of your result you have a few options the first option is that you're able to recover somehow so often a way to recover is like you can pick an appropriate default value so you know that if this thing succeeded you've got a value and if it didn't succeed then there's a default value I can use there's if you were paying attention earlier you could write this more succinctly using like an okay or function ah sorry unwrap or function some people think that an appropriate way to recover from an error is to give it to the user and this is wrong you'd like it might at some points in your program be appropriate to alert the the user that there has been an error and maybe the error the user can give you some input to help recover from that the mistake is and presumably what is going on here is that like your actual arrow types are internal for the to the program it you should almost never actually show your internal arrow types to the user this is almost certainly going to give a bad user experience if you're going to do this you need some kind of like dedicated code in your in your software that handles the the internal arrow type and gives you like you know user interface over that that you're going to present the user okay it's option number two is to just wreath row so we've seen this before we've seen this is what the question mark operator is for this is like in I mean this particular function and I don't know how to recover from this era so I'm going to just throw it up the stack and hope that my caller or their caller and so on will be able to handle it oh yeah that's the yeah question mark operator the third option is that you can panic so a panic is a controlled crash in in rust so it crashes the current threat but it does so by it does so kind of like cleanly by unwinding the stack so destructors will get called and this isn't a seg fault it's not exploitable and you can actually catch this panic or you can catch it at thread boundaries it's you you don't net this doesn't necessarily have to crash your entire process and you can explicitly do that using the panic macro but it's much more common to use the unwrapped or expect functions on an optional error where they if it's if it's okay then we unwrap and if it's not okay then we panic so these are your three options so the first two options are like actual error handling and the third option is not error handling basically and it's acceptable to do that sometimes like if you're writing like a very experimental bit of code then maybe it's okay but really we should be doing one of the first two and so the obvious question it becomes like which of those two should I do so an intuition I want to try and get across is about kind of the modularity of error handling and so I want it like think of think of your code in terms of kind of error modules now these are not like this is not a rust concept this is not like something that's actually explicit in your code and an error module could be smaller than a rust module but more commonly it would be bigger than a rusts module I mean quite common is like your error module fits with a whole crate and an error module like is so within that error module you have context specific ways that you can recover from the errors that happen whereas outside there's no way that you can recover like in a in a kind of local kind of way and so you instead you want to present enough information so that you can do kind of like try something else completely instead so to give you a little example of this like imagine that you're doing some network i/o okay so within your error module then this might be as small as a single function right like you're gonna try to use this connection and maybe you'll get like a woodblock error back from your system call or maybe you've got a you know connection busy kind of error well you can kind of like recover from that in a very context-sensitive way you can just sleep and then try again right but if none of this if this doesn't work then at some point you've got to kind of tell your caller that like you failed to do this i/o and for them like whether you got like you know couldn't get the connection or whether you got like a wood block kind of error this is useless right but they might want to know well there was an i/o error trying to connect to this particular IP address okay so that's kind of a difference between like a kind of internal kind of error in an external error and where you want to convert from one to the other is kind of like the boundary of your error module and generally like within an error module you or a common pattern for error handling is that you always throw re throw your error when you're inside the error module and then you have in US and the kind of boundary of this module you have like a single place where you try and do recovery okay and if that recovery fails then you're going to convert the internal error into a kind of public facing error and so that's kind of like the the intuition that I kind of pushes to knowing when you should kind of like throttle versus recover is kind of getting this intuition of kind of like modular error stuff actually do you have any questions about that so the question is how do you deal with out-of-bounds accesses in arrays and so I so bounds checking is done at runtime and rust and it depends like you can index into an array in different rays the pen which depend will give you different ways of getting the different ways of handling the out-of-bounds error so if you just use like the square bracket operators and you're out of bounds then that's going to panic which you don't want to happen so if you think that you might possibly access out of bounds then you should not be using those indexing operations and you should use an explicit gap which returns an option I think either an option or result bshorts an option and then you can like explicitly check or you can use the various functions we talked about on the first half to say if this is a if there was something there then do something and if it was none than recover in some way so that yeah and then how you do it is up to you ah yes correct okay so if you remember the result type has two type parameters it has the value type that we haven't had success and how's the arrow type hopefully what value type you know that I don't need to talk about that it's obviously just what you're doing and your function that's going to take the value type what should you use for your arrow type this is kind of a big question but first of all result type I mentioned earlier it's not magic it's not built into the language it's just part of the standard library and it's actually totally possible to use your own result type but don't do that there's like there's very rarely any reason you would want to do that unless you are reimplemented some really kind of like fundamental library stuff I can't imagine a scenario where it makes sense to implement your own result type on the other hand aliasing the result type is almost always a good idea if like within a module you are using a single arrow type then kind of having a an alias for that makes your your code much clearer no you can think of like the an alias like this being resolved oh sorry the question was does this prevent kind of implicit coercion between your results I found thus general results I've announcer is no and you probably don't want that like you want to be you want to just be using the result type you want to be able to in gonna interact with the rest of the ecosystem by doing that but by this is just a convenient so you don't have to keep typing my every kind of function that you have this end which gets boring really fast you you can oh sorry so the question is there is an option for not allowing the coercion so I should clarify that there's it's not a coercion like the aliasing is happens or the D aliasing I guess happens very early in compilation so as far as the compiler is concerned these are the same types it's not that it's being coerced from one type to another if you do want an something that is like an alias but does not coerce you can use like a unit struct as like a new type type trick but you should definitely not do that with results it's the one option is you use something really simple as your arrow type so you could not use anything at all if you're if this is really early days code maybe you just want you know it's it's nice to be able to throw errors but there's maybe no relevant information that you can get in there and so you can actually just use the void type which is just the the empty parens and there's no information at all you could use error codes like you know one two three or error codes that's not very user-friendly but it's fine for experimental programming it's pretty common see people use strings like a Gaines it's not something I recommend in kind of production code but it's great when you're experimenting it's very lightweight and it lets you debug it the other option is you do this properly and you have like a struct or an enum with lots of variants and data that it's carrying around which lets gives you lots of information for recovering lots of information that you can give to the user about what went wrong if necessary and you have a couple of and but you have some options you can use like a single arrow type for all of your code or you can use multiple arrow types so if you're using a single one it's quite often to you common to use an enum so here we're just using the single enum type as our arrow type and every possible kind of error gets a different variant and you know any data you want is carried along with that so here we've got like a server error user error connection error whatever else alternatively you can use multiple different arrow types so different functions would return different throw different kinds of errors so here we've got like a struct for a server error and that has like quite different data from all the other kind of errors that we might have you in the end you're gonna have to deal with multiple error types anyway because the standard library is full of different ones with its own kind so you're gonna have to accommodate that so even though using a single kind of arrow type is simpler and often look kind of like what you want to do you still have to handle types often if you're thinking of like this in terms of error modules you want like one arrow type / arrow module and that's like another way that this intuition of arrow module is really helpful and I'd say if you do have multiple types and if you want to do can I error handling properly you should look at the failure library this is a library for error handling and it is on the road to being part of the standard library and hopefully it's gonna be widely used in the ecosystems so it's it gives your your librarian advantage so it lets you handle kind of lots of different kinds of arrow types very clearly it lets you change a rose together so often you'll have an arrow that has like an underlying calls and it lets you deal with that kind of like chain of causes and it'll allow you to have like back traces when you when these kind of errors get surfaced and it lets you interoperate very cleanly with the rest of the rust ecosystem oh and it's extremely easy to use okay like if you have like if this is your error in Unum then you can opt into using failure just with this very small derive fail attribute question question was it seems there's no era hierarchy is there a reason and we get I mean we generally don't have hierarchies and rust life there's no inheritance between data types so there's no kind of obvious weights that even have such a thing yeah is the is the short answer it just doesn't kind of like fit with the rust philosophy of the way we do things to do that okay so what should I do like you're your programmer like what should I do like what kind of approach does have error handling so the first question is are you writing a library or are you writing an application if you're running a library basically you have to do everything properly if you want other people to use your library they're going to expect it to have proper error handling so you need to use your own arrow type don't try and use like a string or an error code or something like a Paik like that you need to think you need to like consider like the arrow boundaries that I was talking about earlier and you should probably use the failure library if you're using an application if you're producing an application then you want to think about what kind of application that is if you're writing a script that you're only going to use once and throw away then just panic everywhere but like bear in mind that scripts that you use once and throw away have a tendency to become scripts that get used twice and then go into production and are in essential part of your you know business plan if you're running like very experimental prototype software then you probably want to have some quite lightweight error handling so you know something like using a string as an error message it's quite a good idea and if you're putting something in production then you need to do like with a library you need to have like a really kind of like full error handling thing I should say that it's kind of hard to refactor from a system that panics everywhere into a system with proper error handling but it's pretty easy to refactor from a system that just uses like strings say into something that those proper error handling so if you're running prototype or experimental software that's why I say you should kind of do kind of like the basic and error handling approach because it's really easy to factor that into like proper error handling whereas if you just panic in everywhere that's that's really difficult plus it lets you do stuff like using the question mark operator and the easy interaction with some of the standard library types ok so that's the end of this section and a good place to ask if people have questions ah sorry yes sorry the question was can you write a trait which relies on things having other traits the answer is yes and but stick around and we can talk about it rather than going into it right now cool okay so I have vastly underestimated how long this talk would take and I only have seven more minutes which is probably not enough time to cover even a half of one of the two sections that I have left so I'm happy to kind of stick around but I think they need to stop video recording at some point people might want to leave so now is probably a good place to stop and carry on thank you thank you Thanks so I wrecked so I have two sections remaining one is on ownership and using that for as a kind of principle of designing your us programs and a second section is on traits as an abstraction mechanism in rust I figure even going into the coffee break that we only have time for one of those so does anybody have a preference so raise your hands if you'd rather hear about ownership it's a few and raise your hands if you'd rather hear about traits I think the traits have it sorry ownership folk I'm happy so I think we'll be done hopefully early in the coffee break and I'm happy it's gonna stick around and talk about ownership if anyone wants to hear about that so that was everything you missed okay so traits traits are the primary abstraction mechanism in rust so if you're coming from kind of like any other well many other languages there's a quite kind of novel abstraction mechanism so rust is in the large at least is a very object-oriented language but it's certainly not a class or incited language and the way that you use traits is very different from the way that you would use classes and even if you think of they're more like interfaces than classes which they are that I think designing with traits is very different so the basic syntax for using traits is pretty simple you declare a trait using the trait keyword you in the ellipses where I've alighted the code you'd have a bunch of method signatures and then when you implement a trait for some concrete type which is what's going on in the second line then you have to provide the implementations of all those methods so I'll do a kind of little comparison between classes and traits so classes are a way to share both behavior and data okay so when you inherit from a class you inherit the data that's in the super class and you inherit the the behavior the methods that are there as well whereas traits are only about sharing behavior you cannot like share data in a trait that's part of the concrete data types that russa provides classes are mostly hierarchical they got a question earlier about kind of like why arrows on hierarchical in rust and we don't really have like the kind of hierarchies that you get all over the place you know in most other languages so just like it's actually possible in classes that you can have multiple inheritance and therefore you have like a dag of inheritance rather than a tree it is possible that you have some kind of inheritance between between traits and rust but it's the exception rather than the rule and in general you compose behavior without inheritance so it's it's more of kind of an unstructured blob of behavior sharing rather than a than a hierarchy so in a class whether you are sharing behavior is declared when you define the class so you write out your class and you write the classes that that it inherits from in rust declaring the traits you implement is completely different from declaring the concrete data types and so that means that you can implements a trait for data types that are in any of your kind of like dependency libraries or even in the standard library so it's quite common for you know I write my own trait in my code and then I can implement that for the integer types and for string and so forth so this means this all these things together basically make traits a much more flexible sharing mechanism than than classes traits are also really important in rust because they're the way that user-defined types can interact with the language itself okay so we saw earlier this iterates a trait and we saw the for loop which is part of the language and by implementing the iterator trait you allow your own like user types to be iterated over using the for loop similarly there is a D ref trait which allows your user defined types to act like pointers and to to take part in kind of dereferencing that happens with the dot operator and elsewhere and then there's the index trait which allows your user defined types to act like an array and be able to be indexed into with the square bracket operators and traits also essential to the safety guarantees and rust a really good example of this is the send and sync traits and these actually don't have any methods in them at all these are just market rates and by implementing the these traits you are making a guarantee to the RUS compiler that your data type is safe to move between threads or to share between threads respectively so this is my kind of lightning summary of traits and rust as any questions about how this is work yep so the question is are you restricted in way you can implement a trait for a concrete data type so the answer is yes so sir rust is structured into crates which are kind of like the compilation unit and similar to a like it's basically a conflict a library so either the traits or the concrete type must be defined in a crate within with the implementation so you can input a few define the trade so you can input a few define the concrete data or both of course but you can't in pull if you don't know either and then there's also a really sorry I should say really complex there's a somewhat complex set of rules around coherence which dictate like what kind of implementations you can have certain data types but yeah we we prevent and I'll do it the orphan problem question is do we prevent two implementations of the same trade for the same data type yes that's what the coherence rules are for but I'm not gonna explain them because they're complicated yeah any other questions okay so you should use more traits this is probably good advice for nearly every Russ programmer it's tenting I mean you can write Russ code without any traits and especially if you come for so you can write methods in what we call an inherent imple which is an impulse rate at all so these are just methods that are available on the data type without without naming a trait and if you're used to kind of like the LOA way of coding this connect you can feel quite a natural way it means you define all your methods on the concrete data type so it's just combining the the data and the behavior but you should probably avoid doing that you should use more traits so by doing so like you enable like much better testing if you're if you take a trait rather than a concrete type it's really easy to mock out the the concrete type and have like mock you have like a mock object for testing it makes your code much more extensible this is especially important if you're writing a library if you're always expecting a concrete type your code is not very flexible whereas if you have generic code that'll accept any type that implements a trait that's much more flexible and much more extensible and that's encouraging like reuse of your code it also tends to lead to a cleaner design like you can think of the traits as kind of document self-documenting code it tells you what what methods are closely associated with each other it tells you like what what the kind of like effect of having all these methods is and and and so on and so forth so it leads to just generally kind of like by using many using more traits you kind of end up with a cleaner more elegant design usually but there's always a limit as with any abstraction mechanism it's really easy to kind of go overboard and you know have the equivalents of a kind of you know enterprise javabean type situations so you know I appreciate the the trade-offs around abstraction and it's pretty easy to kind of factor out trait sort of split traits up and so forth so keep revisiting refactoring your code to to make more traits so what makes a good trait it should be small some traits as I said have no methods at all it's really common for the traits only have a single method for traits with more complex functionality like three four methods super common it's really rare to have like a lot of methods if you have like you know pages full of methods you probably want to refactor that's kind of you you having more of a kind of class design mindset there and they should be independent so don't make them too small if every time you're using one trait you're also using another one they're probably too tightly coupled and you want to either merge them or otherwise refactor and you know I've talked about this from the traits perspective but from the perspective of the concrete data what makes better data structures so I told you before these that you can have like methods in the inherent impulse and you should probably limit that that for behavior that is specific to the data itself so if you've got like a gasser or set of function that's probably better off as a inherent in on a is an inherent method if you've got something very closely tied to the implementation the same if you've got behavior that's could be specified generically or that constitutes an interface then that's something that you should factor out into a trait so hopefully I have enough so this is an example that I would have introduced in the ownership section I probably don't even have the time to introduce it properly here right now but on the right hand side here's a set of methods that I might want on this kind of abstract ship so chunk is an abstraction of some memory being managed so we've got empty to create a new empty chunk we've got two string which gives a string representation of a chunk we've got a debug string which does the same thing but for debugging for a developer rather than a user we've got get buffer to get like the underlying memory as a as a as an array of bytes and the size of that buffer and the potential capacity that we have that this would be not a great way to design this code it would be better to make this some trait so first of all we're going to use some traits from the standard library default is a trait that says I can create a default object and in this case that would be like an empty chunk display and debugger ways to convert an object into a string representation aimed for being a user facing string and a developer facing string and then we're gonna create our own trait buffer which is the kind of collection of methods that we might want for any kind of buffer provider and then that would let us for example have a mock buffer for testing that's always going to return zero or something like that okay so I've really just skimmed the surface of traits it's a really big topic and rust there is like a huge number of like little features around traits in the language that that let you do various things it's a good really good idea to kind of like read up on this stuff and you know it's a great way it's kind of like improve your design and make you a better programmer and I think like the best way to do that is to look at some examples so a few good examples to look at cerveau is the experimental web browser from rust they use a lot of traits to with of quite small traits yeah sure and the the the traits often correspond to something that like a web developer would know about whereas like the concrete data is kind of the internal structures chalk is an implementation of the trait system but this uses a traits for everything and it's great to see like how that kind of like really almost an overuse of traits actually leads to a really great design and Tokyo is an async IO library for rust and is great if you really want to like blow your mind about traits I mean we told stop his summary we talked about lots of stuff this is my contact information thank you very much but all the pay attention [Applause]
Info
Channel: LinuxConfAu 2018 - Sydney, Australia
Views: 68,890
Rating: 4.9169869 out of 5
Keywords: lca, lca2018, #linux.conf.au#linux#foss#opensource, NicholasCameron
Id: vqavdUGKeb4
Channel Id: undefined
Length: 92min 2sec (5522 seconds)
Published: Fri Jan 26 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.