Skip to content
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
68 changes: 68 additions & 0 deletions active/0000-unboxed-closures-detail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
- Start Date: 2014-05-28
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)

# Summary

Unboxed closures should be implemented with three traits (`Fn`, `FnMut`, and `FnOnce`), and there should be a leading sigil (`&:`/`&mut:`/`:`) before the argument list so the programmer can describe which one is meant.

# Motivation

This RFC simply addresses some points that were not ironed out in the previous unboxed closure RFC.

# Detailed design

This builds on RFC #77 "unboxed closures"; see the design for that.

There should be three traits as lang items:

#[lang="fn"]
pub trait Fn<A,R> {
fn call_fn(&self, args: A) -> R;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since these are builtin/generated by the compiler, is there any particular reason to not just use call and call_mut (which are more consistent).

Copy link
Contributor

Choose a reason for hiding this comment

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

Would these autogenerated function names ever appear in error messages, or otherwise be visible to the user?

}

#[lang="fn_mut"]
pub trait FnMut<A,R> {
fn call(&mut self, args: A) -> R;
}

#[lang="fn_once"]
pub trait FnOnce<A,R> {
fn call_once(self, args: A) -> R;
}

The unboxed closure literal form `|a, b| a + b` creates an anonymous structure implementing one of the above three traits. Accordingly, we introduce new syntaxes for unboxed closures to correspond to the three traits above:

let f: |&: a, b| a + b; // implements `Fn`
let g: |&mut: a, b| a + b; // implements `FnMut`
let h: |: a, b| a + b; // implements `FnOnce`
Copy link

Choose a reason for hiding this comment

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

I'd like to suggest using this sugar at the type level as well. Personally, I find |&mut: int, int| -> int to be a much clearer type signature than FnMut<(int,int),int>.

Copy link
Contributor

Choose a reason for hiding this comment

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

They should be let f = |&: a, b| a + b; and so on.

Copy link

Choose a reason for hiding this comment

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

@anasazi I agree, FnMut<(int,int),int> is not very appealing.


Once boxed closures are removed, the regular `|a, b| a + b` syntax will be an alias for `|&mut: a, b| a + b`, since that is the commonest trait to implement.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have any measurements to reinforce this claim? Your datasets in the prior RFC have spoiled us. :)


The idea behind the syntax is that what goes before the `:` mirrors what goes before `self` in the `call`/`call_fn`/`call_once` function signature. This syntax avoids introducing any new keywords to the language.

The call operator `x(y, z)` will desugar to one of `x.Fn::call_fn((y, z))`, `x.FnMut::call((y, z))`, and `x.FnOnce::call_once((y, z))`, depending on the trait that `x` implements. If `x` implements more than one of `Fn`/`FnMut`/`FnOnce`, then the compiler reports an error and the `x(y, z)` form cannot be used.
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that only one trait can be implemented, is there a reason to have separate names call(), call_fn(), and call_once()? All 3 traits could define the same method call().

Copy link
Contributor

Choose a reason for hiding this comment

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

I interpreted this paragraph as saying that the compiler will report an error on the use of the call operator x(y, z) syntax, not that attempting to implement more than one of the traits on a single type was illegal.

In any case, I see value in using distinct names for the methods. The only reason I could see not to do so would be to ease designing certain macros, if our macros were able to expand into method-items (which I believe they currently cannot).


We will remove `proc(A...) -> R` and replace with `Box<FnOnce<(A...),R>>`.

# Drawbacks

* The syntax may be ugly.

* It may be that `Fn` and `FnOnce` are too much complexity.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know about Fn, but I've wanted FnOnce for stack closures for a long time. I'm excited that unboxed closures are introducing it. Tying once-ness to heap allocation (as proc does) was always weird, as the two should be orthogonal concepts.

Copy link
Member

Choose a reason for hiding this comment

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

Keep in mind that once || never went away, and it's relatively usable, if a type hint is present at the closure expression (like expect_once(|| move_out_of(a_capture)) where fn expect_once(f: once ||) {...}).


* Tupling the arguments may have ABI impacts, although I researched this on ARM-EABI and x86 and did not find any.

* Because of argument tupling, we lose the ability to pass DSTs by value, which has been proposed in the past.

# Alternatives

The impact of not doing this at all is that the precise trait that unboxed closures implement will be undefined, and we will continue to have `proc`.

An alternative to tupling arguments is to introduce variadic generics, but that seems like a lot of complexity.
Copy link
Contributor

Choose a reason for hiding this comment

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

#77's proposed trait implementation looked like

impl |int, int| -> int for Foo {
    fn call(&mut self, a: int, b: int) -> int { ... }
}

It wasn't clearly stated, but the idea here is that this is effectively sugar for a trait like FnMut2<int, int, int>.

The implication here is that the function calling traits are actually a family of traits synthesized by the compiler (one for each argument count), rather than 3 specific lang item traits defined in libcore.

Despite being very unconventional, this approach allows for omitting the tupling, and would not prevent passing DSTs by value.

Has this approach been seriously considered and rejected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A family of traits will cause weirdness around name resolution and also complicates the implementation and semantics considerably. I would much rather see if tupling causes any actual problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that tupling is conceptually much nicer. I brought this up largely because of the DST by-value issue, although I don't actually know how passing DST by-value would even work (I have to assume it would really be a fat pointer under the hood, but then why not just pass &T).

Copy link
Contributor

Choose a reason for hiding this comment

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

Variadic generics would allow passing DSTs by-value, yes? Assuming a hypothetical future Rust, this could justify changing the behind-the-scenes implementation. Would such a change be backwards-compatible, or would the details of argument tupling leak out anywhere?

Copy link
Member

Choose a reason for hiding this comment

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

There wouldn't be a family of traits. You would have the same traits, but variadic under the hood - you wouldn't be able to define and name them, but the closure type grammar is enough for referring to them.

Tupling is a serious issue, you need devirtualization, inlining and SROA to happen in order to remove the overhead, so any remaining virtual closure calls (or even unboxed closure calls that don't inline) will have to read all their arguments from behind a pointer, instead of registers or direct stack offsets.

Right now this also causes the loss of argument attributes, so it would prevent certain optimizations in all closures, before we get TBAA.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like we could have the calling convention handle fn foo((x,y): (A, B)) and fn foo(x: A, y: B) identically (theoretically).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eddyb That's not how calling conventions work. On all ABIs we care about, the calling conventions allow struct components to be passed in registers.

Also, I don't see what you mean by devirtualization being necessary. This proposal reduces the need for devirtualization.

The LLVM attribute issue is a valid one, but this should be fixed in LLVM. The logical conclusion of working around this limitation in the language design leads to absurdities such as changing APIs that would take Points to take two values, removing OO-style "self" structs, and whatnot.

Actually, come to think of it, I could do the untupling at the caller side rather than the callee side. This means that in, most cases, the tupling need not be done at all, as there is no reason do it if calling the closure with the () syntax. I believe this is possible because trans always knows when it's about to invoke any call/call_mut/call_only function and can treat them specially if it wants to. This would bring the overhead down to zero in most cases.


# Unresolved questions

It remains to be seen how this interacts with not being able to use "for-all" quantifiers in trait objects. This will break some code until/unless we introduce this capability. How much is unknown.
Copy link
Member

Choose a reason for hiding this comment

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

What does this mean exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can't use a trait as a region binding site. <'a>Trait<'a> doesn't work.


ABI issues relating to tupling struct arguments on uncommon architectures like MIPS and non-EABI ARM have been inadequately explored.