Announcing the birth of my new crate namable_closures


#1

Hi all,

I have just created a new crate namable_closures This will solve some problems we have with closures right now, without requiring any further language supports:

  • Not able to name a family of “compatible” closures. For example |a| a+captured should be compatible with |a| a-captured if captured is the same type, but in fact they don’t have the same type so we cannot put them in the same position.
  • Traits are unsized, and the FnXXX trits are the only thing we know about a specific closure, this makes it hard to put closures in containers that requires its contents to be Sized.
  • The default closure behavour is to capture all variables appear in the body, and there is no way to control what to be captured.

This crate resolve the above by introducing some types, and use macros to simplify usage. See the following example:

#[macro_use] extern crate namable_closures;
use namable_closures::Closure;
fn main() {
    let add_ten:Closure<i32,(i32,),i32> = closure!(state, i,  i+*state, 10);
    println!("{}",add_ten(1)); //11
}

In the above example the closure have a Sized type that can be used later. The type Closure<i32,(i32,),i32> means:

  • When called, the state is called by reference
  • The type of the state is i32 (the first type parameter)
  • It accepts a single parameter typed i32 (the singleton tuple)
  • The return type is i32 (the last type parameter)

The macro closure! accepts 4 pieces:

  • A variable name for the state
  • A tupple pattern or single variable name for the input argument
  • The body of the closure, can use any variable in the state or arguments, but not from the environment
  • An expression to create the (initial) state value. If you have any variable to be captured from the environment, put them here

There are other variants depends on the use case. Check the documents for more information.

Feedback

Please enter the repository and raise an issue if you have anything to tell. Code reviews, advises, features requests and most importantly, bug reports are appreciated.

Future work plans

  • Improve the macros
  • Improve documentation
  • Add recursion support. Use similar technologies this is possible, but I have to find a way to access this feature from the function body easily.

Namable-closures 0.2.1
#2

It would probably be nice to support using regular closure syntax. Something like…

closure! {
    let state = &10;
    move |i| { i + *state }
}

Note that without the closure! part, this is exactly how you’d spell a closure with explicit captures today. I’m not exactly certain what your ownership semantics are, but finding a syntax that works similarly to this will make the crate much easier to use.


#3

I applaud the extremely simple and straightforward macros used here. This is a very pragmatic approach that still accounts for the various ownership requirements rather than just giving up and always implementing Fn.

For me, these closures still aren’t useful unless they themselves can be generic. This was one of the first big troubles I had with rust, and I addressed it with my own DSL-ly alternative here, a crate not to be taken very seriously.
I no longer use it because I lack confidence that the Fn traits will ever be stabilized anytime in the next several years, and that same problem affects this crate.


#4

First of all, the full sementic is not easy to express in regular closure syntax. Your example is an approximation, but the main difference is that your code only moves a reference to the closure, not the value itself, whilst my Closure struct do own the value.

Second, I am new to the Rust macro system, and have no idea how to do something like you said. So this is why I need help on advises.


#5

I am not sure what do you mean by “be generic”. If you mean the closure types, it is already as generic as I can. If you mean I should use a single type rather than multiple types this would be hard but worth to try.

Your unbox macro is very interesting but it looks too ambitious to me. Rather than defining a mini-language, I am just focusing on the key problem to resolve: make a closure type namable.

Anyways, your crate opens a door to me to examine the advanced macro skill, so I could proberly use it to solve @CAD97‘s request: make the grammar better.

Yes, this crate also suffers the same problem of your crate: we have to wait for those unstable traits, and I don’t have a good solution for.


#6

I’ve written a dozen instances of the following pattern recently

fn dfs(...) { 
    return go(root, lots of pass through context);
    fn go(node, lots of pass through context) { ... }
}

Really look forward to recursion support! :slight_smile:


#7

I see, you solved the problem of type parameters by having a few shared types rather than defining one for each closure. Yes, you’re right, these are generic.

They still cannot represent a for<'a> Fn(&'a T) however, If I am not mistaken.

My crate is a poor example of advanced macro techniques; I basically independently came up with a technique similar to TT munching, but without knowing what I was doing, and it just felt a huge mess that was brewed in a Turing tarpit.

Information on many good techniques can be found in DanielKeep’s book, and for a cleaner example of the technique I used, you can try looking at xfix’s extension trait, which tries to parse syntax like

<'a, T: 'a> MyNewTrait<'a> for OtherStruct<T> {
    fn foo(self) { }
}

#8

Thanks for your interest. Right now I am working on making better grammar. Recursion will be the next pirority.

The grammar I am working on right now would looks like this:

let add_ten:Closure<i32,(i32,),i32>
    = closure!({*state=10 => move |i| i+*state });
let add_ten:ClosureRef<i32,(i32,),i32>
    = closure!({&state=&state => |i| i+*state});
let mut accumulate:ClosureMut<i32,(i32,),i32>
    = closure!({*mut state=0 => move |c| {*state+=c;*state}});
let mut match_cnt:ClosureRefMut<i32,(i32,i32),()>
      = closure!({&mut state=&mut state => |(a,b)| {if a==b { *state+=1 }}});
let sign_on:ClosureOnce<Passwd,(String,),Result<(),io::Error>>
    = closure!({passwd=Passwd::get_from_cache() =>
        move |user| authenticate(user,passwd)});

How about this?


#9

I am not sure what do you want. Maybe you mean ClosureRef and ClosureRefMut? They do have lifetime parameters.


#10

Consider the signature of Iterator::filter. Desugared, it has the bound

where P: for<'a> FnMut(&'a Self::Item) -> bool

I do not believe your closures are capable of being used in a type alias for a Filter. For this to work, you need a lifetime that appears in the impl (and the argument list), but not in the type.


#11

I find this confusing:

  • Why is everything in one big brace?
  • Can you not use ident instead of *ident?

#12

Like this?

let add_ten:Closure<i32,(i32,),i32>
    = closure!(state= ref 10 => move |i| i+*state);
let add_ten:ClosureRef<i32,(i32,),i32>
    = closure!(&state=&state => |i| i+*state};
let mut accumulate:ClosureMut<i32,(i32,),i32>
    = closure!(mut state=ref 0 => move |c| {*state+=c;*state});
let mut match_cnt:ClosureRefMut<i32,(i32,i32),()>
      = closure!(&mut state=&mut state => |(a,b)| {if a==b { *state+=1 }});
let sign_on:ClosureOnce<Passwd,(String,),Result<(),io::Error>>
    = closure!(passwd=Passwd::get_from_cache() =>
        move |user| authenticate(user,passwd));

#13

I believe the opposite as my design is just a copy of the current closure design of the language. After desugared they should be the same as my definitions. In your case, it looks like either ClosureMut or ClosureRefMut should do the job.

I am not able to confirm right now. Will try when I have time.


#14

Hmm, now I’m even more confused by why there are refs, or what &state = &state means.

As an observation: Even just to add these few small features, you are already on track to having your own mini-language. Mind, unbox-macro was never originally intended to be so complicated. It became complicated after I kept repeatedly discovering how useless it was without certain features.

Keep me posted! :wink:


#15

Those features are intended to express what the current regular closure system does for us implicitly.

  • the ref in the right hand side means the closure do own the value (so a move must exist, but the state variable is only a reference in the body.) The main concern is we cannot just move a reference to the closure. However, to make it clear we can put it in the left hand side.
  • the &state=&state grammar is better replace with ref state=&state, in which the right hand side need to be a reference (includes state.as_ref() etc), and the left hand side is a reference, This is a bit redundant as we can tell when there is no move keyword appear.

So, considering all those, the grammar would looks like:

//ref + move => ClosureRef (Fn, FnMut, FnOnce)
let add_ten:ClosureRef<i32,(i32,),i32>
    = closure!(ref state= 10 => move |i| i+*state);
//no keywords => Closure (Fn, FnMut, FnOnce)
let add_ten:Closure<i32,(i32,),i32>
    = closure!(state=&state => |i| i+*state};
//ref mut + move => ClosureRefMut (FnMut, FnOnce)
let mut accumulate:ClosureRefMut<i32,(i32,),i32>
    = closure!(ref mut state=0 => move |c| {*state+=c;*state});
//mut => ClosureMut (FnMut, FnOnce, !Copy, !Clone)
let mut match_cnt:ClosureMut<i32,(i32,i32),()>
      = closure!(mut state=&mut state => |(a,b)| {if a==b { *state+=1 }});
//move => ClosureOnce (FnOnce)
let sign_on:ClosureOnce<Passwd,(String,),Result<(),io::Error>>
    = closure!(passwd=Passwd::get_from_cache() =>
        move |user| authenticate(user,passwd));

It would well be the case that this will still lead to a mini language, but my initial intention is to create those types, not the macros (as you can see I am still learning the macro system). So even we don’t use the macros, they can still be part of the program and be used. So the design of my macros are to follow the types, not the other way round,

Even with the concern that the fn_traits feature might not be stabilized in near future, I would consider defining grammars that works today so we can go ahead. After all an ugly grammar would not stop us, lacking features would.

About the input type lifetime issue, I think you are right although I have not tried yet. In Rust I don’t know how can we express “a principle type of a type have associated lifetime”, which will be needed to express what you expected. I think there were an RFC out there for “the minimal lifetime” which may resolve it. But I have no idea where it is.

But it does not mean those closures are useless. At least they are good enough for Futures: they hate lifetimes so things will work better there.

Updated: the example was updated for consistency between the type name and usage.


#16

The above proposal shows that the naming of the types are not consistent with the grammar. So I would make some change:

  • Closure becomes ClosureRef as they need ref keyword
  • ClosureRef becomes Closure and do not need any keyword
  • ClosureMut becomes ClosureRefMut and require ref mut keywords
  • ClosureRefMut becomes ClosureMut and require only mut keyword
  • ClosureOnce remain unchanged, and require move keyword

In addition to that, the appear of ref requires move to appear, because it means the variable refer to an owned value.