rust-lang / keyword-generics-initiative

Public repository for the Rust keyword generics initiative
https://rust-lang.github.io/keyword-generics-initiative/
Other
90 stars 11 forks source link

Possible alternative to tilde/`?` using bound-like syntax #10

Open tgross35 opened 1 year ago

tgross35 commented 1 year ago

Opinion

I stumbled upon this repo as a complete outsider, and one of the things that stood out to me was the syntax. Taking an example from the book:

~async fn async_find<I>(
    iter: impl ~async Iterator<Item = I>,
    closure: impl ~async FnMut(&I) -> bool,
) -> Option<I>

Or (I think) its equivilant where

~async fn try async_find<It, Cl, I>(iter: It, closure: It) -> Option<I>
where:
    It: ~async Iterator<Item = I>,
    Cl: ~async FnMut(&I) -> bool

I find it a bit difficult to read:

async fn foo(F: impl async Iterator) ...

~async fn bar(F: impl ~async Iterator) ...

async fn some_other_foo(F: impl async Iterator) ...

The last thing is a very subjective visual nitpicks, but I think in general this syntax has a potential to get a bit messy (are combinations like ~const ~async eventually expected?)

Suggestion

I think that there's likely a way to leverage trait bound syntax to express these things, in a way that is already familiar. As a simple example:

fn foo<F: FnOnce(i32)>(f: F)
where
    fn: const if F: const,
{ /* ... */ }

And an example with multiple bounds with more complex relationships:

fn foo_finder<It, Cl, I>(iter: It, closure: It) -> Option<I>
where
    It: Iterator<Item = I>,
    Cl: FnMut(&I) -> bool,
    fn: async if It: async + Cl: async,
    fn: const if Cl: const,
    fn: !panics if It: !panics + F2: !panics
{ /* ... */ }

Advantages as I see them:

A downside is that it wouldn't be as simple to express this using the inline syntax with impl as shown above.

Anyway, not sure if something like this has been discussed or if there's a specific reason it wouldn't work, but just wanted to share my 2¢.

tgross35 commented 1 year ago

At first I was thinking that in my example, not having const fn / async fn visible up front was a downside. After thinking about it for a bit though, I think it might actually be a plus: the functions are non-const by default (as most functions that you could pass to it are also non-const), but there are special conditions that make it const.

tgross35 commented 1 year ago

Relevant Zulip thread: https://rust-lang.zulipchat.com/#narrow/stream/328082-t-lang.2Fkeyword-generics/topic/~const.20desugaring

It was brought up there that there needs to be a way to specify bounds on a specific trait, rather than on a type. Possible solution:

fn foo<Closure, ItemTy>(closure: Closure) -> Option<ItemTy>
where
    Closure: FnMut(&ItemTy) -> bool, // const bound is here
    Closure: SomeOtherTrait, // but not here
    fn: const if <Closure as FnMut>: const,
{ /* ... */ }

This extra bound specification would only be needed if type Closure has >1 trait bound. The style would be quite consistent with the existing syntax of needing to narrow function calls like <SomeType as SomeTrait>:foo when there are multiple traits provide a function named foo.

obsgolem commented 1 year ago

I just wanted to hop in and note that this idea is kind of like treating effects as a special kind of marker trait. The proposed syntax above could be thought of as syntactic sugar for something that could be written like this:

impl<F, T> const for fn foo 
where 
       F: FnMut(&T) -> bool,
       F: const

It was brought up there that there needs to be a way to specify bounds on a specific trait, rather than on a type. Possible solution:

fn foo<Closure, ItemTy>(closure: Closure) -> Option<ItemTy>
where
    Closure: FnMut(&ItemTy) -> bool, // const bound is here
    Closure: SomeOtherTrait, // but not here
    fn: const if <Closure as FnMut>: const,
{ /* ... */ }

Continuing my line of thought above, this need is then like saying that const is a "trait for traits". The proposed syntax could then be the way of creating higher order trait bounds.

tgross35 commented 1 year ago

Continuing my line of thought above, this need is then like saying that const is a "trait for traits"

That's exactly the way I see it. They aren't the type: Trait bounds we currently have, but they similarly bind a function or trait to a set of requirements. A const function is required to not call non-const functions, an async function must return a future, !panicking functions can't call functions that may panic, and if user-defined effects are ever possible then they'd likely be similar.

Sp00ph commented 1 year ago

I like the idea of !panics and !unwinds constraints (where !panics implies !unwinds). I feel like it could make unsafe code a lot simpler if you don't need to worry about user provided types unwinding because you wouldn't need as many drop guards and such. If you really wanted to you could even make a !unwinds function wrapper that aborts if the provided function panics (sort of like how noexcept behaves in C++).

jssblck commented 1 year ago

Coming here from the recent WG report.

Please please do not stick with the ?async and friends syntax. In a future where library maintainers can make their functions optionally async, we will, and many functions (maybe even most functions) will become littered with these bounds.

This means that picking something that isn't jarring is critical.

I really don't want to have to read Rust code that looks like this (from the linked report):

trait ?const ?async Read {
    ?const ?async fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
    ?const ?async fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { .. }
}

/// Read from a reader into a string.
?const ?async fn read_to_string(reader: &mut impl ?const ?async Read) -> io::Result<String> {
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

Not only is it very noisy, it's also very ugly; Rust code syntax is kind of already a meme (although I think it's beautiful in its own way), and a change like this would lean into that really hard. Code is read far more often than written, and making code easily legible is key to maintainable software. By mixing upper case (?) with lower-case identifiers in front of the function, IMO the function becomes more difficult to parse and therefore less legible. Even with the older ~ syntax it still felt very noisy and off putting.

I do like the ?effect/.do proposed solution to this, but it's important to recognize that not all functions will be valid for all effects.

For example, this is exactly the kind of thing I'd personally write:

/// We want to explicitly state that this can't panic, so we can't use `?effect` to do it generically.
///
/// Alternately, assume some keyword exists which we don't want to support,
/// or we have more keywords we don't want to support than ones that we do,
/// or we want to support all of today's keywords but not all future keywords forever, etc.
/// The point is to illustrate what it looks like with a bunch of keywords.
trait ?const ?async !panic !unwind Read {
    ?const ?async !panic !unwind fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
    ?const ?async !panic !unwind fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { .. }
}

/// Read from a reader into a string.
?const ?async !panic !unwind fn read_to_string(reader: &mut impl ?const ?async !panic !unwind Read) -> io::Result<String> {
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

This reminds me of fused-effects in a really negative way.


Now, having complained, in regards to this specific issue, I think this (or something like it) is a really good idea. By putting these constraints in the where block, it reduces the apparently noise by moving into a spot where real estate is less premium and mixing upper/lowercase identifiers (or other odd mixes) already exists. Or in other words, "where is already really noisy, so making it more noisy at least limits the blast radius".

If this issue were implemented, the function above becomes something like:

fn read_to_string(reader: &mut R) -> std::io::Result<String>
where
  R: Read,
  fn: async if R: async,
  fn: const if R: const,
  fn: !panic + !unwind,
{
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

Which is much more tractable.

Overall, I love the idea of keyword generics, but I implore the WG to reconsider this syntax and deeply consider what'd be the best syntax for a version of Rust where these bounds were on the vast majority of functions, because I think that's where we're headed.

Sp00ph commented 1 year ago

If this issue were implemented, the function above becomes something like:

fn read_to_string(reader: &mut R) -> std::io::Result<String>
where
  R: Read,
  fn: async if R: async,
  fn: const if R: const,
  fn: !panic + !unwind,
{
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

The !panic + !unwind constraints probably couldn't be satisfied for this function specifically (What if the reader panics? Or what if it produces > isize::MAX bytes and the string allocation panics?). But yes I agree, the where bounds look much more pleasant and readable to me too.

satvikpendem commented 1 year ago

I agree regarding the current ~/? syntax being quite annoying to parse and read through. The above example

?const ?async !panic !unwind fn read_to_string(reader: &mut impl ?const ?async !panic !unwind Read) -> io::Result<String>

does not even really tell me what the actual function is about until half-way through the function signature, and then there is another long string of terms to figure out exactly what the function parameter is.

In contrast, with the below

fn read_to_string(reader: &mut R) -> std::io::Result<String>
where
  R: Read,
  fn: async if R: async,
  fn: const if R: const,
  fn: !panic + !unwind,
{
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

I can tell exactly what is happening at a high overview, that read_to_string takes a reader with some properties that I can then look at in a further in the where clause.

If I were in a large codebase with many such functions in the ~/? syntax, it would be difficult for me to parse through and figure out which function I'm looking for.

If fn looks too much like a type or otherwise seems like a "magical" variable, another user on the /r/rust thread mentioned having effects even be after the where clause via the effect keyword:

fn foo<F, T>(closure: F) -> Option<T>
where 
    F: FnMut(&T) -> bool,
effect 
    const if F: const,
    ?async,
{ /* ... */ }

And here we can use the ? prefix to signify "maybe" and also have the ! prefix to signify not.

Personally I like this last syntax the best since it is much more readable while still allowing the aforementioned prefixes and also not making fn seem somewhat magical. If we have more types of effects in the future, maybe even a full-blown effects system, it is easy enough to add them to the effect list.

utkarshgupta137 commented 1 year ago

What about allowing a "generic-like" syntax? For eg:

trait<R: ?const ?async !panic !unwind> Read<R> {
    fn<R> read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
    fn<R> read_to_string(&mut self, buf: &mut String) -> io::Result<usize> { .. }
}

fn<R: ?const ?async !panic !unwind> read_to_string(reader: &mut impl Read<R>) -> io::Result<String> {
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

fn<R> foo<F, T>(closure: F) -> Option<T>
where 
    F: FnMut<R>(&T) -> bool,
    R: ?const ?async !panic !unwind
{}

struct<R: ?const ?async !panic !unwind> File<R> {
    async waker: Waker,
    !async meta: Metadata,
}

impl<R: ?const ?async !panic !unwind> Read<R> for File<R> {
    fn<R> read(&mut self, buf: &mut [u8]) -> io::Result<usize> { .. }
}

Also, there is an inconsistency in the keyword position. For traits, impls, & structs the generics come after the keyword but for functions, they come before. I think the above syntax solves that problem as well. The main issue I see with this approach is that since it looks like a generic, someone might try to add normal trait bounds to the generic. Also, the compiler should fill in the generic without the user having to provide it. Eg:

fn main() {
    let file = File::new();
    let out = read_to_string(file).unwrap();
}
leslie255 commented 1 year ago

Would it be possible to leave out the fn: part like this?

I don't think this would create any ambiguity for the parser, since type generics are followed by : and keyword generics are followed by if, and async and const are already keywords.

fn foo_finder<It, Cl, I>(iter: It, closure: It) -> Option<I>
where
    It: Iterator<Item = I>,
    Cl: FnMut(&I) -> bool,
    async if It: async + Cl: async,
    const if Cl: const,
    !panics if It: !panics + F2: !panics
{ /* ... */ }
obsgolem commented 1 year ago

keyword generics are followed by if

This might not necessarily be true; an unconditional effect could be expressed using the postfix notation. It might still be unambiguous though.

satvikpendem commented 1 year ago

Yeah something that is const or async doesn't necessarily have to have a conditional attached to it, as in the examples in my comment above, they can still be maybe const or not async, for example, with no conditions on when they would be.

leslie255 commented 1 year ago

This might not necessarily be true; an unconditional effect could be expressed using the postfix notation. It might still be unambiguous though.

For empty type generic constraints rustc requires you to add a :, so it is only consistent to require empty keyword generic constraints to also have an if:

fn f<T>()
where T:,
{ // rustc requires you to add a `:` after `where T`
}
fn g()
where async if,
{ // should also require an `if` after `where async`
}
satvikpendem commented 1 year ago

if would be consistent but it feels weird semantically. Async if...what? Nothing? Maybe we should just have a colon after async and const as well too.

DavidArchibald commented 1 year ago

I think this also has the potential to power or clarify AND versus OR relationship:

~async fn async_find<I>(
    iter: impl ~async Iterator<Item = I>,
    closure: impl ~async FnMut(&I) -> bool,
) -> Option<I>

Must mean one of two things:

I think the + in this proposal very obviously means AND. I think ~async probably has to mean AND as well or would become rather confusing. But I think OR should be possible and under this proposal it could rather naturally be something like |, overall looking like fn: async if A: async | B: async.

Here's some usage examples:

// Should always be possible with either interpretation of how `~async` or the like works.
async_find(async_iter, |i| async { i > 3 }).await;
async_find(sync_iter, |i| i > 3);

// Only possible with OR relationship
async_find(async_iter, |i| i > 3).await;
async_find(sync_iter, |i| i > 3).await;

I feel like this would be a reasonably common desire.

I think the main downside is that someone might expect to be able to use | outside of this proposal's where clause or to make really hairy where clauses. Is there even a use case for these honestly unideal where clauses fn: async if A: async + B: async | C: asnyc, fn: async if (A: async | B: async) + C: async or even fn: async if (A: async | B: async) + (C: async | D: async)? It could be simply be disallowed but it is a genuine downside with my additional proposal that people might end up expecting to be able to do that. If there's a use case though it might be quite nice.

By comparison I think ~async probably has to mean AND for reasons I'll get to later. If OR relationships were to be added later, something like @async (or some other random sigil) would have to be added to mean OR. This might allow avoiding the potentially very hairy where clauses (at the least implementing error messages telling you that you can't write them) but I certainly would forget what @ versus ~ means and | and + are much clearer but doesn't work well out of a where clause, +async or |async just doesn't work as well.

I think ~async has to mean AND for consistency with ~const which probably has to mean AND. For the const and likely !panic, along with maybe other effects the OR relationship doesn't really seem to make sense:

~const fn const_find<I>(
    iter: impl ~const Iterator<Item = I>,
    closure: impl ~const FnMut(&I) -> bool,
) -> Option<I>

It's probably reasonable to assume almost most functions will use every parameters and since you obviously can't iterate a non-const iterator or call a non-const closure inside a const function so const_find can't be, well, const useless unless both iter AND closure are const. In comparison to the ergonomics of OR in the context of async, in const AND just makes a lot more sense.

All in all I really like this and I basically feel like | to represent some effect conditions is a pretty natural extension to this syntax compared to trying to extend ~const syntax and I anticipate people will end up wanting this capability.

edward-shen commented 1 year ago

I suggested an alternative syntax in rust-lang/rust#107003, but it seems like this discussion is here. How does using multiple where bounds and allowing where bounds to have an effect keyword look to folks instead?

fn read_to_string(reader: &mut R) -> std::io::Result<String>
where
  R: Read,
where async
  R: async Read,
where const
  R: const Read,
{
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

It doesn't front load the signature, makes effect bounds explicit (e.g. a different bounds could be used for different effects later one such as AsyncRead), and uses familiar syntax.

It also has the added benefits of visually separating effect bounds too, and doesn't involve using a fn keyword to reference the fn itself.

yoshuawuyts commented 1 year ago

I'm in the process of creating an overview of some of the alternative syntax designs, basing it on the following snippet:

/// A trimmed-down version of the `std::Iterator` trait.
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

/// An adaptation of `Iterator::find` to a free-function
pub fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T> + Sized,
    P: FnMut(&T) -> bool;

@tgross35 I'd be interested in the following two translations of this snippet to your design:

  1. A variant where all items are always async.
  2. A variant where all items are generic over async.
  3. A variant where all items are generic over all available modifier keywords (e.g. effect/.do semantics).

If you believe more variants would be helpful to include as well, please feel free to. Thank you!

edit: We now have a template which can be filled out. That should make it easier to keep track of the various designs.


If anyone else in the thread wants to contribute their designs based on the snippet above, please feel free to. This will help make it easier to compare the syntactic choices made in each design. Thank you!

yoshuawuyts commented 1 year ago

To share an example of a design overview, here is what the syntax we showed off in the progress report looks like using the snippet as a base:

base (reference)

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

pub fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T> + Sized,
    P: FnMut(&T) -> bool;

always async

pub trait async Iterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

pub async fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: async Iterator<Item = T> + Sized,
    P: async FnMut(&T) -> bool;

maybe async

pub trait ?async Iterator {
    type Item;
    ?async fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

pub ?async fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: ?async Iterator<Item = T> + Sized,
    P: ?async FnMut(&T) -> bool;

generic over all modifier keywords

A slight modification from the report, using effect instead of ?effect.

pub trait effect Iterator {
    type Item;
    effect fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

pub effect fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: effect Iterator<Item = T> + Sized,
    P: effect FnMut(&T) -> bool;
lilizoey commented 1 year ago

An idea for effects as similar to const-generic booleans.

Translating examples:

base (reference)

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
}

pub fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T> + Sized,
    P: FnMut(&T) -> bool;

always async

pub async trait Iterator {
    type Item;
    // function assumed async since trait is
    fn next(&mut self) -> Option<Self::Item>;
    !async fn size_hint(&self) -> (usize, Option<usize>);
}
// or
pub trait Iterator<effect async> {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint<effect !async>(&self) -> (usize, Option<usize>);
}
// or
pub trait Iterator where effect async {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>) where effect !async;
}

pub async fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T> + Sized,
    P: async FnMut(&T) -> bool;

// or

pub fn find<I, T, P, effect async>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T> + Sized,
    P: FnMut<effect async>(&T) -> bool;

maybe async

pub trait Iterator<effect A: async> {
    type Item;
    // `<effect async = A>` elided
    fn next(&mut self) -> Option<Self::Item>;
    !async fn size_hint(&self) -> (usize, Option<usize>);
    // or
    fn size_hint<effect !async>(&self) -> (usize, Option<usize>);
    // or
    fn size_hint(&self) -> (usize, Option<usize>) where effect !async;
    // as opposed to `where A = !async` which would make this function
    // only exist if we're in a context where `Iterator<A = true>`
}

pub fn find<I, T, P, effect A: async>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T, effect async = A> + Sized,
    P: FnMut<effect async = A>(&T) -> bool;

generic over all modifier keywords

This would likely require adding quantification over effects, similar to for<'a>. Syntax not fully thought out, but approximately:

pub trait Iterator where for<effect E> ?E {
    type Item;
    // something like `where for<effect E> ?E` elided
    fn next(&mut self) -> Option<Self::Item>;
    !async fn size_hint(&self) -> (usize, Option<usize>);
    // or
    fn size_hint<effect !async>(&self) -> (usize, Option<usize>);
    // or
    fn size_hint(&self) -> (usize, Option<usize>) where effect !async;
}

pub fn find<I, T, P>(iter: &mut I, predicate: P) -> Option<T>
where
    for<effect E> A: E,
    I: Iterator<Item = T, for<effect E> A = E> + Sized,
    P: FnMut<for<effect E> A = E>(&T) -> bool;

!async fn could also be sync fn or just dropped entirely in favor of fn foo<effect !async>

For more details:

We introduce a new kind of generic, `effect` generics. An effect may either be true, or false. We can create a new effect by doing: - `A + B`, which is true whenever both `A` and `B` are - `A | B`, which is true whenever either `A` or `B` are - `!A`, which is true whenever `A` is false When writing generics, we may add `effect E = bool`, where bool is either true or false, at the end of the list, after `const` generics. As an example let's take: ```rs fn foo(...) {...} ``` Syntactic sugar: - `effect E` will be equivalent to `effect E = true`, and - `effect !E` will be equivalent to `effect E = false` so `foo` from above may be more cleanly written as ```rs fn foo(...) {...} ``` `async fn` can also be defined as syntactic sugar for that, meaning the below function is equivalent ```rs async fn foo(...) {...} ``` We may also move the bound to the where clause ```rs fn foo(...) where effect async {...} ``` To be generic over an effect `E` you'd add an `effect A: E`, so to make the above function generic over async-ness you'd write ```rs fn foo(...) {...} ``` If there's only one generic of a specific effect, we can elide the `effect E = A`, and `A` will be assumed to be the one effect of that value so equivalently we can write ```rs fn foo(...) {...} ``` Additionally we can introduce `effect ?E` as syntactic sugar for `effect A: E, effect E = A` so now we can write the above as `fn foo(...) {...}` If there are multiple generics over one effect you'd need to clarify whether the function has that effect For instance to say a function `foo` is `async` if `A1` or `A2` is async, you'd write: ```rs fn foo(...) {...} ``` Every effect will have an assumed default of either `true` or `false` for instance: `const` has default `false` `async` has default `false` `panic` has default `true` If there is no effect-bound on an item, it is assumed to be its default value. when marking something as something other than the default, this may change the type for instance, marking a function `fn foo() -> T` as `fn foo() -> T` would make it so that `T` is a `Future` instead. In a trait, every item is assumed to have the same effect-bounds as the trait itself, unless otherwise specified

Some convenient things about this syntax:

To make a function have specific behavior for when it is async/sync we could do: ```rs fn foo() { if A { // do stuff when foo is async } else { // do stuff when foo is not async } } ``` impl blocks can also look and be used in a very familiar way: ```rs impl SomeTrait MyGenericType { ... } impl SomeTrait MyAsyncType { ... } impl SomeTrait MySyncType { ... } ```
yoshuawuyts commented 1 year ago

@sayaks I just realized I forgot to add something important to the snippet: a size_hint method which is guaranteed to never be async. This should cover being able to mix async and non-async methods in a single trait, which is an important requirement for any plausible design.

Can I ask you to perhaps update your design sample to include size_hint? Apologies for the inconvenience.

lilizoey commented 1 year ago

@yoshuawuyts oki done

lilizoey commented 1 year ago

here is i think a better syntax for the "generic over all keywords" case

pub trait Iterator<effect A: for<effect>> {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    !async fn size_hint(&self) -> (usize, Option<usize>);
}

pub fn find<I, T, P, effect A: for<effect>>(iter: &mut I, predicate: P) -> Option<T>
where
    I: Iterator<Item = T, for<effect> = A> + Sized,
    P: FnMut<for<effect> = A>(&T) -> bool;

however this would mean size_hint is generic over all effects except async, so it might be good to make such universal bounds not automatically applied to all items in a trait, in that case we'd have

pub trait Iterator<effect A: for<effect>> {
    type Item;
    fn next(&mut self) -> Option<Self::Item> where for<effect> = A;
    fn size_hint(&self) -> (usize, Option<usize>);
}

alternatively we could have an opt-out syntax for the implicit bound

pub trait Iterator<effect A: for<effect>> {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>) where for<effect> = default;
}
Description Every type can have a bound placed for all effects over that type, this is done by using `for = A`, where A is some bound over all effects. By default, we'd have every type implicitly have a `for = default` bound, which just means every effect has its default value. (for instance, `false` for `const` or `true` for `panic`.) `>` creates a universal bound over effects named `A` where every effect is generic. a single effect can also be bound by a universal bound, by doing `effect async = A`. this would mainly be useful in cases where there are multiple universal bounds and you want to be specific about certain effects. in theory this could allow the syntax ` = true/false>`, but that seems like it'd not be very useful, since we'd likely have some effect be true by default and others false by default. we could also have ` = !default>` but that again doesn't seem very useful. not only is it difficult for the user to know they are in fact using the non-default effect for every effect. but also it'd make it very easy to add breaking changes. Because if a new effect is added, code that used to compile would stop compiling if it's not compatible with the new effect. Here is an example of multiple universal bounds, note that you now need to specify how to interpret the effect-status of that type since the compiler can't know what you'd want. ```rs fn foo, effect B: for>(closure1: F1, closure2: F2) -> O where // the entire function has an effect, if either A or B has it for = A | B, // however the function is const only when both A and B are const effect const = A + B, F1: FnMut = A>() -> O, F2: FnMut = B>() -> O { ... } ``` For instance, this would mean that `foo` is `async` if either `closure1` or `closure2` is async. This however may not end up working out in practice, i could imagine it not really being possible to write any useful code with such complicated bounds over universal quantifiers. in that case it might be better for each effect to have a default way of combining (`const` would likely be `A + B` by default, whereas `async` is `A | B` by default). It might feel a bit more magical that way, but might be the only reasonable thing to do. In that acse the above function would look like this: ```rs fn foo, effect B: for>(closure1: F1, closure2: F2) -> O where F1: FnMut = A>() -> O, F2: FnMut = B>() -> O { ... } ``` It would be possible to specify bounds for specific effects though, ```rs fn foo, effect async>() { ... } // or async fn foo>() { ... } ``` This is a function that is always async, but generic over every other effect. Or with multiple universal bounds in the case where the universal bond is implicit: ```rs fn foo, effect B: for>(closure1: F1, closure2: F2) -> O where effect async = A + B, F1: FnMut = A>() -> O, F2: FnMut = B>() -> O { ... } ``` This function would be async if *both* closures are `async`, instead of either of them as it would be by default.
yoshuawuyts commented 1 year ago

@sayaks Instead of tracking design proposals in a GitHub thread, I figured it might actually be better if we start checking them in. Can I ask you to create a branch based off this template and file a PR containing your design? That should make it easier to look up the design later on. If it's easier if I do it, just let me know. Thank you!

PeterHatch commented 1 year ago

An idea for effects as similar to const-generic booleans.

@sayaks For the "generic over all keywords" case, I think rather than inventing new syntax you may be able to treat it as similar to a const-generic Effects, where Effects is a struct with a boolean field for each effect.

Alphapage commented 9 months ago
?fn read_to_string(reader: &mut R) -> std::io::Result<String>
where
  ?: ?async ?const !panic  !unwind,
  R: Read,  
{
    let mut string = String::new();
    reader.read_to_string(&mut string).await?;
    Ok(string)
}

In my opinion, the function is impacted directly with optional behaviours. A shortcut (aka interrogation mark or something else) before or after 'fn' should point to a where clause to attract the developer and tell what is 'hidden'...

clarfonthey commented 8 months ago

Since the latest RFC draft that was checked in uses an attribute syntax, I assume that's the plan going forward, and this should be closed as complete?

yoshuawuyts commented 8 months ago

The attribute notation in the draft RFC is intended mainly as a placeholder syntax. It mentions picking a syntax as an unresolved question.

clarfonthey commented 8 months ago

Ah, I guess that's what I get for just skimming the draft. Makes sense.