Writing terrible code

Writing terrible code

There is an art to designing just enough to get feedback, and then using that feedback to improve the design enough to get the next round of feedback.
—Kent Beck, “Extreme Programming Explained”

The secret of being a great coder is to write terrible code. Wait, wait. Let me back up a little. How do we know if code is good, anyway? Surely one of the most important signs is that it’s correct. If your code does the wrong thing, practically nothing else about it matters.

That’s why it’s very helpful to write tests. A good test tells you that your function is correct, but a great test tells you when your function is wrong. And the only way to know if it will do that is to write the wrong function. Let’s do just that.

In The magic function, we wrote this test for an imaginary count_lines function in Rust that, yes, counts lines in its input:

use std::io::Cursor;

#[test]
fn count_lines_fn_counts_lines_in_input() {
    let input = Cursor::new("line 1\nline 2\n");
    let lines = count_lines(input);
    assert_eq!(lines, 2, "wrong line count");
}

(Listing count_2)

Even if you don’t know Rust, I bet you can see what this is testing. We create something called a Cursor, which simulates input to the program, and we put a string containing two lines into it. Calling count_lines with this input should return the answer 2, shouldn’t it? And that’s exactly what the test asserts.

More importantly, any other answer should cause the test to fail, and because we’re sceptical by nature, we want to see that happening. So, to verify the test, let’s go ahead and write a simple-minded version of count_lines that deliberately returns the wrong answer: zero, for instance.

In order to write the function at all, though, we have to work out what its signature is: what type of parameter it takes, and what it returns:

pub fn count_lines(input: ???) -> ???

So what type should count_lines take as a parameter? Well, in the test, we’re passing it a Cursor, but that won’t be the case in the real program. We want the tool to read from its standard input:

use std::io::stdin;

use count::count_lines;

fn main() {
    let lines = count_lines(stdin().lock());
    println!("{lines} lines");
}

(Listing count_2)

So in main, we’re passing the result of calling stdin().lock(), which is a type called, perhaps not surprisingly, StdinLock. So which parameter type should we declare: Cursor or StdinLock?

Neither, because we need it to work with both. So we’ll have to be a bit more general. In Rust we can often use a trait to identify the set of types that we can accept, based on what we want to do with the object. So, what do we need to do with input?

In this case, we want to read from it, so the trait in question would be BufRead: both StdinLock and Cursor implement this.

If we declare input to be of type impl BufRead (that is, “anything that implements BufRead”), then we can accept a Cursor, and a StdinLock, and anything else we could read from:

use std::io::BufRead;

pub fn count_lines(input: impl BufRead) -> usize {
    0
}

For the return value, usize makes sense: it’s the right choice whenever we’re talking about some number of things, such as lines.

And the function ignores input completely and always just returns zero, which is what we said we wanted to do in order to check that the test fails. Well, let’s see!

cargo test

assertion `left == right` failed: wrong line count
  left: 0
 right: 2

Great. That’s exactly what we wanted to see, and if we hadn’t seen it, we would have needed to fix the test until we did. But now we can move on and write the real implementation—or rather, you can. Time for another challenge!

GOAL: Modify count_lines so that it passes this test. Make sure it works with the main function as well.


HINT: Okay, it’s not easy, but I’m not some inhuman monster. I’ve given you all the clues you need to put this puzzle together. You already have the right signature for count_lines, so it’s just the function body you need to change.

You have the code for counting lines, too; you can steal that from the old main function in Listing count_1. All great programmers copy and paste. Why waste your brainpower on things that are already solved?


SOLUTION: Here’s my version:

use std::io::BufRead;

pub fn count_lines(input: impl BufRead) -> usize {
    input.lines().count()
}

(Listing count_2)

How boring! But that’s exactly what we want, or at least it’s what I want: simple, straightforward, and obvious. No fancy stuff. In my book The Secrets of Rust: Tools, you’ll learn more about the power of this approach, by developing some useful programs that are, nonetheless, charmingly simple. All we have to do is focus on writing great tests, and then passing them with as little fuss as possible.

And if your solution passes the test, it’s correct, even if it looks different to mine. Let’s see if this also works when we run the main function as a binary:

echo hello | cargo run

1 lines

We’d be very worried if it had said anything else! So, now that we have a working library crate with a (relatively) user-friendly API, let’s indulge ourselves with a little tidying up and beautifying; “making good”, as the decorators say.

One thing we can do to make our library more Rustaceous is to move our unit tests (well, test) into a module. A module is simply Rust’s way of grouping related code into a little sub-unit, using the mod keyword, so that it can be turned on or off at will.

For example, we only need to compile the test function when we’re running tests. When we build or run the binary with cargo run, why waste time compiling a function we’re not going to call?

The conventional answer to this in Rust is to put our unit tests inside a module (mod tests, let’s say, though the name is up to us), and then protect that module with a cfg(test) attribute:

#[cfg(test)]
mod tests {
    // tests go here
}

cfg turns compilation on or off for the item it’s attached to, based on the value of some expression. cfg(test) means the tests module will be compiled only if we’re running in cargo test mode. Otherwise it will be ignored entirely, saving time, energy, and the planet.

So here’s what our test looks like once we’ve moved it into its own module:

#[cfg(test)]
mod tests {
    use std::io::Cursor;

    use super::*;

    #[test]
    fn count_lines_fn_counts_lines_in_input() {
        let input = Cursor::new("line 1\nline 2\n");
        let lines = count_lines(input);
        assert_eq!(lines, 2, "wrong line count");
    }
}

(Listing count_2)

A module can have its own use declarations, which is handy since we often want to use things in tests that we don’t need in the library itself (Cursor, for example).

Also, a module doesn’t automatically use (that is, have access to) things defined in its parent module. That’s what the use super declaration is about. super always refers to the parent module, and since tests usually want access to everything in the parent, use super::* brings in the whole lot at once.

It’s all very well to test the happy path—what happens when everything goes right—and, indeed, you’ve done so beautifully. Give yourself a pat on the back. But in the next post, we’ll talk about what to do when things go wrong, which, as you’re well aware, they always do. See you then!

Slow, flaky, and failing

Slow, flaky, and failing

0