1. 10
  1.  

  2. 9

    I think we did this already in https://lobste.rs/s/3vt0s6/why_asynchronous_rust_doesn_t_work – but I would also note, as many people did then, and with the benefit of additional months writing and using async Rust, that indeed it does work, for myself and many others. It’s not perfect, but things are pretty good and they continue to improve. No programming language is going to meet everybody’s expectations or needs.

    1. 3

      Agreed on both counts; after the minor disaster that was the public reaction to the last post, I wasn’t really going to write another (potentially needlessly inflammatory…) post like it (!).

      Ironically I use async Rust at my day job (and did at the time of writing), so it certainly works; the title was poorly chosen (and probably didn’t lead to people understanding the point I actually wanted to make)

    2. 4

      Reminds me a bit of doing async in C++ using my homebrewed Actor library — C++ lambdas also have weird unnameable types, and dealing with references is also problematic, only you get runtime bugs instead of compile errors.

      The C++ solution to lambda types is type-erased std::function objects; does Rust have an equivalent?

      I deal with reference lifetimes by making liberal use of ref-counted objects. I know Rust has the Arc type, but it seems to be viewed as cheating, or as a last resort?

      I was excited by Rust early on, but after spending a few weeks with it last year, at the same time I was also working in Nim, I decided I preferred the latter. I love the idea of a compiler that understands lifetimes, but not when it’s always up to the puny human to resolve the resulting issues by complexifying the code until it compiles. It seems better to have ref-counting baked into the language, and let the compiler’s lifetime analysis figure out when it can remove the resulting overhead, as Nim (and Lobster) do.

      1. 2

        The C++ solution to lambda types is type-erased std::function objects; does Rust have an equivalent?

        For functions alone, it’s less a problem, because there’s a sugar you can use for the given example if you just want to accept any closure of a given type. To borrow the simple example given:

        fn closure_accepting_function(func: impl Fn(i32)) {
            /* do stuff */
        }
        

        Structs like the example given are a bigger issue; since impl is really just a sugar for the fn foo<F>(f: F) where ... dance, and it’s a problem to have surprise generic bounds on structs, the impl syntax can’t be used. The close equivalent to the std::function solution mentioned above is using Rust’s trait objects via e.g. dyn Fn(i32), which are in C++ parlance, , but these have their own problem - to provide ownership flexibility, the trait object encompasses the value itself, not a reference to that value. The consequence of this is that trait objects are dynamically sized types because the size of that value is unbounded at compile time, which means making one of two[1] compromises:

        • Retain sizedness by storing the trait object as &dyn Fn(i32), which adds seperate generic syntax noise by requiring lifetime bounds to be specified on the struct, and requires that closures passed in always outlive the struct.

        • Retain sizedness by storing the trait object as Box<(dyn Fn(i32)>, which adds no syntactical overhead to the struct and does not introduce the need to manage the struct/closure lifetimes, but forces the struct to always copy the trait object onto the heap (which would include whatever values the closure has captured).

        Using static dispatch with generics is the only way to allow consumers flexibility in the closures/functions they can pass, with almost[2] no downsides. In addition, idiomatic Rust heavily emphasises abstractions where there is no runtime cost (in the codepath, anyway) introduced. As Rust ultimately exists in the same reality as C++, vtables have similar runtime performance implications, whereas the generic struct’s methods will directly specialise to include the given callsites and make the correct stack allocations at compile time, and the struct storage will be sized to hold whatever environment its closure fields are carrying.

        1. Technically it’s possible to also add a bare dyn Fn(i32) to the struct, but there’s no safe way to actually construct DST structs in Rust at this time. DSTs are forbidden to exist on the stack except behind references or boxes, and you can only take a reference of a value already on the heap or stack, and you can only create boxed values by moving them onto the heap from the stack. Even if you could, you now force the closure trait object to be moved when constructing, and you also just end up punting these same decisions onto users of your struct.

        2. It’s worth remembering that generic structs instantiate separate types for different specializations, so there is the tradeoff that this approach means that users of the struct that want to use it as a field, parameter or return type will themselves need to be generic, either directly or through yet more trait indirection.