rust-lang / keyword-generics-initiative

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

Using `effect` as the syntax of keyword-generics #14

Closed mominul closed 1 year ago

mominul commented 1 year ago

Motivation

As per the progress report, the keyword-generics initiative is heading towards an initial vision that will lead syntax usage like:

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)
}

From my perspective and think many will agree that a function declaration like this:

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

or an extreme example:

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

is really noisy and cumbersome to digest and to have a feel at. It would make the syntax surface of Rust more complicated and make the learning curve steeper.

Details

Personally, I consider that renaming ~ and @ pointers to Box, Rc, and Arc was a great decision made by the Rust team. From my perspective, sigils may shorten the code, but it introduces the need for extra attention while reading the code and makes it noisier in most cases.

I personally really applaud the Rust team for starting this initiative, code duplication because of function coloring is a deep issue and we need to take it down! But currently proposed syntaxes(?async, fn<?async>) don't feel like we are heading toward the right syntax to begin with.

Using the effect clause

I want to propose the usage of the effect clause to achieve operation genericity, for example:

trait Read {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize>
    effect
        async;

    fn read_to_string(&mut self, buf: &mut String) -> Result<usize> 
    effect
        async
    { .. }
}

/// Function to read from the file into a string which may exhibit async or const effect
fn read_to_string(path: &str) -> io::Result<String>
effect
       async, const 
{
    let mut string = String::new();

    // We can be conditional over the context the function has been called from, 
    // only when the function declaration has the `effect` clause
    if async || !async {
        let mut file = File::open("foo.txt")?; // File implements Read
        // Because `read_to_string` is also an `effect` function that may or may not exhibit 
        // async-ness par the declaration, we can use it on both contexts (async/sync) 
        // we are placing the condition on.
        file.read_to_string(&mut string)?;  // .await will be inferred.   
    } else { // must be const
        // As the `read_to_string` doesn't exhibit const-ness, we'll need to handle it ourselves.
        string = include_str!(path).to_string();
    }

    Ok(string)
}

/// A normal function
fn read() {
    let data = read_to_string("hello.txt").unwrap();
}

/// A async function
async fn read() {
    let data = read_to_string("hello.txt").await.unwrap();
}

/// A const function
const fn read() {
    let data = read_to_string("hello.txt").unwrap();
}

So in a nutshell, a function declaration with an effect clause is a special type of function that may or may not exhibit async or const behavior(effect) and it depends on the context of the function being called from and we can execute a different piece of code according to the context from the function was called from too (like the const_eval_select, resolves #11):

fn function() -> Result<()>
effect
    async, const
{
    // ...
    if async {
        // code for handling stuff asynchronously
    else if const {
        // code for handling stuff `const`-way
    else {
        // code for handling stuff synchronously
    }
    // ...
}

Acknowledgment

I have been greatly influenced by the comments in #10, especially of @jssblck and @satvikpendem

It's my first time writing a proposal, so I might be missing stuff or being wrong, but I wanted my concerns to be heard and start a discussion about an alternative approach.

Thanks!

programmerjake commented 1 year ago

note if you want if const {} else {} syntax, we need to make it invalid to use const {...} blocks in if without parenthesis, so we need to adjust the syntax before const {} blocks stabilize. https://github.com/rust-lang/rust/issues/76001

satvikpendem commented 1 year ago

A ? prefix could also be used, so ?const

programmerjake commented 1 year ago

actually, now that I think of it, if async { foo(0) } else {} conflicts with already existing stable syntax if async { foo(0) }.await else {} because of Rust's must-be-able-to-parse-with-3-token-lookahead rule.

yoshuawuyts commented 1 year ago

@programmerjake the parser ambiguity is indeed why we decided against proposing that exact syntax, even if we think it would have been ideal if it could’ve worked.

——-

@mominul if you have the time, I’d be interested in reading how believe your proposal would compare to the effect/.do system we touched on in our blog post. We intentionally didn’t get into all details, such as constraining effects, but I’d still be interested in learning more about what your proposal does differently and whether there’s anything you believe we may have overlooked.

programmerjake commented 1 year ago

one other possible syntax option is if effect async {} else if effect const {} else {}

JackWolfard commented 1 year ago

If you really want to get Rust-y, then you could

match effect {
    async => ...
    const => ...
    _ => ...
}
programmerjake commented 1 year ago

If you really want to get Rust-y, then you could

match effect {
    async => ...
    const => ...
    _ => ...
}

that only works if effect is a keyword (rather than just a soft-keyword), otherwise it's ambiguous with match-ing on a variable named effect. Also, match has the drawback of not easily expressing stuff like async & const

yoshuawuyts commented 1 year ago

Yeah I wouldn’t mind if effect const tbh. I believe this could even work with the is keyword proposal so it could become if effect is const {} — which could open up a path towards of the match syntax too.

We’ll have to see what makes most sense for this. One option is to go with a free function first, and bring it into syntax later. Or perhaps use a macro first. We can probably try some things out to figure out what would work best.

programmerjake commented 1 year ago

imho a free function is a bad idea since it would either have to be compiler magic (not a normal expression) or would prevent using things that don't type check for both try/!try (and other things that change types such as async): e.g. the following won't work with just a normal compiler intrinsic:

pub ~try fn func(f: impl ~try Fn() -> String) -> String {
    if is_try() {
        let result = f();
        result.is_ok(); // type error: String doesn't have an is_ok() method
        // this is because everything in `if false {}` still needs to type check
        todo!()
    } else {
        f()
    }
}
programmerjake commented 1 year ago

though otoh something like const_eval_select can work, though is un-ergonomic

yoshuawuyts commented 1 year ago

I mean, if folks strongly prefer the const-eval-select api as an interim solution over a free function, that's fine by me.

I mostly care that we have something people can use straight away, and we can always figure out a good ergonomic solution later on.

programmerjake commented 1 year ago

if I had to pick between const_eval_select and is_const(), I'd pick const_eval_select because is_const() just plain doesn't work due to the false branch of the if still needing to type check and resolve names and stuff.

satvikpendem commented 1 year ago

If you really want to get Rust-y, then you could

match effect {
    async => ...
    const => ...
    _ => ...
}

This is actually something similar to what I proposed on the Zulip thread, haha

fn foo<T>(t: T) where T: Async + Const + Mut -> U { ... }

// with matching on the type level Async, Const, Mut and extracting what context the function is running, whether async, const, etc
match CONTEXT {
    Const => ...,
    Async => ...,
    ...
}

This isn't exactly feasible it seems but at least for the function syntax, I mentioned on #10 (my comment which @mominul kindly referenced), having an explicit effects clause similar to where would be interesting:

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

which extends this current proposal with conditional contexts on whether a function would be const, async, etc.


@yoshuawuyts in comparison to the effect/.do notation, I think it just looks cleaner to have the function signature be more clearly visible instead of having multiple ?s/?effects everywhere. If I want to know that there is an effect, I can look further into the function, just as how where clauses work today, being afterwards/below the function signature.

It is also nice to specify exactly which effects are used (async, const and possibly future user-defined effects even) but again not run into the soup of ?s before and inside the function signature. I think in essence this effect syntax proposal would solve both the ?const ?async !panic !unwind ... fn function(...) problem of being hard to parse, as well as not have the need to have a ?effect keyword that simply works over all such effects. But then again you mentioned you hadn't shown the details yet of conditional effects so I would have to wait and see what the syntax for that looks like.

Basically, I think the ? prefix looks odd in current Rust and I'd rather ? were used sparingly or even not at all if possible.

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;

@mominul 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: I've shared an example of a design overview here: https://github.com/rust-lang/keyword-generics-initiative/issues/10#issuecomment-1445253811

mominul commented 1 year ago

@yoshuawuyts I think we can map the design we're discussing here like the following:

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 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: Iterator<Item = T> + Sized,
    P: async FnMut(&T) -> bool;

maybe async

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>
    effect async;
    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 effect async;
effect
    async

generic over all modifier keywords

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>
    effect async, const;
    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 effect async, const;
effect
    async, const
yoshuawuyts commented 1 year ago

@mominul 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!

mominul commented 1 year ago

Okay, I'll do it!

mominul commented 1 year ago

@yoshuawuyts I have filled a PR containing the design #23