Lately I’ve been playing around with Rust, and there are some fantastic things to love about it: it’s fast, forces you to think through safety risks up front, and provides some of the best features of functional languages (pattern matching! Closures! Expression-oriented syntax!). Since the best way to learn a language is to use it, I decided to write a simple program to summarize what CPU is in my system via reading /proc/cpuinfo
.
Features
- Read
/proc/cpuinfo
and parse its format - Summarize the following fields:
- Vendor
- Model Name
- # of physical cores
- # of virtual cores (“threads”)
- MHz
- Output the results in a nice, compact format.
- Use idiomatic Rust and the Rust Standard Library
- Use the daily builds of Rust – track the compiler
try! (and facepalm)
Of course, I picked a time when the language is settling down but not 100% stable to jump in to learning it, so the first thing that happened is I let myself get confused by writing the following code in main:
let f = try!(File::open("/proc/cpuinfo"));
…which didn’t compile. It said:
<std macros>:5:8: 6:51 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:: error:: FromError:: from_error ( err ) ) } } )
<std macros>:1:1: 6:57 note: in expansion of try!
src/main.rs:14:13: 14:47 note: expansion site
error: aborting due to previous error
I thought this maybe had to do with recent changes in the standard library (which, in hindsight, was silly – of course Rustaceans wouldn’t miss something this simple!), so I spent some time bisecting what the expectations were of code in std::old_io
vs std::io
, but they were so very similar that it just didn’t make sense.
Thankfully, I found Rust by Example, which gives runnable examples of all the major features of Rust. Fantastic stuff! There, I saw that using match {}
was the way to manage possible failures when opening a file:
let mut cpuinfo = match File::open("/proc/cpuinfo") {
Ok(f) => f,
Err(e) => panic!("{}", e)
};
Fair enough, the return type is ioResult<T>
(which is really a typedef for Result<T, ioError>
), so you have to destructure it. But it was bugging me that you’d have a try!
macro that supposedly does that destructuring yet doesn’t compile. And how do I interpret that compiler error?
I finally came back to reading the documentation on try!
here. Aha! See, my C++/Python/Java roots bit me; I assumed that try! was similar to a try..catch
expression, but no – it’s really intended to make handling a series of operations more elegant – you still ultimately use a match
to destructure the Result<>
. With try!
, you can group together multiple operations that could return an error into a function that returns the Result
of the entire group; if any of the steps returns Err
, you early-return and end up in the error branch.
Okay, so what did that compiler error tell me? Well, it told me what it knew: that main
had no return value defined (thus, ()
), yet my use of try!
was giving a Result<>
. Expression-oriented syntax: the value of the last expression in the function is the return value of the function. So boom: I was being dumb.
So – I learned that Rust has no exceptions, but with a way to group a series of operations and handle failures gracefully. I dig. Certainly avoids the pitfalls of the entire program being a gigantic try..catch
with a single, generic exception handler!
I also learned that the compiler is way smarter than I am.
I also re-learned what Mr. Eugene Lewis Fordsworthe said: Assumption is the mother of all mistakes.
Next time, I’ll actually do something with this file I’ve managed to properly open.