Rust 1.0 Released! I’ve since been tinkering with a small side project here and there (not the vaguly WSGI like thing – that project taught me lots of interesting things, but nothing worthy of more posts about it). This post won’t get into sepecifics about that – I’m saving that for another post or series of posts – but I will talk about something that made me pause to think about, mostly because I’m just not used to dealing with this sort of thing so explicitly!
The problem came from my desired to test my code – for my tests I wanted a stubbed out dummy object that I could put in place of it’s more completed sibling while testing some aspects of the code. Immediately what you find is that traits are a perfect fit for this problem.
For my purposes in this case, they’re basically the equivalent of interfaces – a thing that specifies how something looks if it identifies itself as a thing.
Now, let me show you some code that you might think would work at first glance (but it won’t, for good reason):
trait Sword {
fn foo(&self) -> u8;
}
struct Sting;
impl Sword for Sting {
fn length(&self) -> u8 { 15 } // fixed size since there's only one Sting!
}
impl Sting {
fn new() -> Sting { Sting }
}
struct OrcBlade {
len: u8, // Orc blades come in all sorts of lengths
}
impl Sword for OrcBlade {
fn length(&self) -> u8 { self.len }
}
impl OrcBlade {
fn new(len: u8) -> OrcBlade { OrcBlade { len: len } }
}
struct Character {
sword: Sword,
}
impl Character {
fn new<T: Sword>(s: T) -> Character {
Character { sword: s }
}
}
fn main() {
let sting = Sting::new();
let frodo = Character::new(sting);
println!("How long is the sword Frodo carries? {}in", frodo.sword.length());
let orcblade = OrcBlade::new(24);
let orc = Character::new(orcblade);
println!("How long is this orc's blade? {}in", orc.sword.length());
}
Yes, this example is completely contrived!
This example errors out with just a few well written errors:
swords.rs:28:22: 28:23 error: mismatched types:
expected `Sword`,
found `T`
(expected trait Sword,
found type parameter) [E0308]
swords.rs:28 Character { sword: s }
^
swords.rs:27:28: 27:37 error: the trait `core::marker::Sized` is not implemented for the type `Sword` [E0277]
swords.rs:27 fn new<T: Sword>(s: T) -> Character {
^~~~~~~~~
swords.rs:27:28: 27:37 note: `Sword` does not have a constant size known at compile-time
swords.rs:27 fn new<T: Sword>(s: T) -> Character {
^~~~~~~~~
swords.rs:34:6: 34:11 error: the trait `core::marker::Sized` is not implemented for the type `Sword` [E0277]
swords.rs:34 let frodo = Character::new(sting);
^~~~~
swords.rs:34:6: 34:11 note: `Sword` does not have a constant size known at compile-time
swords.rs:34 let frodo = Character::new(sting);
^~~~~
swords.rs:37:17: 37:34 error: this function takes 0 parameters but 1 parameter was supplied [E0061]
swords.rs:37 let orcblade = OrcBlade::new(24);
^~~~~~~~~~~~~~~~~
error: aborting due to 4 previous errors
Let’s take a look at the first one first!
swords.rs:28:22: 28:23 error: mismatched types:
expected `Sword`,
found `T`
(expected trait Sword,
found type parameter) [E0308]
swords.rs:28 Character { sword: s }
This is the bunch of code that’s erroring out, specifically:
struct Character {
sword: Sword,
}
impl Character {
fn new<T: Sword>(s: T) -> Character {
Character { sword: s }
}
}
What’s the problem? Well, turns out you can’t really use traits as drop in replacements for types. Instead of using sword as a concrete value, you want to swing it around as a reference, and then tell the compiler that whatever object is being referenced should implement the specified trait.
Here’s what I mean, the previous example should actually look like this:
struct Character {
sword: &Sword,
}
impl Character {
fn new<T: Sword>(s: &T) -> Character {
Character { sword: s }
}
}
But what’s this? A completely different error about something called a ‘lifetime’?
swords.rs:24:9: 24:15 error: missing lifetime specifier [E0106]
swords.rs:24 sword: &Sword,
^~~~~~
error: aborting due to previous error
Turns out that Rust likes to know when references to memory aren’t actually needed anymore. It kinda does this automatically and transparently in some situations, but in this situation Rust has no idea when it should expect the sword reference to be valid or invalid. All this means is the code needs to be a little more explicit. I’m still learning a BUNCH about this, and haven’t really wrapped my mind all the way around it, but if we add a few tokens to the code, we won’t have this error anymore:
struct Character<'a> {
sword: &'a Sword,
}
impl<'a> Character<'a> {
fn new<T: Sword>(s: &T) -> Character {
Character { sword: s }
}
}
Sadly, we still have some though:
swords.rs:34:29: 34:34 error: mismatched types:
expected `&_`,
found `Sting`
(expected &-ptr,
found struct `Sting`) [E0308]
swords.rs:34 let frodo = Character::new(sting);
^~~~~
swords.rs:37:17: 37:34 error: this function takes 0 parameters but 1 parameter was supplied [E0061]
swords.rs:37 let orcblade = OrcBlade::new(24);
^~~~~~~~~~~~~~~~~
swords.rs:38:27: 38:35 error: mismatched types:
expected `&_`,
found `OrcBlade`
(expected &-ptr,
found struct `OrcBlade`) [E0308]
swords.rs:38 let orc = Character::new(orcblade);
^~~~~~~~
error: aborting due to 3 previous errors
Lets just look at the first one again:
swords.rs:34:29: 34:34 error: mismatched types:
expected `&_`,
found `Sting`
(expected &-ptr,
found struct `Sting`) [E0308]
swords.rs:34 let frodo = Character::new(sting);
^~~~~
Doh! Forgot to change our test code in main()
! It’s not passing references to the Character::new()
method we modified earlier. Here’s the final code:
trait Sword {
fn length(&self) -> u8;
}
struct Sting;
impl Sword for Sting {
fn length(&self) -> u8 { 15 } // fixed size since there's only one Sting!
}
impl Sting {
fn new() -> Sting { Sting }
}
struct OrcBlade {
len: u8, // Orc blades come in all sorts of lengths
}
impl Sword for OrcBlade {
fn length(&self) -> u8 { self.len }
}
impl OrcBlade {
fn new(len: u8) -> OrcBlade { OrcBlade { len: len } }
}
struct Character<'a> {
sword: &'a Sword,
}
impl<'a> Character<'a> {
fn new<T: Sword>(s: &T) -> Character {
Character { sword: s }
}
}
fn main() {
let sting = &Sting::new();
let frodo = Character::new(sting);
println!("How long is the sword Frodo carries? {}in", frodo.sword.length());
let orcblade = &OrcBlade::new(22);
let orc = Character::new(orcblade);
println!("How long is this orc's blade? {}in", orc.sword.length());
}
Now we’re being explicit with how our references are being passed around, and the Rust compiler can give us piece of mind that we’re handling our data safely!