-
Notifications
You must be signed in to change notification settings - Fork 11
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
Possible alternative to tilde/?
using bound-like syntax
#10
Comments
At first I was thinking that in my example, not having |
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 |
?
using bound-like syntax
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:
Continuing my line of thought above, this need is then like saying that |
That's exactly the way I see it. They aren't the |
I like the idea of |
Coming here from the recent WG report. Please please do not stick with the 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 ( I do like the 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. 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. |
The |
I agree regarding the current ?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 If I were in a large codebase with many such functions in the If 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 Personally I like this last syntax the best since it is much more readable while still allowing the aforementioned prefixes and also not making |
What about allowing a "generic-like" syntax? 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. fn main() {
let file = File::new();
let out = read_to_string(file).unwrap();
} |
Would it be possible to leave out the I don't think this would create any ambiguity for the parser, since type generics are followed by 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
{ /* ... */ } |
This might not necessarily be true; an unconditional effect could be expressed using the postfix notation. It might still be unambiguous though. |
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 |
For empty type generic constraints rustc requires you to add a 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`
} |
|
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 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 By comparison I think I think ~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 All in all I really like this and I basically feel like |
I suggested an alternative syntax in rust-lang/rust#107003, but it seems like this discussion is here. How does using multiple 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 It also has the added benefits of visually separating effect bounds too, and doesn't involve using a |
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:
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! |
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 asyncpub 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 asyncpub 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 keywordsA slight modification from the report, using 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; |
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 asyncpub 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 asyncpub 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 keywordsThis would likely require adding quantification over effects, 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;
For more details: We introduce a new kind of generic,
When writing generics, we may add fn foo<T, O, const N: usize, effect async = true>(...) {...} Syntactic sugar:
fn foo<T, O, const N: usize, effect async>(...) {...}
async fn foo<T, O, const N: usize>(...) {...} We may also move the bound to the where clause fn foo<T, O, const N: usize>(...) where effect async {...} To be generic over an effect fn foo<T, O, const N: usize, effect A: async, effect async = A>(...) {...} If there's only one generic of a specific effect, we can elide the fn foo<T, O, const N: usize, effect A: async>(...) {...} Additionally we can introduce If there are multiple generics over one effect you'd need to clarify whether the function has that effect fn foo<effect A1: async, effect A2: async, effect async = A1 | A2>(...) {...} Every effect will have an assumed default of either when marking something as something other than the default, this may change the type In a trait, every item is assumed to have the same effect-bounds as the trait itself, Some convenient things about this syntax:
To make a function have specific behavior for when it is async/sync we could do:
fn foo<effect A: async>() {
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: impl<effect A: async> SomeTrait<effect async = A> MyGenericType { ... }
impl SomeTrait<effect async> MyAsyncType { ... }
impl SomeTrait<effect !async> MySyncType { ... } |
@SayakS I just realized I forgot to add something important to the snippet: a Can I ask you to perhaps update your design sample to include |
@yoshuawuyts oki done |
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 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;
} DescriptionEvery type can have a bound placed for all effects over that type, this is done by using a single effect can also be bound by a universal bound, by doing in theory this could allow the syntax we could also have 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. fn foo<O, F1, F2, effect A: for<effect>, effect B: for<effect>>(closure1: F1, closure2: F2) -> O
where
// the entire function has an effect, if either A or B has it
for<effect> = A | B,
// however the function is const only when both A and B are const
effect const = A + B,
F1: FnMut<for<effect> = A>() -> O,
F2: FnMut<for<effect> = B>() -> O
{ ... } For instance, this would mean that 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 ( fn foo<O, F1, F2, effect A: for<effect>, effect B: for<effect>>(closure1: F1, closure2: F2) -> O
where
F1: FnMut<for<effect> = A>() -> O,
F2: FnMut<for<effect> = B>() -> O
{ ... } It would be possible to specify bounds for specific effects though, fn foo<effect A: for<effect>, effect async>() { ... }
// or
async fn foo<effect A: for<effect>>() { ... } This is a function that is always async, but generic over every other effect. fn foo<O, F1, F2, effect A: for<effect>, effect B: for<effect>>(closure1: F1, closure2: F2) -> O
where
effect async = A + B,
F1: FnMut<for<effect> = A>() -> O,
F2: FnMut<for<effect> = B>() -> O
{ ... } This function would be async if both closures are |
@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! |
@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. |
In my opinion, the function is impacted directly with optional behaviours. |
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? |
The attribute notation in the draft RFC is intended mainly as a placeholder syntax. It mentions picking a syntax as an unresolved question. |
Ah, I guess that's what I get for just skimming the draft. Makes sense. |
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:
Or (I think) its equivilant
where
I find it a bit difficult to read:
~
is a destructor in c++, bitwise NOT in C and some others, and used to be a heap operator in former Rust. Its usage for "if and only if" relationships is foreign to me and not super intuitive (maybe some other languages use something similar, I'm not aware)~
on its own is kind of a weird character, it's the same width as a letter but sort of visually floats. So it means that if some functions on a page are~async
and some aren't, thefn
keywords misalign just enough to be mildly annoying: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:
And an example with multiple bounds with more complex relationships:
Advantages as I see them:
fn: async if It: async + Cl: async,
says almost perfectly "this function is async if both iteratorIt
and closureCl
are async".!panics
/!panicking
here), and it still looks visually consistentfn: async if Cl: const
orfn: async if I: Sync
. I can't really visualize a use case for it, but at least it's possible.fn: const if Self::get_or_init: const
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¢.
The text was updated successfully, but these errors were encountered: