Skip to content

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

@tgross35

Description

@tgross35

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:

  • To somebody new to the syntax, there's nothing that really indicates the "asyncness" of the function is related to the asyncness of the iterator and closure.
  • ~ 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, the fn keywords misalign just enough to be mildly annoying:
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:

  • I find this to be much more expressive: something like fn: async if It: async + Cl: async, says almost perfectly "this function is async if both iterator It and closure Cl are async".
  • There's no problem adding >1 keyword bound (async and const, as well as the fake !panics / !panicking here), and it still looks visually consistent
  • More nuanced trait bounds are possible, like fn: async if Cl: const or fn: async if I: Sync. I can't really visualize a use case for it, but at least it's possible.
  • This could likely be leveraged in some way with traits / associated functions. fn: const if Self::get_or_init: const
  • It's more familiar and doesn't look like something from a different programming language, and it's no problem to add >1 keyword bound. We already express both trait bounds and lifetimes in this way, why not just extend it to express keyword things?

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¢.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions