Reading from files and counting words

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.

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
  • [00:00 - 00:11] In the last lesson, we use the standard ENV module to get the arguments passed in by the user from the command line. In this lesson, we need to use that user input to get a target file and then eventually count the words in that file.

    [00:12 - 00:33] So first off, the first thing we need to do is bind the first argument passed in by the user to a variable so we can use it later. So what we want to have happen is something like the user runs our program, so we use cargo run in this instance, and then we want to be able to take a file name argument like words.text.

    [00:34 - 00:50] So in order to bind the first value that comes out of this ENV args function, we first have to figure out what the variable syntax is in Rust. So it's the same thing as in JavaScript, we can use the let keyword.

    [00:51 - 01:02] And so unlike in JavaScript, Rust variables are immutable by default. Rust does have a const keyword, but it's very specific and we won't use it in this situation. And we could just use let if we want to have an immutable by default variable.

    [01:03 - 01:15] Also note that Rust Immutability is much stricter than what const does in JavaScript. So in JavaScript, if you use the const keyword, it guarantees shallow mut ability, but you can still do things like change the inner properties of an immutable object.

    [01:16 - 01:31] If we need to have a mutable variable, we can tell Rust explicitly by adding the mute keyword. So if we have this, we call this variable file name, then if we were to do let mute file name, then this would be a mutable variable binding and we can change it later.

    [01:32 - 01:42] For the time being, we'll leave that off because we don't actually need to change this value once we have it. So the next step is we need to figure out what actually goes here.

    [01:43 - 01:51] So now we need to set file name to something. The ARGS function returns a value that we can iterate over, and that value also has a few helper methods on it that we can use.

    [01:52 - 02:05] So we take a look at the ARGS function. We can see that we can chain other helper methods off of the value returned by that function.

    [02:06 - 02:19] So there are a few here and some of them might look familiar, but the one that we're going to specifically use is the nth function. And so we can take a look here, returns the nth element of the iterator, pretty straightforward.

    [02:20 - 02:36] Whatever we pass in here is what we'll get back as far as the array of iter ators that we saw in the last lesson. So this dot syntax for chaining methods together is the exact same as what we 're used to in JavaScript.

    [02:37 - 02:48] And so we're going to need to get the first argument here. And remember that the actual index zero element, so the first element in that array-like thing that we saw earlier, was actually the name of the program.

    [02:49 - 02:57] So we need to get whatever the value is at index one, and that will be the first argument passed in by the user. So something like that.

    [02:58 - 03:03] So we'll go ahead and add that here. And let's see what we get back.

    [03:04 - 03:16] Remove this. And we'll just print out the file name for now.

    [03:17 - 03:25] So let's quickly print this out and see what we get. So we can see we have an error, but let's see what cargo rungives us.

    [03:26 - 03:37] We'll pass in words.text as our first argument. OK, so we see the rest compiler is telling us that option string cannot be formatted with the default form matter.

    [03:38 - 03:45] So this is a little strange because we don't -- we passed in a string. At least it seems like we passed in a string.

    [03:46 - 03:57] So why is it that we're getting back this weird option thing with a string inside of it? And if you're familiar with TypeScript, then this syntax should look pretty familiar.

    [03:58 - 04:08] And if not, this is just saying that the value that we got back, the value that file name is, is an option of string. So an option with a string inside of it.

    [04:09 - 04:18] So why is that the case? So in order to understand why we're getting back an option string instead of just a string, if to understand that in Rust, you don't have null or undefined values.

    [04:19 - 04:31] At least not null or undefined globally. So in JavaScript, if we think about how this function would -- this nth function would be implemented, then it might make a little bit more sense.

    [04:32 - 04:44] So in JavaScript, if we had something and then we called nth on it, we had some type of array or iterator or something, and we called nth -- we would get back one of two values. If the nth thing existed, we'd get it back.

    [04:45 - 05:00] And if it didn't, in JavaScript, we'd probably get back null or undefined. So since we don't have null or undefined in Rust, we have to -- nth has to return a value that can handle both the case where there is nothing and where there is something.

    [05:01 - 05:12] And that's -- in this case, that's the option type. So option can either have a value of none or it can have a value of some and then the value inside.

    [05:13 - 05:32] So in this case, since we passed a valid argument to our program via the command line, we should have a some value with the inner value being that string that we passed in words.text. So in order to get at that inner value, we have to unwrap the result layer away from it.

    [05:33 - 05:46] And so we can do that with the unwrapped method. And so now we should be able to see our words.text argument be printed to standard out instead of this error with option string.

    [05:47 - 06:01] And so we run that there and we see we do in fact get our words.text argument that comes through. So the next logical question is, what happens if we unwrap an option that doesn 't contain a value?

    [06:02 - 06:10] So it contains a none value. And the answer to that question is the program will panic and so it will crash.

    [06:11 - 06:28] And so this probably isn't the most user-friendly experience and it's -- we'll actually come back to this at the end and clean up our unwraps. But for now, just to get things to work, we'll leave it like this and actually show that it does in fact panic if you don't run it with any arguments.

    [06:29 - 06:36] And so there you go, thread main panicked at a call to option unwrap. Can't unwrap on a none value.

    [06:37 - 06:55] So in the correct case when the user passes in an argument, then the program seems to be working correctly. We have file name as being bound to the value that comes through via the ARGS method.

    [06:56 - 07:03] So now let's use that value to actually open a file. So to access the file system, we can again turn to standard -- the standard library.

    [07:04 - 07:10] And this time to the standard FS module. So standard FS is Rust's equivalent to nodes FS.

    [07:11 - 07:19] And nodes FS has a read file function. And similarly, standard FS in Rust has a read to string function.

    [07:20 - 07:25] So we'll just go ahead and do the same thing here. We'll use standard FS just to keep things consistent.

    [07:26 - 07:38] And we can see here that we have the FS module in scope. And on the FS module, we have that read to string function.

    [07:39 - 07:43] So let's go ahead and create another variable. And we'll call this one file content.

    [07:44 - 08:04] So -- And then we need to pass in a path. And since we've bound the file name to whatever the user inputs as that first argument, we should just be able to pass file name in as our path.

    [08:05 - 08:16] As another sanity check, let's go ahead and print out whatever comes out of file contents. And we can see here that we do have an error, but let's go ahead and let the compiler tell us what's going on.

    [08:17 - 08:25] So let's go ahead and run, cargo run with our words.text argument. All right.

    [08:26 - 08:33] And so it looks similar as to before. That file contents value cannot be formatted with the default formatter.

    [08:34 - 08:44] So we know that it's not the string that we're expecting it to, the string of whatever the contents are. And so this time we have a slightly different type.

    [08:45 - 08:51] Looks like we have a -- some type of standard result type. And inside of that is a string and some other stuff.

    [08:52 - 09:04] So this is kind of weird looking, but it does look somewhat similar to what we saw before with an option and a string inside. The result type in Rust is very similar to the option type.

    [09:05 - 09:20] Whereas the option type lets you define something that might not have a value, a type that might not have a value, and alternatively might have a value. The result type in Rust is used whenever the result from an operation could fail.

    [09:21 - 09:38] So in this case, reading file contents or reading files from the file system is an operation that could fail for a number of different reasons outside of the control of the Rust programmer. This file, this read to string method actually doesn't return just the contents .

    [09:39 - 09:50] It returns a result. And in the success case, that result will be a string.

    [09:51 - 10:04] But in the failure case, that result will be something else. And if we go ahead and take a look back at our error here, it looks like it's some type of error is going to be returned in the error case.

    [10:05 - 10:15] So what can we do about this situation? Well, Rust uses option and result in very similar ways, and it actually let you unwrap on both of those types.

    [10:16 - 10:23] So we can just go ahead and unwrap that value as well for now. So now we should see printed out and we see that we don't have an error highlight here.

    [10:24 - 10:31] What we should see print out now is the contents of the words.text file. So we don't have words.text file yet.

    [10:32 - 10:40] So let's go ahead and just add that now. All right.

    [10:41 - 10:55] And so now when we run this program on that targeting that words.text file, we should see whatever the file contents are. So the string representation of that file print to standard out.

    [10:56 - 11:01] So we'll go ahead and run that targeting words.text. And there we go.

    [11:02 - 11:10] We get the contents of that file print to standard out. So far so good.

    [11:11 - 11:18] So at this point, you might be thinking in a lot of ways, Rust feels like a high level language. We have a lot of the nice to have that language like JavaScript has.

    [11:19 - 11:25] We're able to chain different helper methods together. And it seems like a lot of the types have a lot of built in functionality.

    [11:26 - 11:43] So this all might feel somewhat familiar being able to chain different helper methods on to each other. Continuing that theme, we can use some of the helper methods built into the string type to actually go ahead and get into the meat of our program, which is counting words and then displaying that count to the user.

    [11:44 - 11:51] So what we have here is a file contents is represented as a string. So instead of printing that out, so we can go ahead and create a new variable.

    [11:52 - 12:05] We'll call this one number of words. And now what we want to do is we want to somehow count the individual words defined as a string of characters separated by a white space.

    [12:06 - 12:22] And we want to split those up and then count the resulting individual items. And so at this point, if I didn't know any Rust, I would probably go and take a look at some Rust documentation.

    [12:23 - 12:28] Maybe go look at the string type. So we'll go do this now.

    [12:29 - 12:38] And see what kind of helper methods we actually have available to us. And so we can just do a quick search and a research for white space.

    [12:39 - 12:49] And we can see that there are some, it seems like there are some white space helpers, so split ASCII white space, that might be useful. And then we just have a split white space helper here.

    [12:50 - 12:56] Split strings by white space, perfect, that's exactly what we'll need. And then, so let's go ahead and try that to get.

    [12:57 - 13:11] So file contents and then let's split white space. We're going to need to now figure out how we can count whatever the result of this white space split is.

    [13:12 - 13:29] So, or assuming at this point that we have something that contains a bunch of words that are separated by white space. And it looks like we do have a bunch more helper methods that we can chain off of this split white space call.

    [13:30 - 13:35] So let's just go ahead and go with our gut here. Call this count method.

    [13:36 - 13:46] And so we'll take a quick look at the documentation, consumes the iterator, counting the number of iterations and returning it. Give it a shot.

    [13:47 - 14:07] So go ahead and print out this result again. All right, and we'll pass it our same words.text argument.

    [14:08 - 14:16] And, well, it looks like we got what we're looking for out of the program. So a program works as expected here.

    [14:17 - 14:29] And the last thing that we have to do is go back and clean up these unwrapped calls. And the reason that I say cleaning up is because typically it's good practice and rest to only use unwraps while prototyping new code.

    [14:30 - 14:47] Or in some very specific situations where you're sure a user won't be able to cause a panic if that unwrap is called on a non-value or an error or something like that. So one of the idiomatic ways to handle result type and also an option type is to use a match expression.

    [14:48 - 15:01] So this is similar to a switch statement in JavaScript except it's generally used a lot more in Rust and it is also a bit more powerful. So, for example, a match can be used as an expression, meaning we can directly bind its output to a variable.

    [15:02 - 15:19] And so we can take a look at what that means right here. So we're going to end up replacing this binding here. Instead of unwrapping on it, we'll take this off and we'll match on it instead.

    [15:20 - 15:30] And so, match has a block body like this. Another main benefit of match is that it requires us to account for all of the possible types the supplied value can be.

    [15:31 - 15:43] So in this case, we're supplying match with the output of this nth method, which we remember from before is an option type. And so we also remember that option can be one of two values. It can be something or it can be none.

    [15:44 - 15:54] And so we can see that here and actually the auto complete just works. And so the value, this is going to be our, we'll call it input.

    [15:55 - 16:08] And then so match syntax uses the fat arrow. And so in the case that we have some input from the user from that nth argument on args, then we're just going to want to return that input.

    [16:09 - 16:21] Basically, we're doing the same thing when we're digging in, we're unwrapping that outer option value and we're grabbing the input value that's inside. We can see here that we have another argument or another error here.

    [16:22 - 16:35] And if we scroll down and look at the error checking here available in VS code, then we can see that the, it seems like the error is non-exhausted patterns, none not covered. And so that's what we were mentioning before.

    [16:36 - 16:49] Match forces you to account for every possible type that this value could output. And so we've covered the sum case. We also now need to cover the none case.

    [16:50 - 17:01] Before if we unwrapped on a user input that wasn't there, our program would just panic. And so I think we want that to also happen. It doesn't make sense to run our program without an argument.

    [17:02 - 17:18] But instead of returning just some kind of random error to the user, let's go ahead and actually return something that they can use to fix whatever the mistake is. And supply a valid input. So we're still going to cause the program to panic.

    [17:19 - 17:24] And so we can do that explicitly with this panic macro. And so this is one that you'll see pretty often as well.

    [17:25 - 17:35] And it just takes a string slice here as argument. And so let's go ahead and say a file name is required.

    [17:36 - 17:40] So that's a little bit nicer for the user. And it makes it a little bit clearer what needs to happen here.

    [17:41 - 17:50] Doesn't seem like we have any errors. So let's go ahead and run our program again. Just standard check, make sure it works. And yep, there we go. We get our correct output.

    [17:51 - 17:58] So all that's left is we need to do the same thing on this file contents, which returns a result. It doesn't return an option, but they function very similar ways.

    [17:59 - 18:15] And we can use the same pattern of matching on the result value and handle each case independently. So let's go ahead and do that. So we'll match on this FS read to string.

    [18:16 - 18:31] And so in this case, instead of some or none, we're dealing with two other values, similar values, but a little bit different. So the first one is OK. So that means that the operation happens successfully, basically.

    [18:32 - 18:48] And we can match on that same way. So in the OK case, and we'll also have a value associated with the OK case here. So we'll say contents. And so in the OK case, we just want to do the same thing and return contents.

    [18:49 - 19:05] So the other case is the error case. And in this situation, we probably do want to convey to the user whatever the error is. And since this isn't happening in our program, if we were to have an error with read to string, it would be something probably at the operating system level.

    [19:06 - 19:22] And so we'll just want to pass through this error type or this error value to the user and tell them whatever is going on with that. So we'll just do a panic here and we'll pass that error through as a panic.

    [19:23 - 19:41] So this is a little unnecessary. We could have just left the unwrap in this case, but it's good to show that this match works very similarly in both the option for the option type and the result type. So we'll leave it like this for now. And then go ahead and double check the program runs.

    [19:42 - 19:52] And there we go. Get the correct output. So in this lesson, we got our mini work count program functioning as intended. And we learned some new rest concepts like match and result and option.

    [19:53 - 19:56] In the next lesson, we'll jump right into our second command line program.