Optional arguments in Rust 1.12
Posted on Thu 29 September 2016 in Code
Today’s announcement of Rust 1.12 contains, among other things, this innocous little tidbit:
Option
implementsFrom
for its contained type
If you’re not very familiar with it,
From
is a basic converstion trait
which any Rust type can implement.
By doing so, it defines how to create its values from some other type — hence its name.
Perhaps the most widespread application of this trait (and its from
method)
is allocating owned String
objects from literal str
values:
let hello = String::from("Hello, world!");
What the change above means is that we can do similar thing with the Option
type:
let maybe_int = Option::from(42);
At a first glance, this doesn’t look like a big deal at all.
For one, this syntax is much more wordy than the traditional Some(42)
,
so it’s not very clear what benefits it offers.
But this first impression is rather deceptive.
In many cases, this change can actually reduce the number of times we have to type Some(x)
,
allowing us to replace it with just x
.
That’s because this new impl
brings Rust quite a bit closer to having optional function arguments
as a first class feature in the language.
Until now, a function defined like this:
fn maybe_plus_5(x: Option<i32>) -> i32 {
x.unwrap_or(0) + 5
}
was the closest Rust had to default argument values.
While this works perfectly — and is bolstered by compile-time checks! —
callers are unfortunately required to build the Option
objects manually:
let _ = maybe_plus_5(Some(42)); // OK
let _ = maybe_plus_5(None); // OK
let _ = maybe_plus_5(42); // error!
After Option<T>
implements From<T>
, however, this can change for the better.
Much better, in fact, for the last line above can be made valid.
All that is necessary is to take advantage of this new impl
in the function definition:
fn maybe_plus_5<T>(x: T) -> i32 where Option<i32>: From<T> {
Option::from(x).unwrap_or(0) + 5
}
Unfortunately, this results in quite a bit of complexity,
up to and including the where
clause: a telltale sign of convoluted, generic code.
Still, this trade-off may be well worth it,
as a function defined once can be called many times throughout the code base,
and possibly across multiple crates if it’s a part of the public API.
But we can do better than this.
Indeed, using the From
trait to constrain argument types is just complicating things for no good reason.
What we should so instead is use the symmetrical trait, Into
,
and take advantage of its standard impl
:
impl<T, U> Into<U> for T where U: From<T>
Once we translate it to the Option
case (now that Option<T>
implements From<T>
),
we can switch the trait bounds around and get rid of the where
clause completely:
fn maybe_plus_5<T: Into<Option<i32>>>(x: T) -> i32 {
x.into().unwrap_or(0) + 5
}
As a small bonus, the function body has also gotten a little simpler.
So, should you go wild and change all your functions taking Option
als to look like this?…
Well, technically you can, although the benefits may not outweigh the downsides
for small, private functions that are called infrequently.
On the other hand, if you can afford to only support Rust 1.12 and up, this technique can make it much more pleasant to use the external API of your crates.
What’s best is the full backward compatibility with any callers that still pass Some(x)
:
for them, the old syntax will continue to work exactly like before.
Also note that the Rust compiler is smart about eliding the no-op conversion calls like the Into::into
above,
so you shouldn’t observe any changes in the performance department either.
And who knows, maybe at some point Rust makes the final leap, and allows skipping the None
s?…