✨ RFC
When you're ready to start drafting, copy in the template text from the rfcs repository.
Motivation
TBD
Design
Const
#![allow(unused)] fn main() { const fn foo() {} }
desugars to
#![allow(unused)] fn main() { const<C> fn foo() {} }
This means
#![allow(unused)] fn main() { const fn foo<T: const SomeTrait>(t: T) {} }
desugars to this, which is just one character more:
#![allow(unused)] fn main() { const<C> fn foo<T: SomeTrait * C>(t: T) {} }
which allows you to call trait methods within the const fn
.
For the common case however, effects do not need to be named and can be referenced to by _
:
#![allow(unused)] fn main() { const<_> fn foo<T: SomeTrait * _>(t: T) {} }
Async
In contrast to const
(which is "maybe"-by-default) there is no
sugared version.
#![allow(unused)] fn main() { async<A> fn foo() {} }
You can then pass this effect into other uses of async
within
the maybe-async
function:
#![allow(unused)] fn main() { async<A> fn foo<T: SomeTrait * A>(t: T) { t.some_method() } }
which allows you to call trait methods within the ~async fn
.
Combining effects
If you have both async
and const
modifiers, your function may not perform
any non-const operations like accessing the file system or static items, but is
allowed to use await
.
#![allow(unused)] fn main() { // Can be const, async, both or neither const<X> async<X> fn foo<T: SomeTrait * X>(t: T) where effect X: async ^ const, { t.bar().await } }
async
and const
can mutually exclusive, because async
is useful for non-blocking IO,
but const
doesn't have any IO at all. Thus we allow declaring arbitrary exclusive-or
bounds for effects.
#![allow(unused)] fn main() { // Can be const, async, both or neither const<X> async<X> fn foo<T: SomeTrait * X>(t: T) where effect X: async ^ const, { t.bar().await } }
- Yosh's brain error brain worms: we shouldn't have
try
functions, we should have functions which usethrows
, and it specifies which error we throw. - Oli: the carried error from
try
could just be inferred from the call-site.
#![allow(unused)] fn main() { async<E> try<E> fn foo(t: impl SomeTrait * E, p: impl SomeTrait * E) { t.some_method().await?; p.some_method().await?; } }
#![allow(unused)] fn main() { async<E1> try<E2> fn foo(t: impl SomeTrait * E1, p: impl SomeTrait * E2, q: impl SomeTrait * E1 * E2) { t.some_method().await; p.some_method()?; q.some_method().await?; } }
Backwards Compatibility
One important part of keyword generics is that they will allow us to introduce them to the standard library without breaking any existing code. Right now we're already adding constness to many functions in the standard library.
Take for example the method Option::map
. This method takes a closure f
. If we want to make this method const
the closure must be const
as well. This means the constness of the method and the constness of the input types are linked.
Before constification, Option::map
looked like this:
#![allow(unused)] fn main() { impl<T> Option<T> { fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U> { match self { None => None, Some(x) => Some(f(x)), } } } }
We might be tempted to make the function "maybe-const" by adding a const
keyword-generic to the function:
#![allow(unused)] fn main() { impl<T> Option<T> { const<C> fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U> { match self { None => None, Some(x) => Some(f(x)), } } } }
But the compiler won't let us do this, because we're calling the closure f
inside the function body, but f
is not marked "maybe-const":
error[E0000]: `f` is not const
--> lib/option.rs:5:20
|
5 | Some(x) => Some(f(x)),
| ^ implements `FnOnce` but is lacking a `const` keyword
|
hint: consider applying the `C` generic to the `FnOnce` bound:
--> lib/option.rs:2:48
|
2 | const<C> fn map<U>(self, f: impl FnOnce(T) -> U * C) -> Option<U> {
| ++++
- current use of map will keep working because any type which implements
FnOnce
will just keep working. - Yosh question: so we base inference on the parameters we pass it?
fn main() { my_opt.map(|x| x * 2); // non-async } async<A> fn foo(i: i32) -> i32 { i * 2 } async fn main() { my_opt.map(async |x| x * 2).await; // async // ^ ^ or is it this? // | is it this? my_opt.map(foo).await; //^^^ do we infer this to be `async` because of the `await` }
- Conclusion: we must infer based on the
.await
- Because: if we infer based on the argument then in the case of a
maybe async
typefoo
we'd need to start turbofishing arguments. In contrast:.await
here is unambiguous! - But, we want to go further: if the function and the argument are both
maybe async
, and you're inside of anasync fn
, then not.await
ing should trigger a warning. - And: the default should fall back to the !async variant. - TODO: create an example for this!
- Because: if we infer based on the argument then in the case of a
// we cannot base inference on the type of the outer function. This code is valid // today, but if we made it infer based on the enclosing function's type we'd get: async fn main() { my_opt.map(|x| x * 2); // compile error! }
The repeated use of const<C>
enforces that if map
is called with "not const" (so called
from a base Rust function), then the FnOnce
bound can be satisfied by any type that
implements FnOnce
, irrespective of its constness.
Thus backwards compatibility is preserved for const
.
If Option::map
is called from another const fn
or from within a true const
context (like a const item's initializer), then the C
is either "maybe const" or "definitely const",
but either way, only types that implement const FnOnce
are allowed for f
.
#![allow(unused)] fn main() { impl<T> Option<T> { const<C> fn map<U>(self, f: impl FnOnce(T) -> U * C) -> Option<U> { // ^ is linked with this right here ^ match self { None => None, Some(x) => Some(f(x)), } } } }
Multiple different effects
We don't have a decision yet on how try
or (throws
) is supposed to work, which may need to provide a way to define N-copies of the same effect.
This RFC does not address this, however it is forward compatible with adding multiple independent effects of a single keyword:
#![allow(unused)] fn main() { try<E1, E2> fn foo(t: impl SomeTrait * E1 * E2) { t.some_method()??; } }