Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using effect as the syntax of keyword-generics #14

Closed
mominul opened this issue Feb 24, 2023 · 18 comments
Closed

Using effect as the syntax of keyword-generics #14

mominul opened this issue Feb 24, 2023 · 18 comments

Comments

@mominul
Copy link
Contributor

mominul commented Feb 24, 2023

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
Copy link
Member

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. rust-lang/rust#76001

@satvikpendem
Copy link

satvikpendem commented Feb 24, 2023

A ? prefix could also be used, so ?const

@programmerjake
Copy link
Member

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
Copy link
Member

yoshuawuyts commented Feb 25, 2023

@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
Copy link
Member

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

@JackWolfard
Copy link

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

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

@programmerjake
Copy link
Member

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
Copy link
Member

yoshuawuyts commented Feb 25, 2023

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
Copy link
Member

programmerjake commented Feb 25, 2023

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
Copy link
Member

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

@yoshuawuyts
Copy link
Member

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
Copy link
Member

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
Copy link

satvikpendem commented Feb 25, 2023

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
Copy link
Member

yoshuawuyts commented Feb 26, 2023

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: #10 (comment)

@mominul
Copy link
Contributor Author

mominul commented Feb 26, 2023

@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
Copy link
Member

@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
Copy link
Contributor Author

mominul commented Feb 26, 2023

Okay, I'll do it!

@mominul
Copy link
Contributor Author

mominul commented Mar 14, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants