Inner function sugar?

A common pattern in Rust is to use an inner function with fewer generic inputs to contain most of the outer function's implementation without the cost of monomorphization.

fn with_str(s: impl AsRef<str>) {
    fn inner(s: &str) {
        // lots of code
    }
    inner(s.as_ref());
}

This pattern feels pretty "noisy" and easy to resist. It adds a level of indentation to the entire function. Long function signatures have to be basically repeated. And the "first" line is (conventionally) moved to the bottom.

My understanding is that using a closure serves the same purpose, but Rust is currently not very efficient at reducing inherited generics in closures. (?) And closures mostly still have the same issues stated above.

My idea is to create a syntactic sugar for this:

fn with_str(s: impl AsRef<str>) {
    let s = s.as_ref();

    fn_boundary! // (straw man)
    dbg!(s);
    // lots of code
}

You can put the marker fn_boundary! anywhere as a statement. The remainder of the block "captures" the environment through a generated function invocation. You're code can be written in normal execution order and you don't have to write a function signature or add indentation. I suppose you could add attributes to the marker and they will be on the generated function.

And it isn't just useful for generics. There are scenarios where the first one or two lines of code are a good candidate to be inlined independently.

Maybe it is equivalent to wrapping the remainder with move || {..}(). And so maybe this could be a proc macro, but would a compiler feature be significantly faster? I'd like to hear more informed opinions.

I don't think it needs a syntax. It's an optimization that the compiler should be able to do automatically.

14 Likes

momo does this as a proc macro

2 Likes

To chip away at this slightly, I think this bit we could actually fix. Right now having the inner function at the end counts as the trailing expression in the block, which is kinda weird -- that's certainly not what I expect every time I write it that way.

Maybe someone more familiar with the grammar could chime in and say whether it'd be ok for items at the end of the function body to not count like that?

Though I suppose if I want it at the beginning I can always do

 pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
+    return inner(path.as_ref())
     fn inner(path: &Path) -> io::Result<String> {
         let mut file = File::open(path)?;
         let mut string = String::with_capacity(initial_buffer_size(&file));
         file.read_to_string(&mut string)?;
         Ok(string)
     }
-    inner(path.as_ref())
 }

so unless this is a trivial change it's probably not worth doing.

2 Likes