Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unboxed closures #77

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions active/0000-closure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
- Start Date: 2014-05-15
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)

# Summary

The lambda syntax should produce a uniquely typed callable object based on the captures.

# Motivation

Rust's closures should be replaced with a more flexible design acting as sugar for the semantics
elsewhere in the language. It should be possible to capture by-value, along with moving around or
even returning the resulting closure type. Closures should not imply indirect calls, but rather
provide the same choice between static and dynamic already provided by the choice between trait
bounds on generics and trait objects. This feature is provided as part of C++11, and C++14
introduces the necessary support for returning closures.

# Drawbacks

This will require a significant amount of implementation work. The change to by-value capture
semantics will be backwards incompatible, requiring a significant amount of code to be ported.

# Detailed design

This feature would be implemented as sugar over existing ones, so the design is quite simple.

Consider the following closure definition:

```rust
let x = 5; |a, b| a * x + b + x
```

This will be expanded to an instance of a unique type, essentially acting as the following type
definition and construction, but with an unspeakable anonymous type and inaccessible fields:

```rust
struct UniqueClosure_$uniqueid {
x: int
}

impl |int, int| -> int for UniqueClosure_$uniqueid {
fn call(&mut self, a: int, b: int) -> int {
a * self.x + b + self.x
}
}

UniqueClosure_$uniqueid { x: x }
```

The captures will be performed by-value, so non-`Copy` types would be moved into the closure by
default. Since references and mutable references are values, closure expressions do not need any
extra complexity. The resulting type will be `Send` if all the captures are `Send` along with having
the other built-in traits as appropriate just as a normal `struct` definition would.

The current borrowed closures will be directly replaced by the borrowed trait object syntax: `&mut
|u32, bool| -> f64`. It should be possible for any user-defined type to implement this special
trait.

Capturing a mutable reference (`&mut T`) will be special-cased to perform a reborrow (`&mut *x`) in
order to make closures more flexible under the current type system.

# Alternatives

An alternative is leaving the by-reference capture semantics, preventing closures from being
returned if local state is captured. The main motivation behind this proposal is to allow these
patterns, so lack of by-value capture cannot be seriously considered. It would also lead down the
path of closures being more than just sugar, so the existing struggle with closure-specific
soundness and code generation issues would continue.

Variadic generics would remove the need for the trait to be special, but the syntactic sugar will
always be desirable. Closures will exist only to provide sugar, and it makes sense to provide type
sugar in addition to the expression sugar. It would simply be replacing the existing type syntax for
closures, so it has a low cost and high return.

## Syntactic sugar for by-reference and by-mutable-reference captures

The need to capture by reference is uncommon, as shown by an analysis of the existing code. Since
references are first-class values, no special syntax is required. However, it would be possible to
add sugar in the future.

The `ref` and `ref mut` markers are already used in patterns to destructure by-reference, and
re-using these for closure captures would be unambiguous.

Consider the example above again, this time with a by-reference capture:

```rust
let x = 5; |a, b| a * x + b + *(ref x)
```

This would be sugar for the following snippet:

```rust
let x = 5; { let ref_x = &x; |a, b| a * x + b + *ref_x }
```

Capturing the same variable with a different capture strategy would just introduce a new field
inside the closure object to store it. Since this can be defined in terms of existing features, it
will eliminate the need to treat closure captures as special cases.

The usage of `ref` and `ref mut` has the virtue of being unambiguous, introducing no extra keywords
and avoiding a noisy capture list.

# Unresolved questions

It will be possible to return these closures from other closures. However, regular functions
currently require a concrete type signature so this proposal alone is not enough to return them. The
restriction on functions could be relaxed to permit the return type to be an anonymous type
implementing a trait, or in other words an "unboxed trait object". This is future work for another
proposal.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, couldn't this be done without unboxed trait objects? A function that returns a closure isn't referring to an unboxed trait object but instead referring to a specific anonymous struct. The function's implementation would then use this anonymous struct as the type for whatever closure is actually returned. The two big downsides I could see are:

  1. You can't conditionally pick from several closures to return, and doing so would probably cause a very confusing error (because only one can actually be the correct type).
  2. I'm not sure how this would work if the function is generic. Does the anonymous struct then become generic too, using the same type parameters as the function? What if the function isn't generic, but it's part of a generic impl? This seems potentially problematic.

I'm not suggesting this is the way to go. Unboxed trait objects seem like a better solution. I just wanted to mention it as a potential alternative.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just using unboxed trait objects as jargon to refer to an anonymous type implementing some trait. Implementing it with sane error messages seems like it would go a long way to implementing this as a general purpose feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering my suggestion to actually be the inverse of rust-lang/rust#11455. In that proposal, a function returning an unboxed Iterator would actually return the concrete type of whatever iterator the implementation returns. I was thinking of this as the opposite, the return type would define the anonymous type and the closure in the implementation would be forced to be that same type.

But it turns out that's rather unnecessary (and overly complicated, because the function doesn't know what upvars there will be so it can't actually fully define the type). Because treating it the exact same way that the Iterator case is works just as well. So forget I suggested anything.


The current generics syntax makes passing an unboxed closure uglier than the current closures. This
could be improved by building on the "unboxed trait object" concept introduced above and allowing
the following:

```rust
fn foo(x: |u64, u64| -> u64) { ... }
```

However, this is also out of the scope of this proposal.

It might be possible to fit `proc` into this system by making use of `self` and `~self`, but the
regular `call` method can not safely move out of the captured environment. A separate trait would be
required, perhaps using the reserved `once` keyword as a prefix to the closure type sugar. It could
also be done via a future implementation of variadic generics without the sugar, but it would be
significantly uglier and would still be a magical lang item.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really different enough that it deserves to be deferred for a future RFC? Because if you're just looking to avoid a syntax bikeshed...

spawn(proc(x) { ... });  // old
spawn(once |x| { ... });  // new

...then IMO this looks like it would be an improvement. Still not quite as pretty as the old do spawn, but I was never comfortable with how non-closurelike the proc syntax looks. :P