Rfcs: Sugar for closures with restricted access to enclosing's names.

Created on 10 Jul 2018  ·  9Comments  ·  Source: rust-lang/rfcs

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
}
T-lang

Most helpful comment

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

All 9 comments

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) }

A macro can't do this.

  • The only way to hide the non-specified bindings is to define an fn item.
  • An fn signature requires full knowledge of all types.
  • Types are unavailable to a macro.

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

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

  • [ ] Allow transparently borrowing lvalues the same way closures do (without changing the type)

    • This could theoretically be done by, you know, using a closure, but that is incompatible with how I forbid unlisted bindings (which involves coercing a closure into a fn pointer).

  • [ ] Allow initializing an uninitialized value. (let x;)
  • [x] Allow borrowing in a manner that changes type to &T or &mut T
  • [x] Allow moving values
  • [x] Make unlisted bindings unusable
  • [ ] Make unlisted bindings unusable, with a useful error message

    • (in the prototype, you get a type error between an fn pointer type and a closure)

  • [ ] Uh... compile.

    • (I added a hack for type inference at the last minute before posting and apparently introduced some kind of really dumb bug. You know, the kind of bug that inevitably plagues every single WIP token tree muncher ever written since the code is nigh unmaintainable. Given that I can't get what I want design-wise anyways, I'm about fed up with this and do not care to fix it)

    • (also it just occurred to me that the way I use Types are wrong and that they need to be corrected to &Type and &mut Type. Meh.)

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.

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 :)

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

@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:

  • The best places to draw lines in the sand now are not necessarily going to remain the best places to draw lines in the sand in the future.
  • What ought to happen for the best clarity of code is that, as the requirements change, I should eventually inline the function definitions so that I can discover new ways to refactor the flow of data at a higher level, and then factor out a different set of functions.
  • There is an exceedingly small chance that I will ever do that, because it is difficult! Factoring out a function in CLion is a click of the mouse; but seldom do tools provide the inverse capability.
  • Over time, my mind will forget how arbitrarily these lines in the sand were drawn, and will begin to see the new function as a distinct entity. Unable to see that there are now better ways to structure the flow of data, I will hack additional arguments, outputs, and possibly even new responsibilities (gasp!) onto them. (this is especially true in CLI application code)

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

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

  • having to write out all of the types (so yes, I would really love fn foo(x: _)!)
  • having to duplicate all the generic type params and where bounds (renamed of course, since they're not allowed to shadow!)
  • having to deal with the fact that the types of some bindings within the code will suddenly change from an owned type to a borrowed type
  • having to write the arguments in two places

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

burdges picture burdges  ·  3Comments

p-avital picture p-avital  ·  3Comments

onelson picture onelson  ·  3Comments

silversolver1 picture silversolver1  ·  3Comments

3442853561 picture 3442853561  ·  3Comments