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

Distinguish closures from callables #3300

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
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
223 changes: 223 additions & 0 deletions text/0000-struct-closure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
- Feature Name: `struct_closure`
- Start Date: 2022-08-05
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Concrete Closure Types

# Motivation
[motivation]: #motivation

This proposal exists entirely out of spite for the `char::is_ascii*` family of
functions.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Closures in Rust are represented by the struct `core::ops::Closure`, while
Copy link
Member

@shepmaster shepmaster Aug 8, 2022

Choose a reason for hiding this comment

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

I do not see such a struct. Can you please link to it? Or are you proposing that this become what happens?

Copy link
Author

@SoniEx2 SoniEx2 Aug 8, 2022

Choose a reason for hiding this comment

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

The RFC template says to write here "as if [the proposal] was already included in the language". This is that.

Tho we certainly may have taken that too literally.

callables are represented by the traits `Fn`, `FnMut` and `FnOnce`. This
enables closures to participate specially in trait resolution, and be
distinguished from other types of callables (particularly user-defined ones).

In general, when writing generic code, it is preferred to use the `Fn*` traits.
However, in some edge-cases it can be useful to use `Closure` or even both.

## Examples

The main user of the `Closure` struct is `Pattern`:

```rust
fn takes_char(x: char) -> bool {
true
}

fn takes_char_ref(x: &char) -> bool {
true
}

// these both work:
"hello".trim_start_matches(takes_char);
"hello".trim_start_matches(takes_char_ref);
```

This wouldn't be possible if `Pattern` were instead relying on the traits,
as it's entirely possible for a type to implement multiple `Fn` traits, but a
closure will never do so.

Due to historical reasons, `char` provides some functions that take `&self` and
some which take `self`:

```rust
char::is_ascii(&'a');
char::is_numeric('9');
```

and they both work as `Pattern`, thanks to `Closure`:

```rust
"hello".trim_start_matches(char::is_ascii);
"hello".trim_start_matches(char::is_numeric);
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Currently the `Fn*` traits are pretty unstable, including their internal
representation. This proposal simply introduces a new, special lang item:

```rust
#[rustc_paren_sugar]
#[lang="closure"]
struct Closure<F: FnOnce<Args>, Args> {
// compiler special...
}

impl<F, Args> Fn<Args> for Closure<F, Args> where F: Fn<Args> {
// details omitted...
}
impl<F, Args> FnMut<Args> for Closure<F, Args> where F: FnMut<Args> {
// details omitted...
}
impl<F, Args> FnOnce<Args> for Closure<F, Args> where F: FnOnce<Args> {
// details omitted...
}
```

Some things to note: The details of `Args` are subject to change. This proposal
relies on that, because otherwise this cannot have the correct `for<'a>`
bounds. This proposal does not define the details of `Args`, but suggests they
might be of `fn` type. (Indeed, it would be easiest to implement this by
defining `Args` to be an `fn` type. Tho do note the `Args` in `Closure` does
not need to be the same `Args` in `Fn*`, as long as the compiler knows how to
map between them.)

In general, it'd be awkward to use this in a function. Consider:

```rust
fn foo<F: Fn...>(closure: Closure<F, ...>)
```

It's much simpler to just use F directly. But in a trait, things are different,
because you can't have multiple generic impls like that, so it actually becomes
useful:

```rust
// this DOES NOT WORK
impl<F: FnMut(char) -> bool> Pattern for F { ... }
impl<F: FnMut(&char) -> bool> Pattern for F { ... }

// but this would, because these types don't overlap
impl<F: FnMut(char) -> bool> Pattern for Closure<F, fn(char) -> bool> { ... }
impl<F: FnMut(&char) -> bool> Pattern for Closure<F, fn(&char) -> bool> { ... }
```

This is, for all intents and purposes, a "zero-cost" wrapper over
closures (except for one layer of type system nesting); and unlike `fn` types,
it doesn't incur the additional pointer overhead, being the same size as the
underlying closure.

Additionally, as this is the main goal of the RFC, we add the following impl to
`Pattern`:

```rust
impl<'a, F> Pattern<'a> for Closure<F, fn(&char) -> bool>
where
F: FnMut(&char) -> bool,
{
pattern_methods!(CharRefPredicateSearcher<'a, F>, MultiCharEqPattern, CharRefPredicateSearcher);
}
```

Which, for all intents and purposes, is the same as the existing generic impl,
except with `&char` instead of `char`.

# Drawbacks
[drawbacks]: #drawbacks

- Small extra cost due to additional wrapper type. This could affect existing
users of closures with longer compilation times, but it shouldn't affect
runtime performance.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

This proposal intentionally distinguishes closures from other kinds of
callables, solely to fix the issue with our beloved `char::is_ascii*` family of
functions. Given that the sole reason for this proposal is to fix that issue,
there's actually a huge range of possible alternatives. These are a few
examples:

- We might be able to `impl Pattern for fn(&char) -> bool`. This does raise the
question of whether we *want* non-closure types which happen to be callable
with a bare `char` and return a `bool` to also be usable as a `Pattern`, or
only "true" closures. It also raises the question of whether we want to
accept closures which take `&char`, or only functions. This has not been
discussed previously. See unresolved questions.

This proposal rejects this alternative by principle: If `Pattern` works on
`(char) -> bool` closures (*including* `(char) -> bool` functions), and
`(&char) -> bool` functions, it creates an inconsistency where
`(&char) -> bool` closures don't work for seemingly no good reason.
- There have been past discussions about potential "overlapping impls" as a way
of solving this, but they never got very far and were quite underspecified.
This proposal is much narrower in scope, so it shouldn't have those issues.
- Adding `std::char::is_ascii*` which take `char` is fragile at best.

# Prior art
[prior-art]: #prior-art

Tentatively, the way function pointers work today is the prior art.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

1. This proposal raises the question of whether we want non-closure callables
to be `Pattern`. Note that, today, it is not possible to specialize
`Pattern` for such callables.

This can be resolved before stabilization: this proposal allows either
option to be chosen, e.g.:

```rust
// any (char) -> bool callable
impl<F: FnMut(char) -> bool> Pattern for F { ... }
// only (&char) -> bool closures
impl<F: FnMut(&char) -> bool> Pattern for Closure<F, fn(&char) -> bool> { ... }
```

or even with specialization:

```rust
// any (char) -> bool callable (specializable)
impl<F: FnMut(char) -> bool> Pattern for F { ... }
// only (char) -> bool closures
impl<F: FnMut(char) -> bool> Pattern for Closure<F, fn(char) -> bool> { ... }
// only (&char) -> bool closures
impl<F: FnMut(&char) -> bool> Pattern for Closure<F, fn(&char) -> bool> { ... }
```
2. This proposal changes type inference for closures. Specifically, adding
`impl<F: Fn(&char) -> bool> Pattern for Closure<F, fn(&char)>` would break
inference for existing users of `str.matches(|x| x.whatever)`, tho this is
not that big of a deal, as the edition machinery can handle this today, in
a similar way to how it can handle `IntoIterator` for arrays. But the
unresolved question is whether we want to define new stability rules for
trait impls on closures.

Today, Rust mostly considers new trait impls to be non-breaking, but with
workarounds for where such impls cause major ecosystem breakage (arrays).
Should new impls for Closure where there was once only one be considered a
breaking, must-never-do change, such as in the case of `Pattern`, or should
the user be responsible for annotating the closure? This should be resolved
before merging this RFC. (Or, indeed, this RFC should be rejected if any of
this is unacceptable - after all, the proposal sets out to solve `Pattern`
for the `char::is_ascii*` family of functions, and this would prevent
that.)
3. The `Args`. Just like for `Fn*`, this must be resolved before stabilization.

# Future possibilities
[future-possibilities]: #future-possibilities

N/A.