Implementing methods and using Vectors

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

Previous LessonUsing struct to structure dataNext LessonUsing traits

Lesson Transcript

  • [00:00 - 01:07] In the last lesson we saw how structs allows to structure data in a reusable way, but we need both reusable data and reusable functionality to be really productive. Imple mentations are how we add functionality to structs that we create. For MiniLS we'll need some way to display the path property that exists on our entry struct. So let's go ahead and add a display method to entry using impol entry. So we can go ahead and go down here using the key, the impol keyword, and we impol on the name of the struct that we want to add some functionality to. And so in this case that'll be entry. And we're going to create a display function that will display the path of our entry to standard out. So methods in Rust take a self argument and so we'll pass a reference to self here. And then we aren't going to be returning anything. So we can just open up the function body. So in Rust methods take self as an argument. Self is sort of the equivalent to the this keyword in JavaScript, although self isn't accessible just anywhere, only within the context of an impol or a trait. And you have to pass it in explicitly like we're doing here.

  • [01:08 - 02:18] So once we have that argument, you just go ahead and pull off the path property, which we have defined on our struct, and then we have available to us in our implementation methods. So we're going to want to just print a line, and we'll pass a format string, and then our path as the second argument. So if we had an instance of entry bound to a variable called entry, for example, then we could just call entry.display. And we would get the path of our entry out. So the type of functionality we can have inside of an impol block are associated functions. Associated functions are different for methods because they don't take a reference to self. A common use case for associated functions is for constructor functions. In Rust, there is no special new keyword for creating new instances of structs. So a common pattern is to use an associated function called new, and then use that to create new instances of a struct. So we can, we can do that now for entry, just by having a new function here.

  • [02:19 - 03:15] So this is going to return an instance of entry. And so we need to be explicit about that, and pass the entry return type here to the end of our function. So to create a new entry, we can use the struct literal syntax here. And so all the struct literal syntax is, is we pass the name of our struct, in this case, entry, followed by curly brackets, and then the values that we want to bind to our properties. So in this case, we just need to bind a value to path. So in the, so in the struct literal syntax, the colon here denotes that we're going to pass a, or that we're going to bind a value to this property. So this is almost the exact same as the JavaScript object literal syntax. And so we need to bind some string, something of type string, since we define that up here, to this path property.

  • [03:16 - 03:29] And so we'll want this to be dynamic. So we can go ahead and pass a path argument into our new function. And then we can pass, we can bind that value, that argument to this property.

  • [03:30 - 05:45] And actually, rest borrow some of the JavaScript object literal syntax here. And we can just shorten that to path, as long as the property name and the value are the same. We can be explicit about this and return the entry. But since it's the last and only expression in our function, we can leave the return off, and this will be implicitly returned. So now that we have our entry struct, a way to create new entry structs and a way to display the path property, next we need to figure out how to get the metadata for a target directory from the file system. So we'll need a way to get all of the entries in a group given directory, and a way to get the path of each entry, which we can then turn into instances of our entry struct for accessing directory metadata. We'll again use the standard FS module. So we can go ahead and use standard FS up here. And on FS, we have a function called reader. And so reader returns an iterator over the entries within a directory. So something to note is that both reader and the standard ENV args function that we used earlier in mini WC, both return something that we can iterate over. But this point you might be asking, what is an iterator in Rust? So in JavaScript, typically, if we want to do something with a collection of values, those values are almost always in an array. A raise in JavaScript come with some built in functionality, like being able to loop or map over their values. And in Rust, any data type can be an iterator, as long as it implements the iterator trait. So we'll get to traits in a bit. But for now, you can think of the iterator trait as kind of like an ES6 class, and every value or type that we can iterate over has to extend that iterator class. This isn't a perfect analogy, but it should give you a basic idea of what's happening when we deal with values in Rust that we can iterate over. So using the reader function, we get an iterator. And so we can go ahead and so we can go ahead and loop over each result in this call to reader. And we can pass in our surname as the path argument in here. And so there is one gotcha here using the reader function, we don't get a plain iterator. And said the iterator is wrapped in a result.

  • [05:46 - 07:18] Since reading a directory is an operation that could fail. So in order to get the iterator, we have to unwrap it. And you can actually see here in the method in the function signature, that this is actually a result of reader. So now we have reader, which is an iterator over zero or more directory entries or dir entries. But actually, we don't get a just a plain dir entry here, we get a result wrapping our dir entry. And so this happens because of a quark and how redur the reader function actually interfaces with the us. But the particulars don't matter too much because we can just unwrap this result as well. So let's create an intermediary variable here, and set that equal to result.unwrap. And so now we have a plain dir entry value that we can use on the dir entry type, we have a path method that will return the full path to the file that this entry represents. So that's exactly what we need in this case. And we can go ahead and test this out and see what it looks like, just by doing a quick print ln. And we'll pass in the item path. Now this item path doesn't return something that can be displayed with just the regular format string. So we'll go ahead and have to pass in the debug variation here. And so now we can go ahead and run our program and see what this looks like.

  • [07:19 - 07:53] And we'll leave off any arguments and let it just default to the current directory. And there we go. So we have each item wrapped in double quotes here, showing each item in the current directory. Other than having to use the debug string format in print ln, we seem to be getting what we expect from our program so far. But we're not using our entry struct yet. So let's do that now. So because each directory can have multiple entries, we're going to need a way of maintaining a list of our entry type. In JavaScript, we'd use an array, but in Rust arrays are much more limited data type.

  • [07:54 - 08:15] So we need us some type of global list, which in Rust is the VEC type, short for vector. VEC is the Rust equivalent to the JavaScript array type. And actually, VEC is so commonly used that Rust just puts it in scope for us by default. So you can see we actually just have the VEC type here. So we want to create a new entry in each iteration of our loop and then add that entry to a VEC.

  • [08:16 - 09:31] To add entries to a VEC, first we need to create one. So we'll go ahead here and above our for loop, we'll create an entries variable. And that'll be equal to at least a start a new VEC. And you can see here that the VEC type also uses this new associated function idiom that we implemented down here. And in the source code for VEC, it actually looks very similar to what we did down here. And since we're going to be modifying this entries back on every iteration of our loop, we'll go ahead and mark this mutable as well. So now we right now we have an error because the Rust compiler doesn't know what type that this VEC is going to hold. So unlike JavaScript arrays, VECs and Rust can only hold one type. So for example, if you put, if you have a vector of integers, you can't add a string item in there. In order to fix this error for now, what we need to do is tell Rust that we are going to have a VEC of some type. And in this case, it's going to be a VEC of the of our entry type that we define below. And so now in our for loop , instead of printing out each item, we'll want to create a new entry and add it to our entries VEC.

  • [09:32 - 09:55] So in order to do that, we'll need to figure out one thing first. And that's how to get a string value from the item dot path method that we call that we can call on item here. This returns a type that is not string or turns a path above. Since our entry struct expects a path, that's the that's a type string, we'll need to figure out how to turn this item path into a string.

  • [09:56 - 10:47] So chained onto this item dot path method, we have this one method, we have these two methods that look a little weird, but do have the that do say something about strings and that's into OS string and into string lossy. We're not sure if that's exactly what we want. We do have also have this display method on path, which sounds a little bit close to what we want to do in the end. But this method also returns something that's not a string, it returns according to the documentation returns an object that implements display for safely printing paths. So this sounds pretty close to what we want, although we don't really know what this what it means that it implements display. But if we just continue down the rabbit hole for a moment, we can see that this whatever this display type is it actually has a two string method that we can chain off of it.

  • [10:48 - 11:13] So if we go ahead and set that this to a variable for now, we now have a path name that is of type string. And that's exactly what we need . So it might seem a little confusing happening to chain all these methods together, just to simply get a string. The reason for this is that interfacing with the file system is complex. And unlike JavaScript, Rust doesn't try to hide the complexity from the programmer.

  • [11:14 - 12:24] In this case, we trade complexity for more control. So with a string representation of each path, we can create a new entry in each iteration of our loop and then push that value to our entry's vector that we define above. So we can go ahead and do that by creating another variable here, entry. And we're going to set this to our entry new constructor. And we need to pass it a path. And we can pass it path name, which is now a string. And then finally, we have to add this entry to our vector of entries. So we can do that with entries, and then use dot notation to chain on a call to push. So very similar to JavaScript arrays, the vector type in Rust has a bunch of helper utilities that we can use that function almost identically. And so we can go ahead and pass our entry to the entries vector. And so so now an interesting note, if we actually remove this explicit type annotation, the Rust compiler still knows that this is a vector of entries.

  • [12:25 - 12:45] And that's because the only thing that we try and put into the vector is something that has the entry type down here. So we can go we can leave that off now, because the Rust compiler is smart enough to figure out what's going on. And so we'll just go ahead and clean up some spacing. And so now that we have all of these instances of entry being pushed into our entries, VEC.

  • [12:46 - 13:01] Now all that's left to do is loop over that VEC and call our display method on each entry. So we'll go ahead and do another for loop down here. And we can loop over a vector, because it is also something that is iterable. It implements that iterator trait that we talked about earlier.

  • [13:02 - 13:37] And then for each of these entries, we can call our display method and go ahead and test our code. So we'll go ahead and leave it on the default directory. And there we have it. We have regular strings being passed in and using our display method that we defined below. Go ahead and test it on a different directory just to make sure this argument passing is working as well. Yep. And there we go, a minimal LS clone.

  • [13:38 - 14:04] So in this lesson, we learned how to implement functionality on our structs with Impul. And we also learned a bit about iterators in Rust. We also learned about the equivalent to JavaScript, raisin, rust, the VEC. So next, we'll do a little cleanup of our mini LS program and extend it to have a little bit more functionality. So we'll change the program to show file size, modify time and permissions. And we'll change our display implementation to use a trait, which is a little bit more idiomatic.

This lesson preview is part of the Rust For JavaScript Developers course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Rust For JavaScript Developers, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Rust For JavaScript Developers

In the last lesson we saw how structs allows to structure data in a reusable way, but we need both reusable data and reusable functionality to be really productive. Imple mentations are how we add functionality to structs that we create. For MiniLS we'll need some way to display the path property that exists on our entry struct. So let's go ahead and add a display method to entry using impol entry. So we can go ahead and go down here using the key, the impol keyword, and we impol on the name of the struct that we want to add some functionality to. And so in this case that'll be entry. And we're going to create a display function that will display the path of our entry to standard out. So methods in Rust take a self argument and so we'll pass a reference to self here. And then we aren't going to be returning anything. So we can just open up the function body. So in Rust methods take self as an argument. Self is sort of the equivalent to the this keyword in JavaScript, although self isn't accessible just anywhere, only within the context of an impol or a trait. And you have to pass it in explicitly like we're doing here. So once we have that argument, you just go ahead and pull off the path property, which we have defined on our struct, and then we have available to us in our implementation methods. So we're going to want to just print a line, and we'll pass a format string, and then our path as the second argument. So if we had an instance of entry bound to a variable called entry, for example, then we could just call entry.display. And we would get the path of our entry out. So the type of functionality we can have inside of an impol block are associated functions. Associated functions are different for methods because they don't take a reference to self. A common use case for associated functions is for constructor functions. In Rust, there is no special new keyword for creating new instances of structs. So a common pattern is to use an associated function called new, and then use that to create new instances of a struct. So we can, we can do that now for entry, just by having a new function here. So this is going to return an instance of entry. And so we need to be explicit about that, and pass the entry return type here to the end of our function. So to create a new entry, we can use the struct literal syntax here. And so all the struct literal syntax is, is we pass the name of our struct, in this case, entry, followed by curly brackets, and then the values that we want to bind to our properties. So in this case, we just need to bind a value to path. So in the, so in the struct literal syntax, the colon here denotes that we're going to pass a, or that we're going to bind a value to this property. So this is almost the exact same as the JavaScript object literal syntax. And so we need to bind some string, something of type string, since we define that up here, to this path property. And so we'll want this to be dynamic. So we can go ahead and pass a path argument into our new function. And then we can pass, we can bind that value, that argument to this property. And actually, rest borrow some of the JavaScript object literal syntax here. And we can just shorten that to path, as long as the property name and the value are the same. We can be explicit about this and return the entry. But since it's the last and only expression in our function, we can leave the return off, and this will be implicitly returned. So now that we have our entry struct, a way to create new entry structs and a way to display the path property, next we need to figure out how to get the metadata for a target directory from the file system. So we'll need a way to get all of the entries in a group given directory, and a way to get the path of each entry, which we can then turn into instances of our entry struct for accessing directory metadata. We'll again use the standard FS module. So we can go ahead and use standard FS up here. And on FS, we have a function called reader. And so reader returns an iterator over the entries within a directory. So something to note is that both reader and the standard ENV args function that we used earlier in mini WC, both return something that we can iterate over. But this point you might be asking, what is an iterator in Rust? So in JavaScript, typically, if we want to do something with a collection of values, those values are almost always in an array. A raise in JavaScript come with some built in functionality, like being able to loop or map over their values. And in Rust, any data type can be an iterator, as long as it implements the iterator trait. So we'll get to traits in a bit. But for now, you can think of the iterator trait as kind of like an ES6 class, and every value or type that we can iterate over has to extend that iterator class. This isn't a perfect analogy, but it should give you a basic idea of what's happening when we deal with values in Rust that we can iterate over. So using the reader function, we get an iterator. And so we can go ahead and so we can go ahead and loop over each result in this call to reader. And we can pass in our surname as the path argument in here. And so there is one gotcha here using the reader function, we don't get a plain iterator. And said the iterator is wrapped in a result. Since reading a directory is an operation that could fail. So in order to get the iterator, we have to unwrap it. And you can actually see here in the method in the function signature, that this is actually a result of reader. So now we have reader, which is an iterator over zero or more directory entries or dir entries. But actually, we don't get a just a plain dir entry here, we get a result wrapping our dir entry. And so this happens because of a quark and how redur the reader function actually interfaces with the us. But the particulars don't matter too much because we can just unwrap this result as well. So let's create an intermediary variable here, and set that equal to result.unwrap. And so now we have a plain dir entry value that we can use on the dir entry type, we have a path method that will return the full path to the file that this entry represents. So that's exactly what we need in this case. And we can go ahead and test this out and see what it looks like, just by doing a quick print ln. And we'll pass in the item path. Now this item path doesn't return something that can be displayed with just the regular format string. So we'll go ahead and have to pass in the debug variation here. And so now we can go ahead and run our program and see what this looks like. And we'll leave off any arguments and let it just default to the current directory. And there we go. So we have each item wrapped in double quotes here, showing each item in the current directory. Other than having to use the debug string format in print ln, we seem to be getting what we expect from our program so far. But we're not using our entry struct yet. So let's do that now. So because each directory can have multiple entries, we're going to need a way of maintaining a list of our entry type. In JavaScript, we'd use an array, but in Rust arrays are much more limited data type. So we need us some type of global list, which in Rust is the VEC type, short for vector. VEC is the Rust equivalent to the JavaScript array type. And actually, VEC is so commonly used that Rust just puts it in scope for us by default. So you can see we actually just have the VEC type here. So we want to create a new entry in each iteration of our loop and then add that entry to a VEC. To add entries to a VEC, first we need to create one. So we'll go ahead here and above our for loop, we'll create an entries variable. And that'll be equal to at least a start a new VEC. And you can see here that the VEC type also uses this new associated function idiom that we implemented down here. And in the source code for VEC, it actually looks very similar to what we did down here. And since we're going to be modifying this entries back on every iteration of our loop, we'll go ahead and mark this mutable as well. So now we right now we have an error because the Rust compiler doesn't know what type that this VEC is going to hold. So unlike JavaScript arrays, VECs and Rust can only hold one type. So for example, if you put, if you have a vector of integers, you can't add a string item in there. In order to fix this error for now, what we need to do is tell Rust that we are going to have a VEC of some type. And in this case, it's going to be a VEC of the of our entry type that we define below. And so now in our for loop , instead of printing out each item, we'll want to create a new entry and add it to our entries VEC. So in order to do that, we'll need to figure out one thing first. And that's how to get a string value from the item dot path method that we call that we can call on item here. This returns a type that is not string or turns a path above. Since our entry struct expects a path, that's the that's a type string, we'll need to figure out how to turn this item path into a string. So chained onto this item dot path method, we have this one method, we have these two methods that look a little weird, but do have the that do say something about strings and that's into OS string and into string lossy. We're not sure if that's exactly what we want. We do have also have this display method on path, which sounds a little bit close to what we want to do in the end. But this method also returns something that's not a string, it returns according to the documentation returns an object that implements display for safely printing paths. So this sounds pretty close to what we want, although we don't really know what this what it means that it implements display. But if we just continue down the rabbit hole for a moment, we can see that this whatever this display type is it actually has a two string method that we can chain off of it. So if we go ahead and set that this to a variable for now, we now have a path name that is of type string. And that's exactly what we need . So it might seem a little confusing happening to chain all these methods together, just to simply get a string. The reason for this is that interfacing with the file system is complex. And unlike JavaScript, Rust doesn't try to hide the complexity from the programmer. In this case, we trade complexity for more control. So with a string representation of each path, we can create a new entry in each iteration of our loop and then push that value to our entry's vector that we define above. So we can go ahead and do that by creating another variable here, entry. And we're going to set this to our entry new constructor. And we need to pass it a path. And we can pass it path name, which is now a string. And then finally, we have to add this entry to our vector of entries. So we can do that with entries, and then use dot notation to chain on a call to push. So very similar to JavaScript arrays, the vector type in Rust has a bunch of helper utilities that we can use that function almost identically. And so we can go ahead and pass our entry to the entries vector. And so so now an interesting note, if we actually remove this explicit type annotation, the Rust compiler still knows that this is a vector of entries. And that's because the only thing that we try and put into the vector is something that has the entry type down here. So we can go we can leave that off now, because the Rust compiler is smart enough to figure out what's going on. And so we'll just go ahead and clean up some spacing. And so now that we have all of these instances of entry being pushed into our entries, VEC. Now all that's left to do is loop over that VEC and call our display method on each entry. So we'll go ahead and do another for loop down here. And we can loop over a vector, because it is also something that is iterable. It implements that iterator trait that we talked about earlier. And then for each of these entries, we can call our display method and go ahead and test our code. So we'll go ahead and leave it on the default directory. And there we have it. We have regular strings being passed in and using our display method that we defined below. Go ahead and test it on a different directory just to make sure this argument passing is working as well. Yep. And there we go, a minimal LS clone. So in this lesson, we learned how to implement functionality on our structs with Impul. And we also learned a bit about iterators in Rust. We also learned about the equivalent to JavaScript, raisin, rust, the VEC. So next, we'll do a little cleanup of our mini LS program and extend it to have a little bit more functionality. So we'll change the program to show file size, modify time and permissions. And we'll change our display implementation to use a trait, which is a little bit more idiomatic.