Skip to content

Commit a5447ff

Browse files
committed
Create RFC for "return position enum impl trait"
1 parent 873890e commit a5447ff

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
- Feature Name: (fill me in with a unique ident, `multi_type_return_position_impl_trait`)
2+
- Start Date: (fill me in with today's date, 2023-01-05)
3+
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This RFC enables [Return Position Impl Trait (RPIT)][RPIT] to work in functions
10+
which return more than one type. This is achieved by desugaring the return type
11+
into an enum with members containing each of the returned types, and
12+
implementing traits which delegate to those members:
13+
14+
[RPIT]: https://doc.rust-lang.org/stable/rust-by-example/trait/impl_trait.html#as-a-return-type
15+
16+
```rust
17+
// ✅ Single-type RPIT compiles today
18+
fn single_iter() -> impl Iterator<Item = i32> {
19+
1..10 // `std::ops::Range<i32>`
20+
}
21+
22+
// ❌ Multi-type RPIT does not yet compile
23+
// error[E0308]: `match` arms have incompatible types
24+
fn multi_iter(x: i32) -> impl Iterator<Item = i32> {
25+
match x {
26+
0 => 1..10, // `std::ops::Range<i32>`
27+
_ => vec![5, 10].into_iter(), // `std::vec::IntoIter<i32>`
28+
}
29+
}
30+
```
31+
32+
# Motivation
33+
[motivation]: #motivation
34+
35+
[Return Position Impl Trait (RPIT)][RPIT] is used when you want to return a value, but
36+
don't want to specify the type. In today's Rust (1.66.0 at the time of writing)
37+
it's only possible to use this when you're returning a single type from the
38+
function. The moment multiple types are returned from the function, the compiler
39+
will error. This can be frustrating, because it means you're likely to either
40+
resort to using `Box<dyn Trait>` or manually construct an enum to to map the
41+
branches to. It's not always desirable or possible to use `Box<dyn Trait>`. And
42+
constructing an enum manually can be both time-intensive, complicated, and can
43+
obfuscate
44+
the intent of the code.
45+
46+
What we're proposing here is not so much a new feature, as an expansion of the
47+
cases in which `impl Trait` can be used. We've seen previous efforts for this,
48+
in particular [RFC 1951: Expand Impl Trait][rfc1951] and more recently in [RFC
49+
2515: Type Alias Impl Trait (TAIT)][TAIT]. This continues that expansion by
50+
enabling more code to make use of RPIT.
51+
52+
[rfc1951]: https://github.com/rust-lang/rfcs/blob/master/text/1951-expand-impl-trait.md
53+
[TAIT]: https://rust-lang.github.io/rfcs/2515-type_alias_impl_trait.html
54+
55+
A motivating example for this is use in error handling: it's not uncommon to
56+
have a function return more than one error type, but you may not necessarily
57+
care about the exact errors returned. You may either choose to define a `Box<dyn
58+
Error + 'static>` which has the downside that [it itself does not implement
59+
`Error`][no-error]. Or you may choose to define your own enum of errors, which
60+
can be a lot of work and may obfuscate the actual intent of the code. It may
61+
sometimes be preferable to return an `impl Trait` instead:
62+
63+
[no-error]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=97894fc907fa2d292cbe909467d4db4b
64+
65+
```rust
66+
use std::error::Error;
67+
use std::fs;
68+
69+
// ❌ Multi-type RPIT does not yet compile (Rust 1.66.0)
70+
// error[E0282]: type annotations needed
71+
fn main() -> Result<(), impl Error> {
72+
let num = i8::from_str_radix("A", 16)?; // `Result<_, std::num::ParseIntError>`
73+
let file = fs::read_to_string("./file.csv")?; // `Result<_, std::io::Error>`
74+
// ... use values here
75+
Ok(())
76+
}
77+
```
78+
79+
# Desugaring
80+
[reference-level-explanation]: #reference-level-explanation
81+
82+
## Overview
83+
84+
Let's take a look again at the code from our motivation section. This function
85+
has two branches which each return a different type which implements the
86+
[`Iterator` trait][`Iterator`]:
87+
88+
[`Iterator`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html
89+
90+
```rust
91+
fn multi_iter(x: i32) -> impl Iterator<Item = i32> {
92+
match x {
93+
0 => 1..10, // `std::ops::Range<i32>`
94+
_ => vec![5, 10].into_iter(), // `std::vec::IntoIter<i32>`
95+
}
96+
}
97+
```
98+
99+
This code should be desugared by the compiler into something resembling the following
100+
([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=af4c0e61df25acaada168449df9838d3)):
101+
102+
```rust
103+
// anonymous enum generated by the compiler
104+
enum Enum {
105+
A(std::ops::Range<i32>),
106+
B(std::vec::IntoIter<i32>),
107+
}
108+
109+
// trait implementation generated by the compiler,
110+
// delegates to underlying enum member's values
111+
impl Iterator for Enum {
112+
type Item = i32;
113+
114+
fn next(&mut self) -> Option<Self::Item> {
115+
match self {
116+
Enum::A(iter) => iter.next(),
117+
Enum::B(iter) => iter.next(),
118+
}
119+
}
120+
121+
// ..repeat for the remaining 74 `Iterator` trait methods
122+
}
123+
124+
// the desugared function now returns the generated enum
125+
fn multi_iter(x: i32) -> Enum {
126+
match x {
127+
0 => Enum::A(1..10),
128+
_ => Enum::B(vec![5, 10].into_iter()),
129+
}
130+
}
131+
```
132+
133+
## Step-by-step guide
134+
135+
This desugaring can be implemented using the following steps:
136+
137+
1. Find all return calls in the function
138+
2. Define a new enum with a member for each of the function's return types
139+
3. Implement the traits declared in the `-> impl Trait` bound for the new enum,
140+
matching on `self` and delegating to the enum's members
141+
4. Substitute the `-> impl Trait` signature with the concrete enum
142+
5. Wrap each of the function's return calls in the appropriate enum member
143+
144+
The hardest part of implementing this RFC will likely be the actual trait
145+
implementation on the enum, as each of the trait methods will need to be
146+
delegated to the underlying types.
147+
148+
# Interaction with lifetimes
149+
150+
`dyn Trait` already supports multi-type _dynamic_ dispatch. The rules we're
151+
proposing for multi-type _static_ dispatch using `impl Trait` should mirror the
152+
existing rules we apply to `dyn Trait.` We should follow the same lifetime rules
153+
for multi-type `impl Trait` as we do for `dyn Trait`:
154+
155+
```rust
156+
fn multi_iter<'a>(x: i32, iter_a: &'a mut std::ops::Range<i32>) -> impl Iterator<Item = i32> + 'a {
157+
match x {
158+
0 => iter_a, // `&'a std::ops::Range<i32>`
159+
_ => vec![5, 10].into_iter(), // `std::vec::IntoIter<i32>`
160+
}
161+
}
162+
```
163+
164+
This code should be desugared by the compiler into something resembling the following
165+
([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=60ddacbb20c4068a0fff44a5481a7136)):
166+
167+
```rust
168+
enum Enum<'a> {
169+
A(&'a mut std::ops::Range<i32>),
170+
B(std::vec::IntoIter<i32>),
171+
}
172+
173+
impl<'a> Iterator for Enum<'a> {
174+
type Item = i32;
175+
176+
fn next(&mut self) -> Option<Self::Item> {
177+
match self {
178+
Enum::A(iter) => iter.next(),
179+
Enum::B(iter) => iter.next(),
180+
}
181+
}
182+
183+
// ..repeat for the remaining 74 `Iterator` trait methods
184+
}
185+
186+
fn multi_iter<'a>(x: i32, iter_a: &'a mut std::ops::Range<i32>) -> Enum<'a> {
187+
match x {
188+
0 => Enum::A(iter_a),
189+
_ => Enum::B(vec![5, 10].into_iter()),
190+
}
191+
}
192+
```
193+
194+
It should be fine if multiple iterators use the same lifetime. But only a single
195+
lifetime should be permitted on the return type, as is the case today when
196+
using `dyn Trait`:
197+
198+
```rust
199+
// ❌ Fails to compile (Rust 1.66.0)
200+
// error[E0226]: only a single explicit lifetime bound is permitted
201+
fn fails<'a, 'b>() -> Box<dyn Iterator + 'a + 'b> {
202+
...
203+
}
204+
```
205+
206+
# Prior art
207+
[prior-art]: #prior-art
208+
209+
## auto-enums crate
210+
211+
The [`auto-enums` crate][auto-enums] implements a limited variation of what is
212+
proposed in this RFC using procedural macros. It's limited to a predefined set
213+
of traits only, whereas this RFC enables multi-type RPIT to work for _all_
214+
traits. This limitation exists in the proc macro because it doesn't have access
215+
to the same type information as the compiler does, so the trait delegations
216+
have to be authored by hand. Here's an example of the crate being used to
217+
generate an `impl Iterator`:
218+
219+
[auto-enums]: https://docs.rs/auto_enums/latest/auto_enums/
220+
221+
```rust
222+
use auto_enums::auto_enum;
223+
224+
#[auto_enum(Iterator)]
225+
fn foo(x: i32) -> impl Iterator<Item = i32> {
226+
match x {
227+
0 => 1..10,
228+
_ => vec![5, 10].into_iter(),
229+
}
230+
}
231+
```
232+
233+
# Future possibilities
234+
[future-possibilities]: #future-possibilities
235+
236+
## Anonymous enums
237+
238+
Rust provides a way to declare anonymous structs using tuples. But we don't yet
239+
have a way to declare anonymous enums. A different way of interpreting the
240+
current RFC is as a way to declare anonymous type-erased enums, by expanding what
241+
RPIT can be used for. It stands to reason that there will be cases where people
242+
may want anonymous _non-type-erased_ enums too.
243+
244+
Take for example the iterator code we've been using throughout this RFC. But
245+
instead of `Iterator` yielding `i32`, let's make it yield `i32` or `&'static
246+
str`:
247+
248+
```rust
249+
fn multi_iter(x: i32) -> impl Iterator<Item = /* which type? */> {
250+
match x {
251+
0 => 1..10, // yields `i32`
252+
_ => vec!["hello", "world"].into_iter(), // yields `&'static str`
253+
}
254+
}
255+
```
256+
257+
One solution to make it compile would be to first map it to a type which can
258+
hold *either* `i32` or `String`. The obvious answer would be to use an enum for
259+
this:
260+
261+
```rust
262+
enum Enum {
263+
A(i32),
264+
B(&'static str),
265+
}
266+
267+
fn multi_iter(x: i32) -> impl Iterator<Item = Enum> {
268+
match x {
269+
0 => 1..10.map(Enum::A),
270+
_ => vec!["hello", "world"].into_iter().map(Enum::B),
271+
}
272+
}
273+
```
274+
275+
This code resembles the desugaring for multi-value RPIT we're proposing in this
276+
RFC. In fact: it may very well be that a lot of the internal compiler machinery
277+
used for multi-RPIT could be reused for anonymous enums.
278+
279+
The similarities might become even closer if we consider how "anonymous enums"
280+
could be used for error handling. Sometimes it can be useful to know which error
281+
was returned, so you can decide how to handle it. For this RPIT isn't enough: we
282+
actually want to retain the underlying types so we can match on them. We might
283+
imagine the earlier errror example could instead be written like this:
284+
285+
```rust
286+
use std::{fs, io, num};
287+
288+
// The earlier mult-value RPIT version returned `-> Result<(), impl Error>`.
289+
// This example declares an anonymous enum instead, using made-up syntax
290+
fn main() -> Result<(), num::ParseIntError | io::Error> {
291+
let num = i8::from_str_radix("A", 16)?; // `Result<_, std::num::ParseIntError>`
292+
let file = fs::read_to_string("./file.csv")?; // `Result<_, std::io::Error>`
293+
// ... use values here
294+
Ok(())
295+
}
296+
```
297+
298+
There are a lot of questions to be answered here. Which traits should
299+
this implement? What should the declaration syntax be? How could we match on
300+
values? All enough to warrant its own exploration and possible RFC in the
301+
future.
302+
303+
## Language-level support for delegation/proxies
304+
305+
One of the trickiest parts of implementing this RFC will be to delegate from the
306+
generated enum to the individual enum's members. If we implement this
307+
functionality in the compiler, it may be beneficial to generalize this
308+
functionality and create syntax for it. We're already seen [limited support for
309+
delegation codegen][support] in Rust-Analyzer as a source action [^disclaimer], and [various crates]
310+
implementing delegation exist on Crates.io.
311+
312+
[support]: https://github.com/rust-lang/rust-analyzer/issues/5944
313+
[various crates]: https://crates.io/search?q=delegate
314+
315+
[^disclaimer]: I (Yosh) filed the issue and authored the extension to Rust-Analyzer
316+
for this. Which itself was based on prior art found in the VS Code Java extension.
317+
318+
To provide some sense for what this might look like. Say we were authoring some
319+
[newtype] which wraps an iterator. We could imagine we'd write that in Rust
320+
by hand today like this:
321+
322+
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
323+
324+
```rust
325+
struct NewIterator<T>(iter: std::array::Iterator<T>);
326+
327+
impl<T> Iterator for NewIterator<T> {
328+
type Item = T;
329+
330+
#[inline]
331+
pub fn next(&mut self) -> Option<Self::Item> {
332+
self.0.next()
333+
}
334+
335+
// ..repeat for the remaining 74 `Iterator` trait methods
336+
}
337+
```
338+
339+
Forwarding a single trait with a single method is doable. But we can imagine
340+
that repeating this for multiple traits and methods quickly becomes a hassle,
341+
and can obfuscate the _intent_ of the code. Instead if we could declare that
342+
`NewIterator` should _delegate_ its `Iterator` implementation to the iterator
343+
contained within. Say we adopted a [Kotlin-like syntax], we could imagine it
344+
could look like this:
345+
346+
[Kotlin-like syntax]: https://kotlinlang.org/docs/delegation.html#overriding-a-member-of-an-interface-implemented-by-delegation
347+
348+
```rust
349+
struct NewIterator<T>(iter: std::array::Iterator<T>);
350+
351+
impl<T> Iterator for NewIterator<T> by Self.0; // Use `Self.0` as the `Iterator` impl
352+
```
353+
354+
There are many open questions here regarding semantics, syntax, and expanding it
355+
to other features such as method delegation. But given the codegen for both
356+
multi-value RPIT and delegation will share similarities, it may be worth
357+
exploring further in the future.

0 commit comments

Comments
 (0)