Skip to content

Instantly share code, notes, and snippets.

@Skrylar
Last active March 29, 2021 12:54
Show Gist options
  • Save Skrylar/8142997 to your computer and use it in GitHub Desktop.
Save Skrylar/8142997 to your computer and use it in GitHub Desktop.
Tips for Rust

Tips for Rust: Result Types

Expecting certain outcomes from a Result

Why is it that there is an Option.expect(M), but no Result.expect(M)?

I opened a pull request [pr] which put this in, and it turns out the reason there is no expect function is because you are intended to use composition:

This method is unnecessary with the option adapters - res.ok().expect() does exactly what this method would do.
— Kimundi

So you write the following Rust to expect success:

let my_value = this_might_fail().ok().expect("It failed :(");

And this to expect failure:

let my_value = this_might_succeed().err().expect("It succeeded :(");

Using a result as its own error

If Result contains an Err(M), why is there no method which ensures you have an Ok(T) and fails otherwise?

Imagine I have some code which calls out to an external library, and returns a handle on success or an error string on failure. Now I want to retrieve this handle, and fail out if we were not able to get it. Upon failure, I want the reason for failure to be known so we can see what went wrong. Here are our requirements:

  • A simple call that performs the necessary matching, unboxing and failure.

  • A useful error message on failure.

Here is some Rust code that embodies that use case:

let main_window = sdl::Window::create( ... ).unwrap(); // (1)
main_window.be_awesome();
  1. This will fail if the window could not be created, however we will get a generic error message. Even though create gives us a specific error message as to 'why' creation failed, this isn’t reported at task failure.

unwrap [std] checks that the result is Ok and errors otherwise; this is what I want, but it throws a generic error message instead of the value of Err. This has the syntax we want, but doesn’t fulfill the second requirement.

According to the pull request [pr] from earlier, this functionality actually does belong in the unwrap function:

The expect method has previously not existed on the Result type because you have an obvious error message (the Err value). In the past the error value had the ToStr bound so a good message could be presented as part of unwrap, but that seems to have gone away.
— Alex Crichton

Fortunately, Rust is an awesome language and we can implement our own way of doing this 'without altering the standard library.'

Writing our custom expect using traits

First we need to define the trait, which specifies our own version of unwrap. Since there are plans to change unwrap to get in the future, we will modify the future API in our example.

trait GetWithFailMessaged<T> {
	fn getM(self) -> T;
}

We have made a 'generic trait', which means this definition can be applied to 'anything which returns an object of a specific type to be determined later.' Next, we need to make an 'implementation' of our trait for the Result types:

impl<T,E:Any+Send> GetWithFailMessaged<T> for Result<T,E> {
	fn getM(self) -> T {
		match self {
			Ok(a) => a,
			Err(b) => fail!(b)
		}
	}
}

What we have done is specify the 'implementation', which applies to any Result as long as the error object is Send-able. Since we are interested in strings as errors, and strings are Send-able, then any result which takes a string in the Err field is applicable. Now we can simply use getM:

let x = this_might_fail().getM();

Which performs the same action as unwrap [std], but throws the error stored within the Result instead of a generic one.

trait GetWithFailMessaged<T> {
fn getM(self) -> T;
}
impl<T,E:Any+Send> GetWithFailMessaged<T> for Result<T,E> {
fn getM(self) -> T {
match self {
Ok(a) => a,
Err(b) => fail!(b)
}
}
}
fn this_might_fail() -> Result<int, ~str> {
Err(~"Oh no.")
}
fn main() {
let _x = this_might_fail().getM();
}

Tips for Rust: Strings

This episode of 'Tips for Rust' covers strings and the basic things you would want to do with them.

Things to Know

Strings are str

Hey, what is Rust’s string type? I tried string and String but the compiler is sad :(

Rust’s string type, such as it is, has the name str.

str isn’t really a type

I tried typing in str but I continue to be surrounded in failure.

That is because in Rust, str isn’t 'really' a type. You have to use ~str, which indicates an 'owned box' to contain the string. Since a str can contain zero or more characters, you have to 'box it up' and store it on the heap.

Note
In the past, @str was allowed for a string which would exist until picked up by the 'garbage collector'. As of 0.9-pre, managed boxes are currently in flux and have nothing to do with garbage collection anymore.

Strings are vectors

A Rust string is just a 'vector' or 'buffer' of bytes, with some special behavior to ensure that only valid Unicode enters into them. This means that a ~str is essentially just ~[u8] with some duct tape over it. As vectors in Rust carry their bounds around with them, there is no need for an ANSI NUL character at the end of each buffer; they act more like Pascal strings in that the length is attached to the string explicitly.

Warning
There is no NULL (0) character at the end of a Rust str. This is important to consider when you start interacting with C code later on.

Finagling

Turning a String in to Bytes

I need to write a some text to a file, but those APIs are based on reading and writing bytes! How do I write out my ~str variables?

One option is to use the bytes! macro, if the string you are writing is known at compile time:

amazing_write_function(bytes!("Success."));

This has the advantage of being done by the compiler, our friend rustc, at compile time. Whatever string you pass in will be inserted directly as though it had always been [u8] all along. Unfortunately this only works when you already know what the string will be; if the string is provided by 'other code', you’ll need something different.

Zero-copy Sending

I have prepared this string which will be sent over the network to another user, and then I will discard the local copy as it will no longer be needed.

When you own a string (you have the ~str, not a &str), you have the option of 'consuming' the string (using .into_bytes()) and obtaining the 'raw bytes' of it instead. This takes the boxed string and removes all of the special handling, returning the underlying bytes as a replacement. You can then hand these bytes over to be transmitted across the network, and summarily discarded once transmission is complete.

To summarize:

  • You must own the string. This means you have a ~str type, not a borrowed string (&str.)

  • Converting the string to bytes 'without making a copy' will 'consume' the original ~str value.

  • You may then treat the remaining ~[u8] as though it was any piece of raw data.

You can read more about into_bytes() in the standard library documentation.

Tips for Rust: Structures

Preventing unit structures from being created wildly

Here’s an interesting use case:

  • You are dealing with a C library which handles terminal I/O.

  • You want to gate all terminal I/O through an owned object, which ensures a strict control flow for who and what may read and write to the terminal.

Well, 'unit structs' are pretty good for this. You can make a struct which has no fields, but is still public:

struct Terminal;

The good part about this is that they are the size of a pointer, and you can implement Drop for them. With a proper Drop implementation you can ensure that the terminal library is closed [1] whenever the control structure is no longer required.

I ran in to this exact scenario while writing a binding for TermBox. I noticed there were already bindings, but I wanted to write some that behaved in a very Rust-like way. So access to the terminal is shielded as we just discussed.

Using 'mut' we can ensure that only mutable handles can actually write to the screen, while still allowing a few querying functions if we want to do that for some reason. There’s only one problem with this whole idea [irc]:

skrylar: an unit struct as in "struct FooBar;"? - that’s not a good idea, because anyone can construct it [..] if you can see the type, you can construct it
— eddyb

Fortunately, there is an elegant solution [irc]:

struct FooBar { priv _dummy: () }?
— Yurume

If a structure contains 'private fields', you cannot actually create it from outside the module which defines it. This is because creating a struct requires placing values in every field, and you cannot access fields which are hidden. By placing a unit object within FooBar, there is no actual data added to the structure but it must still be specified to create a new FooBar. Effectively, this means that now a user must go through our library to retrieve the shielded object.

And our library can make whatever internal checks are necessary to ensure new fails in the event a terminal has already been allocated (without being subsequently Drop-ped.)

Testing it out

Lets test this out with some source files:

library.rs
pub struct MyType {
	priv _shield: ()
}
user.rs
mod library;
fn main() {
	let x = library::MyType
}

Now lets compile it; oh, we can’t:

user.rs:4:9: 4:13 error: `library::MyType` is a structure name, but this expression uses it like a function name

Rust requires the fields to be specified, so lets try doing that:

user.rs (take 2)
mod library;
fn main() {
	let x = library::MyType { _shield: () }
}

And another compile:

user.rs:4:9: 4:13 error: field `_shield` is private

Success. Instances of MyType are only going to be created when we want them to. Which means in the use case (a terminal library), multiple locations of the program cannot grab access to the terminal. Screen I/O will happen only as we expect it to, with the full weight of Rust’s memory and type safety behind it.

  • [irc] #rust at irc.mozilla.org


1. Its really important that you shut down terminal libraries like TermBox; failure to do so leaves the user’s shell in a possibly unusable state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment