A Simple Web App in Rust, Part 2a
Table of Contents
1 Context
If you haven't checked out part 1 of this series, I would start there.
In the first part, we successfully set up the Rust project and built a simple "hello world" web app.
Originally, in this part, I wanted to write a program that writes dates to the filesystem. However, I ended up fighting with the type checker so much that this post ended up mostly being about that.
2 Starting Out
The last time wasn't too bad. When I did some of this earlier, I remember this being the hardest part.
Let's start by moving the existing main.rs
out of the way so that we
can work with a fresh file.
$ pwd /Users/joel/Projects/simple-log $ cd src/ $ ls main.rs $ mv main.rs web_main.rs $ touch main.rs
3 Remembering "hello world"
Can I write a hello world without needing to look?
Let me try:
fn main() { println!("Hello, world"); }
Then:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) Running `target/debug/simple-log` Hello, world
So, I guess I remember it OK. I was a little unsure about needing to
import something for println!
, but it must be unnecessary.
4 A Naive Approach
Ok, moving on. Searching the Internet for "rust create a file" leads
me to this page on std::fs::File
:
https://doc.rust-lang.org/std/fs/struct.File.html. Let's try a piece
from one example:
use std::fs::File; fn main() { let mut f = try!(File::create("foo.txt")); }
Building:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) <std macros>:5:8: 6:42 error: mismatched types: expected `()`, found `core::result::Result<_, _>` (expected (), found enum `core::result::Result`) [E0308] <std macros>:5 return $ crate:: result:: Result:: Err ( <std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } ) <std macros>:1:1: 6:48 note: in expansion of try! src/main.rs:5:17: 5:46 note: expansion site error: aborting due to previous error Could not compile `simple-log`.
When I wrote the first version of this, this error took a really longtime to figure out. I don't get on IRC very often anymore, so figuring things out like this can be pretty rough. Figuring it out left a big impression on me, so I know the answer right away.
The problem with the above code is that try!
expands to something
that returns early with an Err
type in case of an error. Since
main
returns Unit ("()
")1, this causes a type error.
I think three things make this complicated:
- At this point, I'm not really sure how to read the error
message. What does 'expected' and 'found' refer to? Since I know
the answer, I can see that 'expected' refers to the return value of
main
, but I could easily see 'expected'/'found' going either way. - For me, reading the documentation for
try!
does not immediately indicate to me howtry!
impacts the return value of the function it is called from. Of course, I should have noticed thereturn
in the macro definition. At any rate, I didn't figure the problem out until I found a remark in the Rust book about howtry!
can't be called frommain
because of this exact problem. - The error actually occurs inside a macro. It didn't hit me at the time, but the rust compiler can output the code after macros have been expanded. That makes this kind of thing much easier to debug.
In number 3, expanding macros is alluded to. Viewing expanded macro is such a useful way to debug these kinds of issues that its worth discussing in more depth.
5 Debugging by Expanding Macros
First off, I figured this out by searching for "rust expand macros". Given this code:
use std::fs::File; fn main() { let mut f = try!(File::create("foo.txt")); }
… we can run the compiler to show us the expanded macro output this way:
$ rustc src/main.rs --pretty=expanded -Z unstable-options #![feature(no_std)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; use std::fs::File; fn main() { let mut f = match File::create("foo.txt") { ::std::result::Result::Ok(val) => val, ::std::result::Result::Err(err) => { return ::std::result::Result::Err(::std::convert::From::from(err)) } }; }
This is way easier to debug. Macros are a very powerful tool, but like any tool you need to know when and how to use them.
So, see that return
statement in the above output? That's the
problem. Its trying to return an Err
result from main
, which again
has the return type Unit.
6 Fighting with Types
I need to know how to resolve this type problem. I'm going to start by imitating the
try!
macro, but this time only returning Unit:
use std::fs::File; fn main() { match File::create("foo.txt") { Ok(val) => val, Err(err) => () } }
Running:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:5:5: 8:6 error: match arms have incompatible types: expected `std::fs::File`, found `()` (expected struct `std::fs::File`, found ()) [E0308] src/main.rs:5 match File::create("foo.txt") { src/main.rs:6 Ok(val) => val, src/main.rs:7 Err(err) => () src/main.rs:8 } src/main.rs:7:21: 7:23 note: match arm with an incompatible type src/main.rs:7 Err(err) => () ^~ error: aborting due to previous error Could not compile `simple-log`.
Huh. So, I'm not really sure how to say "don't do anything, here". I
guess the type 'val' must be 'std::fs::File', and so its assuming that any
match return value must be that. Can I make the Ok
branch not return
anything, either?
use std::fs::File; fn main() { match File::create("foo.txt") { Ok(val) => (), Err(err) => () } }
Running:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default src/main.rs:6 Ok(val) => (), ^~~ src/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default src/main.rs:7 Err(err) => () ^~~ Running `target/debug/simple-log` $ ls Cargo.lock Cargo.toml foo.txt src target
It created foo.txt
! Of course, the code could be cleaner, but thats
fine for now. Let me try something else:
use std::fs::File; fn main() { File::create("foo.txt") }
=>
$ rm foo.txt $ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:5:5: 5:28 error: mismatched types: expected `()`, found `core::result::Result<std::fs::File, std::io::error::Error>` (expected (), found enum `core::result::Result`) [E0308] src/main.rs:5 File::create("foo.txt") ^~~~~~~~~~~~~~~~~~~~~~~ error: aborting due to previous error Could not compile `simple-log`. To learn more, run the command again with --verbose.
I've seen this before. This must mean main
is returning the result of
File::create
. I was thinking it would return nothing, but I guess I
didn't really think that through. What if I add a semicolon?
use std::fs::File; fn main() { File::create("foo.txt"); }
=>
$ rm foo.txt $ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:5:5: 5:29 warning: unused result which must be used, #[warn(unused_must_use)] on by def ault src/main.rs:5 File::create("foo.txt"); ^~~~~~~~~~~~~~~~~~~~~~~~ Running `target/debug/simple-log` $ ls Cargo.lock Cargo.toml foo.txt src target
So, we now get an "unused result" warning, although it still runs and creates the file. Let's go back and try doing something that handles the results:
use std::fs::File; fn main() { match File::create("foo.txt") { Ok(val) => println!("File created!"), Err(err) => println!("Error: could not create file.") } }
=>
$ rm foo.txt $ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default src/main.rs:6 Ok(val) => println!("File created!"), ^~~ src/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default src/main.rs:7 Err(err) => println!("Error: could not create file.") ^~~ Running `target/debug/simple-log` File created!
Now there are unused variables. My hunch is that either ellipses or removing the variable name will fix this:
use std::fs::File; fn main() { match File::create("foo.txt") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) Running `target/debug/simple-log` File created!
So, ellipses worked. What happens when I instead remove the ellipses?
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:12: 6:13 error: nullary enum variants are written with no trailing `( )` src/main.rs:6 Ok() => println!("File created!"), ^ src/main.rs:7:13: 7:14 error: nullary enum variants are written with no trailing `( )` src/main.rs:7 Err() => println!("Error: could not create file.") ^ error: aborting due to 2 previous errors Could not compile `simple-log`.
It didn't like that. I'm guessing that "nullary" means "zero-arity", and it needs those removed. If I remove the parentheses totally:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:9: 6:11 error: this pattern has 0 fields, but the corresponding variant has 1 field [ E0023] src/main.rs:6 Ok => println!("File created!"), ^~ src/main.rs:7:9: 7:12 error: this pattern has 0 fields, but the corresponding variant has 1 field [ E0023] src/main.rs:7 Err => println!("Error: could not create file.") ^~~ error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
This makes sense, and is basically what I expected. My mental model is starting to form!
7 Writing to a file
Let's try something a little harder. How about this:
- Try to create the log file. If it exists, great; if not, boo.
- Try to write a string to the log file.
- Clean everything up.
This first example doesn't even attempt half of that, but we'll go with it:
use std::fs::File; fn log_something(filename, string) { let mut f = try!(File::create(filename)); try!(f.write_all(string)); } fn main() { match log_something("log.txt", "ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:3:26: 3:27 error: expected one of `:` or `@`, found `,` src/main.rs:3 fn log_something(filename, string) { ^ Could not compile `simple-log`. To learn more, run the command again with --verbose. $
So I guess function arguments need must have type annotations:
use std::fs::File; fn log_something(filename: &'static str, string: &'static str) { let mut f = try!(File::create(filename)); try!(f.write_all(string)); } fn main() { match log_something("log.txt", "ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) <std macros>:5:8: 6:42 error: mismatched types: expected `()`, found `core::result::Result<_, _>` (expected (), found enum `core::result::Result`) [E0308] <std macros>:5 return $ crate:: result:: Result:: Err ( <std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } ) <std macros>:1:1: 6:48 note: in expansion of try! src/main.rs:4:17: 4:45 note: expansion site src/main.rs:5:12: 5:29 error: type `std::fs::File` does not implement any method in scope named `wr ite_all` src/main.rs:5 try!(f.write_all(string)); ^~~~~~~~~~~~~~~~~ <std macros>:1:1: 6:48 note: in expansion of try! src/main.rs:5:5: 5:31 note: expansion site src/main.rs:5:12: 5:29 help: methods from traits can only be called if the trait is in scope; the f ollowing trait is implemented but not in scope, perhaps add a `use` for it: src/main.rs:5:12: 5:29 help: candidate #1: use `std::io::Write` <std macros>:5:8: 6:42 error: mismatched types: expected `()`, found `core::result::Result<_, _>` (expected (), found enum `core::result::Result`) [E0308] <std macros>:5 return $ crate:: result:: Result:: Err ( <std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } ) <std macros>:1:1: 6:48 note: in expansion of try! src/main.rs:5:5: 5:31 note: expansion site src/main.rs:10:9: 10:15 error: mismatched types: expected `()`, found `core::result::Result<_, _>` (expected (), found enum `core::result::Result`) [E0308] src/main.rs:10 Ok(..) => println!("File created!"), ^~~~~~ src/main.rs:11:9: 11:16 error: mismatched types: expected `()`, found `core::result::Result<_, _>` (expected (), found enum `core::result::Result`) [E0308] src/main.rs:11 Err(..) => println!("Error: could not create file.") ^~~~~~~ error: aborting due to 5 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
That's a lot of errors. Looking at the first error, I'm guessing
that log_something
needs to have a return value specified. I've
tried a few things, but right now I'm stuck. To the search engines!
A few minutes have passed, and I finally have an answer. I did some searching on GitHub, but it wasn't fruitful. I tried about 50 different things, but got this to work:
use std::io::prelude::*; use std::fs::File; fn log_something(filename: &'static str, string: &'static str) -> Result<File,std::io::error::Error> { let mut f = try!(File::create(filename)); try!(f.write_all(string)); } fn main() { match log_something("log.txt", "ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
I'm not really sure why it works. If I understand correctly, the
return value is of Result
type that's parameterized with the types
File
and std::io::error::Error
. What does this mean, exactly? It
seems strange to me that of the two types, one type is the
actual result (a file), yet the second is an Error
type. Why? I'm
thinking that once I fix the remaining error(s), this will need fixing
again.
So, now when I try to run it, I get:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:8:22: 8:28 error: mismatched types: expected `&[u8]`, found `&'static str` (expected slice, found str) [E0308] src/main.rs:8 try!(f.write_all(string)); ^~~~~~ <std macros>:1:1: 6:48 note: in expansion of try! src/main.rs:8:5: 8:31 note: expansion site error: aborting due to previous error Could not compile `simple-log`. To learn more, run the command again with --verbose.
Ok, so I saw in the example that they prefixed the string with a b
,
which I neglected to do just to see what would happen. Fixing the
parameters:
use std::io::prelude::*; use std::fs::File; fn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File,std::io::error::Error> { let mut f = try!(File::create(filename)); try!(f.write_all(string)); } fn main() { match log_something("log.txt", "ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:4:85: 4:106 error: struct `Error` is private src/main.rs:4 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File, std::io::error::Error> { ^~~~~~~~~~~~~~~~~~~~~ error: aborting due to previous error Could not compile `simple-log`.
Ugh – I knew this was going to be a problem. Time to do some searching and reading.
The Rust book has a section on Result
. Hmm. It seems like what I'm
doing may not be idiomatic? I'd say that it seems like the "best" way
to handle what is going on, but I am confused. I've seen this
unwrap
thing a few times, and it seems like it could be what I
want. If I try unwrap, things might be different:
fn log_something(filename: &'static str, string: &'static [u8; 12]) { let mut f = File::create(filename).unwrap(); f.write_all(string); } fn main() { log_something("log.txt", b"ITS ALIVE!!!") }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:5: 6:25 warning: unused result which must be used, #[warn(unused_must_use)] on by def ault src/main.rs:6 f.write_all(string); ^~~~~~~~~~~~~~~~~~~~ Running `target/debug/simple-log` $ ls Cargo.lock Cargo.toml foo.txt log.txt src target $ cat log.txt ITS ALIVE!!!
So, that worked, although there is a warning. I think this is not "the Rust way", since its failing early/throwing errors away.
The real problem with try!
and returning a Result
is that there's
this weirdness dealing with this line in the try!
macro:
return $crate::result::Result::Err($crate::convert::From::from(err))
This means that whatever I pass in has to have a From::from
trait
implemented on an enum, but I really have no idea how traits or enums
work, and I think the whole thing is overkill anyway for what I'm
trying to do.
I've gone to the documentation for Result
, and it looks like I may
be going in the wrong direction:
https://doc.rust-lang.org/std/result/. This io::Result
example seems
to be similar enough to what I'm doing, so let me see if I can fix
that up:
use std::io::prelude::*; use std::fs::File; use std::io; fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> { let mut f = try!(File::create(filename)); try!(f.write_all(string)); } fn main() { match log_something("log.txt", b"ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:5:1: 8:2 error: not all control paths return a value [E0269] src/main.rs:5 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> { src/main.rs:6 let mut f = try!(File::create(filename)); src/main.rs:7 try!(f.write_all(string)); src/main.rs:8 } error: aborting due to previous error Could not compile `simple-log`. To learn more, run the command again with --verbose.
After some time thinking, I see the problem:
an Ok(())
statement must be added as the final statement in
log_something
. I realized this because I saw that this is how things
happen in the Result
documentation.
I've been used to the idea that not having something after the final
semicolon means return ()
; however, the message "not all control
paths return a value" doesn't make sense – to me, this is a type
mismatch. Unless, of course, ()
is not a value, which it might not
be, but I still think that's confusing.
Our final result (for this post):
use std::io::prelude::*; use std::fs::File; use std::io; fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> { let mut f = try!(File::create(filename)); try!(f.write_all(string)); Ok(()) } fn main() { match log_something("log.txt", b"ITS ALIVE!!!") { Ok(..) => println!("File created!"), Err(..) => println!("Error: could not create file.") } }
=>
$ rm log.txt $ cargo run Running `target/debug/simple-log` File created! $ cat log.txt ITS ALIVE!!!
Ok, it works. Great. I'm going to end here because this has been pretty challenging. I'm sure improvements could be made on this code, but this is a good stopping point and a good time to research dates and times in Rust, which will be the the next post.
8 Updates
Footnotes:
lol.