Rust is jam-packed with language level features that make it easier to get good work done.  In my opinion, the macro unimplemented! shines brightly in this category as a tool that helps you get to a solid design in a hurry.  This is especially true given the fact that Rust is, as opinionated languages come, extremely opinionated.  The compiler has a reputation for having a zero-tolerance policy when it comes to sloppiness, and occasionally that gets in the way of getting an idea out of your head and into your editor.

In this post, I want to explore how unimplemented! helps get the language out of your way, at least temporarily, and gets you designing faster.  In a future post, yet to be written or even understood, I'd like to unwrap how unimplemented! actually works - but for now, let's talk about what it can do for you.

Getting the compiler out of your way

When I write code, I'm often in a position where I want to see how the code is going to look before I know how it's going to run.  This isn't purely an aesthetic.  In strongly typed languages like Rust, it's often possible to understand what a program does (at least superficially) by just reading the type signatures.  For example, you'd forgive me for thinking that a function called n_chars with a signature that looks like String -> usize probably counts the characters in a string.

Granted, I don't need the type annotation to have a pretty good guess, but it's additional evidence.  Entire programs can be understood in much the same way.  Complex functions can be broken down in terms of simply what comes in and what comes out, and arguably software architecture is just what comes in and what comes out.  For that reason and more, it's nice to be able to write the skeleton of your program before doing any of the hard work of implementing the functions of which it's comprised.

Here's what this might look like.

fn into_chars(a: String) -> Vec<char> {
	# Somehow this is going to break this string into characters.
}

fn count_unique(b: Vec<char>) -> usize {
	# This will count how many unique elements are in the input vector.
}
    
fn main() {
	let input = String::from("this is my input");
	let output = count_unique(into_chars(input));
	println!("{}", output);
}	

Imagine that I wrote the above, just trying to see how my input string(s) flow through the program.  In this contrived case, I just want to know that my types make sense, that I haven't committed some fundamental error about how my functions work, and that the logic is therefore sane.  This looks great.

Except that it doesn't compile.  And while that might be OK in a simple case such as the above, that won't fly in more complicated cases.

Here's an example: I recently wrote a hardware abstraction layer for hardware that, like a lot of hardware, didn't technically exist yet.  Writing that code involved a lot of functions that dispatched requests to the underlying hardware layer.  Combined with a trait object, that code looks something like this:

trait Hardware {
	fn enable_output(&mut self) -> Result<()>;
}
       
pub struct HardwareAbstractionLayer {
	hardware: Box<dyn Hardware>;
}
        
impl HardwareAbstractionLayer {
	pub fn enable_output(&self) -> Result<()> {
		self.hardware.enable_output()
	}
}

In terms of this abstraction layer, we can write all of our client code.  After all, enable_output is fully implemented.  But what about for the actual object that implements Hardware?  If I don't know what code to write, how can I write code that will compile and will let me move forward with integrating the hardware into my program, at least at a high level?

This is where unimplemented! comes to the rescue.

Let's write a hypothetical struct that implements Hardware, but use unimplemented! where we don't know exactly what's going to happen yet.

pub struct Underlying {
	// ... some data
}

impl Hardware for Underlying {
	fn enable_output(&mut self) -> Result<()> {
		unimplemented!()
	}
}

This code will compile.  In fact, as long as the code above the call to unimplemented! will compile, this function will compile and you'll be in good shape to move forward with architecting the rest of the code.

Now what?

You write the rest of your code!  When that hardware shows up and you know exactly what you need to write, you can fill in your implementation and run it.

In a future post, I'll explore exactly how Rust performs this magic.  For now, it's a pretty good trick right?