Small refactoring pain points

#1

I like to think of a struct being a tiny classic C language program with disgusting global variables and procedures full of spaghetti code using those globals. This is okay though, because the mess is encapsulated into a nice little scope that isolates both the names and the state from the outside world.

In other words, I like to think of nested namespaces as continuing into structs and eventually into the function body.

mod a {
    mod b {
        struct c {
            fn d {
                for ... {
                    if ... { 
                       // ... etc ... 
                    }
                }
            }
        }
    }
}

Ideally, it should be possible to extract a snippet of code, move its variables into to a struct to hold the state, and the code to functions to manipulate the state, in isolation from the rest of the code.

In Java and C#, this is trivial. Just cut & paste the variable declarations into an empty struct/class definition. Then cut & paste the code and wrap a function declaration around it. It’s not much more complicated than just typing in the struct scope in the sample above.

Rust’s syntax however just makes this… not complex, but unnecessarily fiddly. Just about every step is full of small irritations, and practically every transplanted line of code will require some editing, even if its semantics is completely unchanged.

The amount of effort scales with the amount of code linearly at a minimum, but in some cases it’s worse. When using multiple lifetimes, the scaling is quadratic, with a truly hideous amount of fiddly edits required when making any such refactoring. Even once this effort has been invested, further edits yet again require significant effort. Adding one more lifetime to a struct, for example, requires many small edits.

// Imagine starting with a block of spaghetti code you'd like
// to extract into a struct (or enum):
fn foo() {
    // ...some data...
    let a: i32 = 1;
    let b: i32 = 0;    
    let c: &str = "hello";  
    let d = &a;

    // ... some code...
    let bar = || { 5 };
    let _e = a + b + bar();
}

// Why not just infer the lifetime specifiers from the struct body definition?
// It's just a list all of the lifetimes used in the struct. It's an error *not* to 
// use a lifetime as well, so this ought to be 1:1 with the struct body.
struct Extracted<'a> {
    // the `let` and `mut` keywords have to be removed. The `let` keyword, I can
    // understand, but note that Rust is one of the few non-functional languages
    // to make assignment an explicit keyword, *yet* it allows silent shadowing, 
    // which practically no modern language allows for safety reasons. Feels like
    // a warning label attached to the trigger saying "do not press" instead of
    // an actual safety switch. 
    //
    // It *would* make sense to keep `mut` in structs to allow both immutable and
    // partially immutable types to be represented, but this is not allowed. Struct
    // members are mutable by default, and immutability is all or nothing for the
    // whole struct. IMHO this goes against the grain of modern safe programming
    // practices of "make everything immutable unless explicitly required".
    a: i32,    // Can't set defaults in-line here because... reasons?
    b: i32,    // Have to replace ';' with ','. For more reasons?
               // Note that C, C++, Java, and C# use ';' in structs, making this
               // particularly grating.
    c: &'a str // also means no trailing ',', unlike ';' separated structs in
               // other languages. Stops a simple '.'->';' search & replace editing.
    // d: ...? // ugh, don't even try. Something hideous to do with Pin<...>?
}

// Lets repeat the lifetime list again. Twice. DRY is blown out of the water
// now, we're up to 4x repetitions, minimum, of every lifetime used in a struct!
//
// This topic has been discussed, but not much has come of it:
//    https://internals.rust-lang.org/t/lang-team-minutes-elision-2-0/5182
//
// Why mandate separate 'impl' blocks in the first place for structs not 
// implementing a Trait? Why not just allow directly nesting impl code for 
// the default case, i.e.: when not implementing a specific trait?
impl <'a> Extracted<'a> { 

    // For added inconsistency, associated constants, unlike fields,
    // are separated by semicolons:
    const FIVE: i32 = 5;
    const _SIX: i32 = 6; // We can have a trailing ';' now, unlike struct fields.
    
    fn bar() -> i32 {
        // "Self::" prefixes have to be added... for reasons.
        // Maybe, just maybe, the compiler could just assume I meant
        // the scope of the struct and just go with that unless
        // I explictly select the global scope in the rare event of
        // a name conflict I actually care about.
        //
        // Note that this is called "Extracted::FIVE" in all other code
        // which makes it frustrating to search through or read
        // code written like this. (Yes, I'm aware I can use Extracted::FIVE
        // here, but other programmers are not forced to do so, which means
        // most code won't be consistent with either rule.)
        Self::FIVE
    }

    fn compute_e( &self ) -> i32 {
        // Have to prefix every. Field. And Method. Every. Time.
        //
        // Add on top the inconsistency of associated functions 
        // and constants, which need the even more verbose 
        // Capital Self Colon Colon to really drive the point home...
        self.a + self.b + Self::bar()
    }
    
    // PS: Wouldn't it be nice to have C# style one-line functions?
    fn compute_e_brief(&self) => a + b + bar();
}

fn foo_refactored() {

    // If using a struct intead of loose variables the variable 
    // initialisation code now uses ':' instead of '=', despite
    // doing the same thing. Quite possibly *literally* the same
    // instructions being emitted to modify the exact same bytes
    // of memory in exactly the same way.
    let data = Extracted {
        a: 1, // Must specify everything. Can't have defaults!
        b: 0,
        c: &"hello"
    };
    
    // The first line of code in this example that had a necessary
    // edit made to it, instead of an unnecessary one driven purely
    // by missing features or odd syntax choices...
    let _e = data.compute_e();
}
1 Like
#2

Working on that.

Works fine?

struct Foo {
    x: u8,
}

You can just write:

impl Extracted<'_> { ... }

these days.

Working on that as well; well, not exactly that… but rather:

fn compute_e_brief(&self) = a + b + bar();

which is in the same spirit of what you wanted.

1 Like
#3

I haven’t seen that yet. Is there an RFC/issue to follow?

#4

Not yet; I’ll post a link here once my RFC is done. Stay tuned!