Skip to content

Commit c7171f4

Browse files
committed
Make addendum to discuss generic Option parameters in depth.
1 parent aa1d6b7 commit c7171f4

File tree

1 file changed

+69
-9
lines changed

1 file changed

+69
-9
lines changed

text/0000-default-type-parameter-fallback-take-two.md

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ If we call `func(None)` then the type of `P` cannot be inferred. This is frustra
7070

7171
We may guess this is the most often occurring use case for default arguments. [It comes](https://github.com/gtk-rs/gir/issues/143) [up](https://github.com/jwilm/json-request/issues/1) [a lot](https://github.com/rust-lang/rust/issues/24857). We need type parameters defaults for optional arguments to be a well supported pattern, and even more so of we wish to dream of having optional arguments as a first-class language feature.
7272

73+
Note that this example is a fourtunate case, see "Addendum: Getting generic `Option` arguments to work" at the bottom for an analysis of less fourtunate, and yet common, cases.
74+
7375
## Backwards-compatibily extending existing types
7476

7577
It's perfectly backwards-compatible for a type to grow new private fields with time. However if that field is generic over a new type parameter, trouble arises. The big use case in std is extending collections to be parametric over a custom allocator. Again something that must be backwards compatible and that most users don't care about. The existing feature was successful in making `HashMap` parametric over the hasher, so it has merits but it could be improved. To understand this let's try a simplified attempt at making `Arc` parametric over an allocator ([a real attempt](https://github.com/rust-lang/rust/pull/45272)).
@@ -397,30 +399,31 @@ fn use_my_arc<T>(arc: Arc<T>) {}
397399
We want to upgrade them backwards compatibly. The first thing we might attempt is:
398400

399401
```rust
400-
fn make_my_arc<T, A>(t: T) -> Arc<T, A> {}
401-
fn use_my_arc<T, A>(arc: Arc<T, A>) {}
402+
// We need `Default` to be able to construct the allocator.
403+
fn make_my_arc<T, A: Alloc + Default>(t: T) -> Arc<T, A> {}
404+
fn use_my_arc<T, A: Alloc>(arc: Arc<T, A>) {}
402405
```
403406

404407
But that would break `use_my_arc(make_my_arc(0))` . Maybe what we mean is:
405408

406409
```rust
407-
fn make_my_arc<T, A = alloc::Heap>(t: T) -> Arc<T, A> {}
408-
fn use_my_arc<T, A = alloc::Heap>(arc: Arc<T, A>) {}
410+
fn make_my_arc<T, A: Alloc + Default = alloc::Heap>(t: T) -> Arc<T, A> {}
411+
fn use_my_arc<T, A: Alloc = alloc::Heap>(arc: Arc<T, A>) {}
409412
```
410413

411414
Which is not pretty. Do we really have a choice for the default here? If we tried:
412415

413416
```rust
414-
fn make_my_arc<T, A = MyAllocator>(t: T) -> Arc<T, A> {}
415-
fn use_my_arc<T, A = MyAllocator>(arc: Arc<T, A>) {}
417+
fn make_my_arc<T, A: Alloc + Default = MyAllocator>(t: T) -> Arc<T, A> {}
418+
fn use_my_arc<T, A: Alloc = MyAllocator>(arc: Arc<T, A>) {}
416419
```
417420

418-
Then `use_my_arc(make_my_arc(0))` works but now we broke `use_my_arc(Arc::from_raw(ptr))`. So the only reasonable choice is to use the default in the type definition. Therefore we use an elided default in this situation, using the the default in the type definition as the default for `A`.
421+
Then `use_my_arc(make_my_arc(0))` works but now we broke, for example, `use_my_arc(Arc::from_raw(ptr))`. So the only reasonable choice is to use the default in the type definition. Therefore we use an elided default in this situation, using the the default in the type definition as the default for `A`.
419422

420423
```rust
421424
// The default of `A` in these declarations is `alloc::Heap`
422-
fn make_my_arc<T, A = _>(t: T) -> Arc<T, A> {}
423-
fn use_my_arc<T, A = _>(arc: Arc<T, A>) {}
425+
fn make_my_arc<T, A: Alloc + Default = _>(t: T) -> Arc<T, A> {}
426+
fn use_my_arc<T, A: Alloc = _>(arc: Arc<T, A>) {}
424427
```
425428

426429
It can be difficult to reason about whether a type parameter can use an elided default. To help usability we lint when a parameter that may have an elided default does not have a default. In rare cases this lint may be a false positive. But this doesn't seem bad as `#[allow(default_not_elided)]` will serve as an indication that a default is purposefully not set.
@@ -623,3 +626,60 @@ fn main() {
623626
```
624627

625628
We need to figure the design and implementation of defaults in specialization chains. Probably we want to allow only one default for a parameter in a specialization chain. This needs to be resolved prior to stabilization, but hopefully shouldn't block the acceptance of the proposal.
629+
630+
## Addendum: Getting generic `Option` arguments to work
631+
632+
Improving generic `Option` arguments is perhaps the big motivation of this RFC. However the example given in the motivation section glossed over some other common difficulties. The example was similar to:
633+
634+
```rust
635+
// A default is necessary otherwise `func(None)` cannot infer a type for `P`.
636+
fn func<P: AsRef<Path> = str>(p: Option<&P>) {
637+
match p {
638+
None => { /* do something */ }
639+
Some(path) => { /* do something else */ }
640+
}
641+
}
642+
```
643+
644+
And it would work fine. But in many cases we wish to somehow give a default value in case of `None`, which we might naively attempt as:
645+
646+
```rust
647+
fn func<P: AsRef<Path> = str>(p: Option<&P>) {
648+
// This does not (and should not) type check,
649+
// because "p == None" does not imply "P = str".
650+
let p_or_default = p.unwrap_or(&"/default/path");
651+
}
652+
```
653+
654+
What now? There are multiple ways to make this work. A simple way is to use a trait object instead, if possible. Either in the argument type itself or in the body of the function, examples:
655+
656+
```rust
657+
// This works today (modulo dyn Trait syntax).
658+
fn func(p: Option<&dyn AsRef<Path>>) {
659+
let p_or_default = p.unwrap_or(&"/default/path");
660+
}
661+
// Some cases may want to convert to a trait object inside the body.
662+
fn func<P: AsRef<Path> = str>(p: Option<&P>) {
663+
let p_or_default: &dyn AsRef<Path> =
664+
if let Some(p) = p { p } else { &"/default/path" };
665+
}
666+
```
667+
668+
Trait objects have limitations, so they might not work for all cases. A flexible but more convoluted way is to use an auxiliary function, like this:
669+
670+
```rust
671+
fn func<P: AsRef<Path> = str>(p: Option<&P>) {
672+
if let Some(p) = p {
673+
inner_func(p)
674+
} else {
675+
inner_func(&"/default/path")
676+
};
677+
678+
fn inner_func<P: AsRef<Path>>(p: &P) {
679+
/* actually do something with p */
680+
}
681+
}
682+
```
683+
684+
We need type parameter defaults to help these things work, but are these patterns reasonable? Or are they just too complex?
685+

0 commit comments

Comments
 (0)