Some usability issues with closures

I’ve run into a couple of usability issues with closures that I wanted to discuss.

Some parts of my code end up having to pass methods to other functions:

struct Foo();
impl Foo { fn foo(self, i32, i32) {} }

bar(|x, y| obj.foo(x, y)).baz(|x,y| obj2.foo(x, y)) /* ... */; 

but if instead of methods I am using functions I can just write bar(fn_0).baz(fn_1).

While I understand why things are the way they are, I would really like to see sugar that allows me to write the way nicer bar(obj.foo).baz(obj2.foo)... and that desugars to the closure version above. There is an RFC Issue 1287 for this and maybe it could be framed in the Roadmap for 2017 for improving usability.

A minor issue is “recursion”, or at least not being able to capture the closure in itself (or access it through self. within the closure). This blog post was written in 2013 and gives some reasons for this. I wonder whether they are still relevant.

Finally, another issue I’ve run with closures is in code like this, where I have:

pub fn noop_fold_expr<F: Folder>(folder: &mut F, Expr { node, attrs }: Expr) -> Expr {
     use self::ExprKind::*;
     Expr {
         node: match node {
             ExprKind::Box(e) => ExprKind::Box(e.lift(|e| folder.fold_expr(e))),
             InPlace(place, value) => {
                 InPlace(place.lift(|e| folder.fold_expr(e)),
                         value.lift(|e| folder.fold_expr(e)))
             }
             Array(array) => Array(array.lift(|e| folder.fold_expr(e))),
               // ...
           }
       }

Notice the repetition of |e| folder.fold_expr(e) (and similar closures) all over the place. Ideally I would like to just write place.lift(fold_expr), but there are some issues that could be solved if there were better ways to annotate closures.

1st Attempt:

pub fn noop_fold_expr<F: Folder>(folder: &mut F, Expr { node, attrs }: Expr) -> Expr {
  let fold_expr = |e| folder.fold_expr(e); 
// ...

obviously cannot possibly work, because the closure uniquely borrows folder, which means it cannot be used anywhere else.

2nd Attempt

pub fn noop_fold_expr<F: Folder>(folder: &mut F, Expr { node, attrs }: Expr) -> Expr {
  let fold_expr = |f| |e| f.fold_expr(e); 
  // ...
  something.lift(fold_expr(folder));
   

this should actually work for my case, but the compiler does not know it is safe. The inner closure captures the environment of the outer closure, and although that environment contains just a reference to the function scope, the inner closure does not “capture” its life-time. If I were able to somehow annotate the lifetimes:

3rd hypothetical attempt

pub fn noop_fold_expr<F: Folder>(folder: &mut F, Expr { node, attrs }: Expr) -> Expr {
  let fold_expr = <'a>|&'a mut f| -> 'a impl FnMut(Expr) -> Expr { |e| f.fold_expr(e) }; 
  // ...
  something.lift(fold_expr(folder));   

the compiler would know that it is safe to return that closure with its captured environment and the places in which it’s safe to use it (e.g. I cannot return it out of the fn noop_fold_expr without further annotations).

4th Attempt

Just want to mention it briefly but if fn_traits were stable, I could probably mock the closure with a struct, implement a call operator for it, and get it to work.

These are just two examples of some of the pain I’ve had with closures and high-order functions in Rust. Without the ability to add life-time annotations, mention type-parameters, use where clauses, impl Trait, … they feel a bit like second citizens within Rust. In other situations in which the borrowck fails, one can make extra guarantees (by adding annotations) and issues can be worked around, but with closures, there aren’t currently many workarounds possible. I’ve seen some other usability issues mentioned before (NLL, auto-dereferencing in matches/removing refs, …) but I haven’t seen many issues about closures which I use a lot to remove repetitive code (I prefer them over macros).

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.