[rust-dev] RFC: Future-proof for unboxed closures

Gábor Lehel glaebhoerl at gmail.com
Sat Jan 25 10:07:32 PST 2014


I've been thinking about this on and off.


On Sat, Jan 25, 2014 at 6:31 AM, Niko Matsakis <niko at alum.mit.edu> wrote:

> This is good to think about, though I think writing `&mut || foo`
> feels like a nonstarter to me.
>

I agree.


>
> I still feel that `&my` -- meaning, a pointer that gives ownership of
> the referent but not the memory where the referent lives -- is the
> right approach here. Basically, the type of `|| ...` would be `&my T`
> where `T` is some fresh type that implements `Fn<U,R>`.
>

I prefer the name `&move`: it's a natural progression on `&mut`, which
confers permission to mut[ate], while `&move` also confers permission to
move. When creating one, I think `&move some_foo` (move out of my_foo) is
also clearer than `&my some_foo`. Plus it feels weird for the stronger one
to have a shorter name.


>
> Now, the consumer of the closure could be any of:
>
> - `fn foo(x: |U| -> R)`: In this case, the type `|U| -> R`
>   is equivalent to `&mut Fn<U,R>`. This is an automatic
>   object coercion and also an automatic reborrow. That is,
>   written out very explicitly, `foo(|u| r)` would be
>   equivalen to `foo((&mut *(&my T { ... })) as &mut Fn<U,R>)`,
>   where again `T` is the anonymous closure type.
>
> - `fn foo<T:Fn<U,R>>(x: &my T) -> T`: Now `foo` takes ownership of the
>   value `T`. Because it's an `&my` pointer, `foo()` can move the
>   referent around.
>
> - `fn foo<T:Fn<U,R>>(x: &mut T) -> T`: `foo` does not take ownership
>   of the closure, but does avoid virtual dispatch.
>

If you wanted to pass an unboxed closure without indirection though, like
`fn foo<T: Fn<U, R>>(x: T)`, then you would have to explicitly dereference
the closure, i.e. `foo(*|u| r)` (which might be OK).


>
> This can be extended to "once closures" in a pretty straightforward
> way:
>
> - `fn foo(x: once |U| -> R)`
> - `fn foo<T:OnceFn<U,R>>(x: &my T)`
>

The plan I had been thinking of is similar in some ways, different in
others, and likely has different advantages and drawbacks.

The basic problem is that there's a zoo of closure types:
http://glaebhoerl.tumblr.com/rust_closure_types (I will be referring to
things from this in the following!)
and it's difficult to have convenient and consistent syntax for all of them
at the same time. There's also several different areas of the language
which we want to all work ergonomically: writing closures, invoking
closures, using higher-order functions, and writing the types of closures.
I'll go through these in turn.

Anonymous closure literals, `|args| foo`: I was thinking these would
represent an unboxed closure. That seems like the most straightforward
thing (and the same as C++). This raises two questions:

 - Which traits does it implement? Having to explicitly annotate it somehow
would be onerous, so I think this should be inferred from the way the body
of the closure uses captured variables. What's not totally clear to me is
how to handle the case where you have an `&move FnOnce`, but only some of
the captured variables are moved out of by the closure body: presumably the
programmer would expect the remainder to remain available afterwards.

 - If a HOF expects a stack closure, would you have to write e.g. `&mut
|args| foo`? Again, this feels onerous. On the other hand, when making a
heap closure, having to write e.g. `~|args| foo` explicitly is actually
desirable. The least bad solution I can think of is that anonymous closures
would auto-borrow to `&`, `&mut`, or `&move`.

Invoking a closure also raises questions: does `foo()` imply using `call()`
from `Fn`, `call_mut()` from `FnMut`, or `call_once()` from `FnOnce`? Once
again the least bad solution I can think of is that there would be a bit of
magic, and it would select the "best" option (the one least restrictive on
the caller) from the ones in scope (so Fn > FnMut > FnOnce).

Finally there's the question of how to write their types. The raw
trait-based syntax, `&mut FnMut<(int, int), int>`, is just awful. The
current approach the language takes is to have syntax sugar for a couple of
types (`&mut FnMut` is `|args| -> foo` and `~FnOnce` is `proc`), but this
leaves a lot of other equally legitimate types out in the cold. And if a
closure literal `|args| foo` denotes an unboxed closure, it would be weird
for the same thing at the type level to mean something different. On the
other hand, there's too many useful types to provide dedicated syntax for
all of them.

Here the least bad option I managed to think of is to introduce a new
syntax for writing generic types: `Foo(A, B) -> C` would be equivalent to
either `Foo<(A, B), C>` or `Foo<C, A, B>` depending on how variadics end up
working. I don't think this would introduce any ambiguities: parentheses in
types denote tuples, and it's not currently legal to follow an identifier
with a tuple type. (Correct me if I'm wrong.) This syntax would be
available both when declaring a generic type and when referring to one. (To
avoid confusion, we might want to require the same syntax be used
consistently for a given type, so that if you declare it with function-like
syntax, you also have to refer to it that way, and vice versa.) Then the
programmer can write decent-looking typedefs for whichever closure types
she will be using frequently. The standard library could provide a few
basic ones:

    // yes, it would also work for traits!
    trait Fn(Args...) -> Ret { ... }
    trait FnMut(Args...) -> Ret { ... }
    trait FnOnce(Args...) -> Ret { ... }

    // so `MutFn(int, int) -> int` is the new `|int, int| -> int`, a slight
downgrade, in exchange for much flexibility elsewhere.
    type MutFn(Args...) -> Ret = &mut FnMut(Args...) -> Ret;
    type OnceFn(Args...) -> Ret = &move FnOnce(Args...) -> Ret;

    // Would this still be important to have next to `OnceFn`? Anyway, it's
cute.
    type Proc(Args...) -> Ret = ~FnOnce(Args...) -> Ret;



>
>
> Niko
>
> On Mon, Dec 30, 2013 at 07:31:45PM -0800, Patrick Walton wrote:
> > Yes, it would need to be &mut, you're right.
> >
> > I think the underlying type syntax would be something like
> `Fn<int,&int>` for the unboxed version, and `&mut Fn<int,&int>` for the
> boxed version. The type syntax with the bars is just syntactic sugar for
> the latter (and, in trait bound position, for the former).
> >
> > It's somewhat unfortunate but I don't see a particularly good
> alternative if we want boxed and unboxed closures alike to have
> nice-looking APIs. The alternative, I guess, is to block 1.0 on unboxed
> closures, convert all our APIs to unboxed closures where possible, and just
> say that if you want a boxed closure you have to write `&mut |x| x + 1` at
> each closure construction site...
> >
> > Patrick
> >
> > "Gábor Lehel" <glaebhoerl at gmail.com> wrote:
> > >Wouldn't it have to be `&mut` rather than `&` to fit the semantics of |
> > >|,
> > >which is affine and can mutate its environment?
> > >
> > >And wouldn't this lead to divergence between the type- and value
> > >syntax,
> > >with | | as a type being a boxed closure (`&mut FnMut`), and an unboxed
> > >closure as a value? This was one of the nicer points of the recent
> > >closure
> > >overhaul, and it would be a shame to lose it so soon.
> > >
> > >
> > >On Mon, Dec 30, 2013 at 10:11 PM, Patrick Walton
> > ><pcwalton at mozilla.com>wrote:
> > >
> > >> I've been thinking that to future-proof unboxed closures in the
> > >future we
> > >> should maybe limit `|x| x+1` lambda syntax to either (a) require `&`
> > >in
> > >> front or (b) in function position.
> > >>
> > >> So today you would write:
> > >>
> > >>     let f = |x| x+1;
> > >>
> > >> But tomorrow you would write:
> > >>
> > >>     let f = &|x| x+1;
> > >>
> > >> But it would always work here:
> > >>
> > >>     v.map(|&x| x+1);
> > >>
> > >> The reason is simply that we'd like `|x| x+1` to become an unboxed
> > >closure
> > >> in the future and it's easier in the language semantics to
> > >future-proof for
> > >> it this way: we simply special-case the function argument position.
> > >>
> > >> Alternatively we can do it with assignability: say that `|x| x+1` is
> > >an
> > >> anonymous type (an error today) that is assignable to the type
> > >> `|int|->int`. That might be cleaner than special-casing the function
> > >> argument position.
> > >>
> > >> Patrick
> > >> _______________________________________________
> > >> Rust-dev mailing list
> > >> Rust-dev at mozilla.org
> > >> https://mail.mozilla.org/listinfo/rust-dev
> > >>
> >
> > --
> > Sent from my Android phone with K-9 Mail. Please excuse my brevity.
>
> > _______________________________________________
> > Rust-dev mailing list
> > Rust-dev at mozilla.org
> > https://mail.mozilla.org/listinfo/rust-dev
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/rust-dev/attachments/20140125/e75a6126/attachment-0001.html>


More information about the Rust-dev mailing list