Trouble Refactoring by Extracting Methods

What are some habits I can learn to make refactoring easier with Rust? My typical pattern of getting something to work first and then extracting common tasks into specific functions is falling apart. The intermediate solution I've come up with in shoving the contents of every Iterator into a Vec so I can return it from a function feels like a huge code smell. What am I doing wrong? How am I supposed to write code that doesn't look like garbage in Rust?

Example: How could I write a function that just returns files/directories that start with "./o" from this playground example? Rust Playground

How would I refactor that into a function that just uses get_files (or takes its output as an argument) does the filter and returns something that can be iterated over without making a new datastructure and emptying it into other?

Or is this just the pattern that's expected?

There's a few things at play here:

  1. Rust, by being the kind of language it is, may be a bit bigger than you're used to. As in, in Ruby, I basically never write functions that are over 5 or 7 lines, but in Rust, fifteen or twenty seems roughly to be about a comfortable minimum in many cases. Though not all! Lots of one-liners too.

  2. Returning iterators is a bit of a pain at the moment. We're actively discussing a language change that would make it much easier, which I think would help with your code.

  3. Let's look at one of your lines of code here:

    if f.as_path().to_str().unwrap().starts_with("./o"){ // yuck

This line is a bit yucky, but that's because Rust forces you to deal with error conditions that other languages paper over. There are two of them here: paths are not strings, because paths might not be utf8, and so you need the to_str() conversion. Because it checks said validity, it returns an Option<T>, and so you call unwrap() on it, meaning that you are going to crash if anything isn't. Then, the business you wanted in the first place: the starts_with.

If this is the error handling strategy you want, you can do things like tuck it away yourself. For example, this works:

trait MyPathExt {
    fn starts_with(&self, s: &str) -> bool;
}

impl MyPathExt for Path {
    fn starts_with(&self, s: &str) -> bool {
        self.to_str().unwrap().starts_with(s)
    }
}

fn main() {
    for f in get_files(Path::new(".")){
        if f.starts_with("./o"){ // no more yuck
            println!("{}", f.display())
        }
    }
}

It's possible that over time, Rust will gain libraries which just give you these kinds of conveniences, so this could look like:

extern crate simple_path;
use simple_path:PathExtensions;

fn main() {
    for f in get_files(Path::new(".")){
        if f.starts_with("./o"){ // no more yuck
            println!("{}", f.display())
        }
    }
}

But as always, this convenience comes with a danger: again, your program will crash in certain circumstances. Only you can say if that's appropriate, but Rust gives you the tools to decide for yourself.

Hope that helps!

3 Likes

It's worth noting that in this particular case, Path has a starts_with method, so you can just write f.as_path().starts_with("./o").

Not quite:

Only considers whole path components to match.

1 Like

ah, good to know, thanks!

Coming from a dynamic language background, totally agree -- I have a habit of keeping things as short as possible. What about Rust tends to make functions to be a little longer? Is it a style thing, or do Rust users just feel more comfortable with longer blocks of code? Or is it just okay to let things grow longer because the compiler is managing state way more aggressively at the input/output of a function?

Ooo, cool, could you link me? I feel like the discussion on this would help me understand more about the intent in design of Rust.

Ah, nice this is definitely the sort of thing I am looking for. Because I'm coming from dynamic languages, I wouldn't think to use features in Rust's type system to factor out parts of my code. I still want to get more fluent at re-sculpting high level control flow, but adding a method to an object seems like something I should be using way more often.

Regarding the bit about libraries that could just add convenience methods to types:

That might be nice, though it feels a little like from requests import *, where the namespace gets polluted by who knows what. I want to better learn how to use what Rust gives me to express the problems I'm trying to solve. I write code that moves data around, and then when I want to move the code around Rust complains -- which is fine, I know now that it's protecting me from various sorts of errors, but I want to figure out if I keep stubbing my toes because I'm walking in the wrong direction or if you just have to keep stumbling over type errors till the compiler concedes.