1. 15
  1. 13

    Graydon Hoare’s feedback on this proposal is worth a read. Excerpt:

    IMO this whole effort, while well-meaning, is an unwise direction. Writing two different copies of things when they are as fundamentally different as sync and async versions of a function is not bad. Trying to avoid the few cases that are just block_on wrappers aren’t worth the cost to everyone else by pursuing this sort of genericity. At some point adding more degrees of genericity is worse than partially-duplicated but actually-different bodies of code. This initiative greatly overshoots that point.

    1. 5

      The whole Reddit comment section went pretty wild over this one. https://www.reddit.com/r/rust/comments/119y8ex/keyword_generics_progress_report_february_2023/

      I’m naive to this problem space, coming from Ruby which has a global interpreter lock (but I’m very experienced with threading in both Ruby and somewhat with C).

      I would say that Gordon’s take seems reasonable that there’s not a ton of cases where both can be easily represented in the same interface. Still I think that there are cases where authors do want to provide one function and have it handle both and I would like it if their lives were easier.

      I don’t love having question mark for try operator and for something else. I like that a question mark now jumps out in my mind now when I read code. I actually originally thought in the example the “await?” call was part of the new syntax rather than handling an err. It was a bit confusing.

      My original reaction to the example code and my naive reaction to the problem was: why not just let “impl async Read” accept a Read by default too. If you can rationalize that a sync function is a special case of an async function (one that always awaits immediately) then you could treat them the same). That would avoid need for new syntax.

      Thinking about it though, a function author might be okay with that some times but in other times want to force only allowing async inputs in the way they could specify they only want a blocking Read. I believe this is what the color problem is talking about (correct me if I’m wrong).

      Again, naively speaking, if async is a behavior essentially meaning it has an await or async method on it another reaction was, why not make Async a trait then you could require “impl Read + Async” and also have another trait like “MaybeAsync” which would indicate there’s a variant of the same type that also implements the same interface but async. I’m sure there’s a good reason, but it’s not obvious to me now.

      I’m working through the Atomics book now and I’ve not written any async code in Rust yet. I’m happy lots of smart people seem to be thinking about these problems. I’m also learning a lot.

    2. 1

      The is_async() branching is maybe the worst offender here. Ideally real code would make little use of it. The more Java I write, the more I recognize that function coloring would be useful, and that different colored functions will inevitably have different implementations. I feel like the Keyword Generics Initiative needs to better explain why function coloring in Rust is a problem that needs to be solved. Bob Nystrom’s post from 2015 proclaims Java as being in the right, which I find pretty insane:

      Wanna know one that doesn’t [have function coloring]? Java. I know right? How often do you get to say, “Yeah, Java is the one that really does this right.”? But there you go. In their defense, they are actively trying to correct this oversight by moving to futures and async IO. It’s like a race to the bottom.

      If there is some kind of cohesive framework for building optionally colored functions, I don’t think it includes prepending every kind of color to your declarations.

      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> { .. }

      const Read, async Read, and Read feel so fundamentally different that declaring them as one trait looks incorrect. Same goes for the optional async fields:

      struct ?async File {
          file_descriptor: std::os::RawFd,  // shared field in all contexts
          async waker: Waker,               // field only available in async contexts
          !async meta: Metadata,            // field only available in non-async contexts

      Here, we’re really looking at two different structs overlapping. This kind of polymorphism feels useless to me, and would only get worse for structs with ?const ?async. There are a couple examples the KGI has given for why they’re going down this route, but I want to see large real world examples where this would dramatically shrink the amount of code written. Are there crates out there that are literally copy/pasting everything for normal, const, & async contexts? Or is this initiative only for a few implementations?

      An example use case KGI gives is map and some variations: map, async_map, try_map, & async_try_map.

      That’s a lot of API surface for just a single method, and that problem multiplies across the entire API surface in the stdlib. We expect that once we start applying “keyword generics” to traits, we will be able to solve the sandwich problem.

      If the implementation for a ?async ?const ?try map would contain branching with is_async(), is_const(), or is_try(), I would say they are effectively different functions that should have different names. The API surface would be larger, but I don’t see that as an issue.

      I’m not a large user of async Rust, so this could be a solution to the biggest problem those users are facing. I’m curious to see how the RFCs pan out. If library authors can use these features intelligently (which imo includes avoiding is_async and optional fields), then this could be a win for async cohesion.

      1. 1

        Isn’t this a retrofitting of an effect system over rust? Does it fit nicely into the existing type system?