rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.87k stars 1.56k forks source link

Sugar for closures with restricted access to enclosing's names. #2496

Open bpsuntrup opened 6 years ago

bpsuntrup commented 6 years ago

If I am writing a long function, I often want to close off a chunk of code with { code_in_here } so that local variables I declare within the chunk don't "escape". Example:

fn long_function() {
   let mut x = 1;
   let y = 3
   {
      let y = 2;
      println!("I have {} apples");
   }
   assert!(y == 3);
}

However, inside the { }, it's still possible to read (and even write to) x. When reading long functions, I would like a way of assuring that a given complicated chunk of code only touches the variables I want it to touch.

A solution:

fn long_function() {
   let x = 1;
   let y = 3
   {fn t(x: &i32) { 
      let y = 5;
      println!("I have {} apple and {} oranges.", x, y);
   }t(&x);}
   assert!(y == 3);
}

This works, and does exactly what I need, but is rather ugly. I suggest a bit of built-in sugar for this use case. Perhaps rather than:

{fn t(args: &T, to: &mut U, pass: &V) { 
   //isolated logic
}t(&args, &mut to, &pass);}

we would have:

close (&args, &mut to, &pass) {
   // enclosed logic, assured not to touch anything but args, to and pass
}

And we can tell just by looking at one line exactly what variables this chunk of code touches and what its side effects are.

...or even better, a closure with controlled access to its enclosing scope that returns a value:

let a: i32 = close (&args, &mut to, &pass) -> i32 {
   // enclosed logic, assured not to touch anything but args, to and pass
   to = args + pass;
   let some_int = 5 - args;
   some_int
}
burdges commented 6 years ago

You'd do this to improve readablity / auditability, so you'll frequently prefer the nested function call with a descriptive name and good comments. Also a macro could build code like:

{ fn tempname<A,B,C>(name1:A, name2: B, name3: C) {
} tempname(name1,name2,name3) }
ExpHP commented 6 years ago

A macro can't do this.

Edit: Wait a second. I think I have a (terrible) idea...

ExpHP commented 6 years ago

Well, okay. I thought I had a prototype, but it barely does any of what I wanted it to:

Here is the broken mess.

let a = vec![()];
let b = vec![()];
let mut c = vec![()];
let d = vec![()];
let mut e = vec![()];

using!{[b, mut c, ref d, ref mut e] {
    let _ = b;            // b is Vec<()>. It was moved.
    c.sort();             // c is Vec<()>. It was moved.
    let _ = d.iter();     // d is &Vec<()>
    let _ = e.iter_mut(); // e is &mut Vec<()>
    // let _ = a.iter();  // a cannot be used.
}}

Sigh. The only way to make it work really nice is with language support. There's simply no other way to check off the first two bullets.

burdges commented 6 years ago

Cute trick with the closure type inference rules.

An RFC for closure style type inference for _ types in nested fns makes this trivial, except not the first two points. And sounds vastly more useful.

fn bar() {
    .. 
    fn foo(x: _) { .. } 
    ..
}

It's also much more convincing since you merely argue that nested fns should benefit the same ergonomics that closures do.

I still think: You're doing this for readability, so go the whole way and split up the long function or at least give your nested functions good names.

As an aside, if you want truly hackish then one could maybe do this latex style with a procedural macro that produced side effectual debug builds that wrote the type information to an .aux file using std::intrinsics::type_name :)

oli-obk commented 6 years ago

You can always just write a clippy lint that enforces the behaviour you want. It'll essentially come down to

#[clippy::close(mut x, move y)]
{
     // modifying x and y possible within block. Everything else can only be read
    // can modify Cell/Mutex though
}

Restricting the language is exactly what lints do. Especially our restriction lints

ExpHP commented 6 years ago

@burdges when I factor out a function and give it a name, I'd rather it serve a purpose beyond just being a line drawn in the sand for readability. The reason for this is that, in code that is subject to frequent changes in requirements:

And thus factoring out a function to make the code cleaner in the short term can backfire in the long run, if done arbitrarily.

ExpHP commented 6 years ago

Oh, um, er... I think I misread your posts and didn't realize you were still advocating nesting the function body inside the original function.

In that case, my major annoyances are really

burdges commented 6 years ago

I do think nested fn foo(x: _) sounds like an uncontroversial RFC, since closures already work that way.

leonardo-m commented 6 years ago

#[clippy::close(mut x, move y)] looks a lot like a similar Ada/SPARK feature.