Skip to content

Commit 7f5f35c

Browse files
committed
Add design notes for function-type Default implementation discussion
1 parent 8157ddc commit 7f5f35c

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
- [Design notes](./design_notes.md)
1111
- [Allowing integer literals like `1` to be inferred to floating point](./design_notes/int_literal_as_float.md)
1212
- [Generalizing coroutines](./design_notes/general_coroutines.md)
13+
- [Extending the capabilities of compiler-generated function types](./design_notes/fn_type_trait_impls.md)
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Extending the capabilities of compiler-generated function types
2+
3+
## Background
4+
5+
Both standalone functions and closures have unique compiler-generated types.
6+
The rest of this document will refer to both categories as simply "function
7+
types", and will use the phrase "function types without upvars" to refer to
8+
standalone functions _and_ closures without upvars.
9+
10+
Today, these function types have a small set of capabilities, which are
11+
exposed via trait implementations and implicit conversions.
12+
13+
- The `Fn`, `FnMut` and `FnOnce` traits are implemented based on the way
14+
in which upvars are used.
15+
16+
- `Copy` and `Clone` traits are implemented when all upvars implement the
17+
same trait (trivially true for function types without upvars).
18+
19+
- `auto` traits are implemented when all upvars implement the same trait.
20+
21+
- Function types without upvars have an implicit conversion to the
22+
corresponding _function pointer_ type.
23+
24+
## Motivation
25+
26+
There are several cases where it is necessary to write a [thunk]. A thunk
27+
is a (usually short) generic function that is used to adapt another function
28+
in some way.
29+
30+
Thunks have the caveat that they must be standalone functions. They cannot
31+
capture any environment, as it is often necessary to convert them into a
32+
function pointer.
33+
34+
Thunks are most commonly used by compilers themselves. For example, when a
35+
`dyn Trait` method is called, the corresponding vtable pointer might refer
36+
to a thunk rather than the original method in order to first down-cast
37+
the `self` type to a concrete type.
38+
39+
However, thunks can also be useful in low-level code that needs to interface
40+
with C libraries, or even in higher level libraries that can use thunks in
41+
order to simplify their public-facing API without incurring a performance
42+
penalty.
43+
44+
By expanding the capabilities of compiler-generated function types it would
45+
be possible to write thunks using only safe code.
46+
47+
[thunk]: https://en.wikipedia.org/wiki/Thunk
48+
49+
## History
50+
51+
Several mechanisms have been proposed to allow thunks to be written in safe
52+
code. These have been discussed at length in the following places.
53+
54+
PR adding `Default` implementation to function types:
55+
56+
- https://github.com/rust-lang/rust/pull/77688
57+
58+
Lang team triage meeting discussions:
59+
60+
- https://youtu.be/NDeAH3woda8?t=2224
61+
- https://youtu.be/64_cy5BayLo?t=2028
62+
- https://youtu.be/t3-tF6cRZWw?t=1186
63+
64+
## Implementing the `Default` trait
65+
66+
The solution initially proposed was to implement `Default` for function types
67+
without upvars. Safe thunks would be written like so:
68+
69+
```rust
70+
fn add_one_thunk<F: Fn(i32) + Default>(arg: i32) {
71+
let f = F::default();
72+
f(arg + 1);
73+
}
74+
```
75+
76+
Discussions of this design had a few central themes.
77+
78+
### When should `Default` be implemented?
79+
80+
Unlike `Clone`, it intuitively does not make sense for a closure to implement
81+
`Default` just because its upvars are themselves `Default`. A closure like
82+
the following might not expect to ever observe an ID of zero:
83+
84+
```rust
85+
fn do_thing() -> impl FnOnce() {
86+
let id: i32 = generate_id();
87+
|| {
88+
do_something_with_id(id)
89+
}
90+
}
91+
```
92+
93+
The closure may have certain pre-conditions on its upvars that are violated
94+
by code using the `Default` implementation. That said, if a function type has
95+
no upvars, then there are no pre-conditions to be violated.
96+
97+
The general consensus was that if function types are to implement `Default`,
98+
it should only be for those without upvars.
99+
100+
However, this point was also used as an argument against implementing
101+
`Default`: traits like `Clone` are implemented structurally based on the
102+
upvars, whereas this would be a deviation from that norm.
103+
104+
### Leaking details / weakening privacy concerns
105+
106+
Anyone who can observe a function type, and can also make use of the `Default`
107+
bound, would be able to safely call that function. The concern is that this
108+
may go against the intention of the function author, who did not explicitly
109+
opt-in to the `Default` trait implementation for their function type.
110+
111+
Points against this argument:
112+
113+
- We already leak this kind of capability with the `Clone` trait implementation.
114+
A function author may write a `FnOnce` closure and rely on it only being callable once. However, if the upvars are all `Clone` then the function itself can be
115+
cloned and called multiple times.
116+
117+
- It is difficult to construct practical examples of this happening. The leakage
118+
happens in the wrong direction (upstream) to be easily exploited whereas we
119+
usually care about what is public to downstream crates.
120+
121+
Without specialization, the `Default` bound would have to be explicitly listed
122+
which would then be readily visible to consumers of the upstream code.
123+
124+
- Features like `impl Trait` make it relatively easy to avoid leaking this
125+
capability when it's not wanted.
126+
127+
Points for this argument:
128+
129+
- The `Clone` trait requires an existing instance of the function in order to be
130+
exploited. The fact that the `Default` trait gives this capability to types
131+
directly makes it sufficiently different from `Clone` to warrant a different
132+
decision.
133+
134+
These discussions also raise the question of whether the `Clone` trait itself
135+
should be implemented automatically. It is convenient, but it leaves a very
136+
grey area concerning which traits ought to be implemented for compiler-generated
137+
types, and the most conservative option would be to require an opt-in for all
138+
traits beyond the basic `Fn` traits (in the case of function types).
139+
140+
### Unnatural-ness of using `Default` trait
141+
142+
Several people objected on the grounds that `Default` was the wrong trait,
143+
or that the resulting code seemed unnatural or confusing. This lead to
144+
proposals involving other traits which will be described in their own
145+
sections.
146+
147+
- Some people do not see `Default` as being equivalent to the
148+
default-constructible concept from C++, and instead see it as something
149+
more specialized.
150+
151+
To avoid putting words in people's mouths I'll quote @Mark-Simulacrum
152+
directly:
153+
154+
> I think the main reason I'm not a fan of adding a Default impl here is
155+
> because you (probably) would never actually use it really as a "default";
156+
> e.g. Vec::resize'ing with it is super unlikely. It's also not really a
157+
> Default but more just "the only value." Certainly the error message telling
158+
> me that Default is not implemented for &fn() {foo} is likely to be pretty
159+
> confusing since that does have a natural default too, like any pointer to
160+
> ZST). That's in some sense just more broadly true though.
161+
162+
- There were objections on the grounds that `Default` is not sufficient to
163+
guarantee _uniqueness_ of the function value. Code could be written today that
164+
exposes a public API with a `Default + Fn()` bound, expecting all types
165+
meeting that bound to have a single unique value.
166+
167+
If we expanded the set of types which could implement `Default + Fn()` (such
168+
as by stabilizing `Fn` trait implementations or by making more function
169+
types implement `Default`) then the assumptions of such code would be
170+
broken.
171+
172+
On the other hand, we really can't stop people from writing faulty code and
173+
this does not seem like a footgun people are going to accidentally use, in
174+
part because it's so obscure.
175+
176+
### New lang-item
177+
178+
This was a relatively minor consideration, but it is worth noting that this
179+
solution would require making `Default` a lang item.
180+
181+
## Safe transmute
182+
183+
This proposal was to materialize the closure using the machinery being
184+
added with the "safe transmute" RFC to transmute from the unit `()` type.
185+
186+
The details of how this would work in practice were not discussed in detail,
187+
but there were some salient points:
188+
189+
- This solves the "uniqueness" problem, in that ZSTs are by definition unique.
190+
- It does not help with the "privacy leakage" concerns.
191+
- It opens up a new can of worms relating to the fact that ZST closure types
192+
may still have upvars.
193+
- Several people expressed something along the lines of:
194+
195+
> if we were going to have a trait that allows this, it might as well be
196+
> Default, because telling people "no, you need the special default" doesn't
197+
> really help anything.
198+
199+
Or, that if it's possible to do this one way with safe code, it should be
200+
possible to do it in every way that makes sense.
201+
202+
## `Singleton` or `ZST` trait
203+
204+
New traits were proposed to avoid using `Default` to materialize the function
205+
values. The considerations here are mostly the same as for the "safe
206+
transmute" propsal. One note is that if we _were_ to add a `Singleton` trait,
207+
it would probably make sense for that trait to inherit from the `Default`
208+
trait anyway, and so a `Default` inmplementation now would be
209+
backwards-compatible.
210+
211+
## `FnStatic` trait
212+
213+
This would be a new addition to the set of `Fn` traits which would allow
214+
calling the function without any `self` argument at all. As the most
215+
restrictive (for the callee) and least restrictive (for the caller) it
216+
would sit at the bottom of the `Fn` trait hierarchy and inherit from `Fn`.
217+
218+
- Would be easy to understand for users already familiar with the `Fn` trait hierarchy.
219+
- Doesn't solve the problem of accidentally leaking capabilities.
220+
- Again raises the question: if you can call the function via `FnStatic`, then
221+
why not allow it via `Default + Fn` too?
222+
223+
## Const-eval
224+
225+
Initially proposed by @scalexm, this solution uses the existing implicit
226+
conversion from function types to function pointers, but in a const-eval
227+
context:
228+
229+
```rust
230+
fn add_one_thunk<const F: fn(i32)>(arg: i32) {
231+
F(arg + 1);
232+
}
233+
234+
fn get_adapted_function_ptr<const F: fn(i32)>() -> fn(i32) {
235+
add_one_thunk::<F>
236+
}
237+
```
238+
239+
- Avoids many of the pitfalls with implementing `Default`.
240+
- Requires giving up the original function type. There could be cases where
241+
you still need the original type but the conversion to function pointer
242+
is irreversible.
243+
- It's not yet clear if const-evaluation will be extended to support this
244+
use-case.
245+
- Const evaluation has its own complexities, and given that we already have
246+
unique function types, it seems like the original problem should be solvable
247+
using the tools we already have available.
248+
249+
## Opt-in trait implementations
250+
251+
This was barely touched on during the discussions, but one option would be to
252+
have traits be opted-in via a `#[derive(...)]`-like attribute on functions and
253+
closures.
254+
255+
- Gives a lot of power to the user.
256+
- Quite verbose.
257+
- Solves the problem of leaking capabilities.

0 commit comments

Comments
 (0)