1. 9
  1. 2

    Nice read. Thanks!

    OT, but how did you get the notes in the sidebar? I like it a lot. I was wondering if you used a third-party library.

    1. 4

      Thanks! I hand-rolled that feature =)

      Basically, I slice the page into multiple “rows”, where the left side of the row is text, and the right side is a list of sidenotes. One can see it more clearly when appending ?nojs to the URL. The left side of each section is white, and the right side of each section is blue. It only looks like the page is two columns.

      It’s nice because it still looks good when javascript is disabled. But if javascript is available, then it goes in and better aligns the notes with their “anchors” in the actual text.

      There probably are libraries out there for it. I haven’t found any, but then again, I probably didn’t look too hard!

      1. 1

        Neat!

        I’m on about my 12th iteration of blogging, and currently I’m trying to let ghost do all of the heavy lifting while I just keep a server running and patched. It’s funny: I could get into the templates and hack around a bit, but it’d would be just as easy (or easier) to hand-roll something myself.

        It’s a nice look, cool idea, and I love the noscript fallback!

    2. 2

      The opening sort of intertwines simple concurrent dispatch into the definition of “structured concurrency”. The latter isn’t about how easy it is to make things parallel, but instead having constructs which automatically wait for concurrently dispatched work to complete in a structured manner.

      1. 1

        Great point, I focused on the benefit but didn’t really include that kind of definition. Adding now, thanks!

      2. 2

        What is the idiomatic way to get data out of a thread in this model? Or to gather outputs from each iteration of a parallel foreach?

        1. 2

          If we produce a value from the body of the foreach, then they’ll be assembled into a list, that we can capture in a local:

            results =
              foreach i in range(0, 5) {
                pow(i, exponent)
              };
          

          Part 2 will also explore using mutexes and channels; threads can all write to the same channel, so we can get data from them earlier.

        2. 1

          @Verdagon, it seems to me that your final Vale example is very similar to how Pony would look like if it allowed you await a message, instead of relying on callbacks. If you make input a val reference then you can safely reference it from any other actor.

          One question about read-only regions, though. Does the Vale compiler warn you if a reference is both used as read-only and as a regular reference at the same time?

          1. 1

            The article claims that Rust doesn’t have seamless structured concurrency. Rayon (and crossbeam’s scoped threads under the hood) solves most of this problem.

            We can use its trait implementations:

            use rayon::iter::*;
            

            Define some expensive functions:

            /// A number p is prime if iff: (p-1)! mod p = p-1
            /// (apparently this is called "Wilson's theorem")
            fn is_prime(p: u64) -> bool {
                factorial_mod_n(p - 1, p) == p - 1
            }
            
            fn factorial_mod_n(k: u64, n: u64) -> u64 {
                (1..=k).reduce(|acc, item| (acc * item) % n).unwrap()
            }
            

            And then pretty quickly max out every core on your computer finding primes.

            fn main() {
                // Find primes up to N.
                const MAX: u64 = 200000;
            
                // This works.
                let primes: Vec<u64> = (2..=MAX).into_par_iter().filter(|&n| is_prime(n)).collect();
            }
            

            We can even reference data on the stack. This works because crossbeam’s “scoped threads” library enforces that the thread joins back into its parent thread before the reference becomes invalid.

                let my_cool_prime = 503;
                primes.as_slice().par_iter().for_each(|&prime| {
                    if prime == my_cool_prime {
                        println!("found my prime!");
                    }
                });
            
            1. 3

              As mentioned in sidenote 10, one can only sometimes reference data on the stack in parent scopes, only things that happen to be Sync. One will often find that their data doesn’t implement Sync, because of some trait object buried within. In that situation, one has to refactor to make it Sync somehow.

              If one has to refactor existing code, it’s not seamless, IMO.

              1. 2

                Something not being Sync usually means it uses interior mutability without a mutex. It’s pretty hard to make something magically have a mutex in it; even if you can, it’s a 50/50 chance that it’ll be heavily contested and you’ll need to restructure it anyway. The other main time I’ve had something not be Sync is when it contains an OS resource or thread-local or such that is explicitly not allowed to be shared between threads, and you generally can’t fix that just by find-and-replacing it with Mutex<T>.

                Sometimes complicated things are just complicated, I guess.

                1. 1

                  See the Rust example linked from the article (I think it was in a side note), something as simple as having a regular trait object somewhere in your data is enough to make something not Sync.

                  1. 1

                    Trait objects can be sync if you write the type as dyn Trait + Sync.

                    This needs to be written explicitly, because otherwise the trait allows non-sync implementations, and you could have a data race.

                    1. 1

                      Yep, that’s precisely the issue that’s in the way of Rust’s seamless concurrency.

                      1. 2

                        Oh, come on. If it doesn’t accept buggy code it’s not seamless? Running of single-threaded code on multiple threads should not be “seamless”.

                        1. 1

                          Excuse me? I didn’t say anything about accepting buggy code.

                          1. 2

                            You’ve complained that non-Sync (i.e. non-thread-safe) trait objects are prevented from being used in multi-threaded code. If they were allowed, that would be unsound, and “seamlessly” allow data race bugs.

                            Just to be clear: Rust trait objects can be compatible with multi-threaded code. You don’t need to eliminate them or work around them. They just come in two flavors: thread-safe and non-thread-safe. If you have a non-thread-safe trait object, then of course you can’t “seamlessly” use it, as that would be incorrect. But when you have dyn Trait + Send + Sync, it just works in both single-threaded and multi-threaded contexts.

                            1. 1

                              That’s a pretty big misinterpretation of my words. I’m happy to clarify what I’ve said.

                              I am not suggesting any changes to Rust to make safe seamless concurrency possible; I’m not sure that’s even possible, without introducing a lot of complexity to the language. I’m not saying it should change its rules to allow use of non-Sync/non-Send data in multi-threaded code. I’m not saying anything at all about changing Rust.

                              I’m simply stating that, because Rust requires refactoring existing code (for example deeply changing trait objects and other existing data to be Sync), it cannot do this seamless concurrency. If you’re really curious about the root cause of why Rust requires Sync and Send, and therefore why it can’t do seamless concurrency, let me know and I can enlighten. All I mean so far is that Rust cannot do this, because of its own decisions and limitations.

                              1. 2

                                But by this definition, languages that care about correctness of multi-threaded code can’t be “seamless”. Because otherwise you can always be in a situation where you’re dealing with code that either actually isn’t thread-safe, or incorrectly declares itself to be thread-unsafe, and you need to fix it instead of having compiler ignore the problem and let you run it anyway. From Rust’s perspective the refactoring you’re describing is a bug fix, because this situation would not happen in properly written code.

                                1. 1

                                  That’s not true; the article shows an example where a language can safely have multiple threads concurrently reading the same data, without refactoring.

                                  It also links to an example showing that it is not possible to do safely in Rust, without refactoring. Given that it is possible, but not in Rust, it indicates that the problem is with Rust here.

                                  From Rust’s perspective the refactoring you’re describing is a bug fix, because this situation would not happen in properly written code.

                                  Yes, if we tried this in Rust, it would be a bug in that Rust code, because Rust can’t safely express this pattern. That doesn’t mean it would be a bug in other languages.

                                  1. 2

                                    You’re disqualifying Rust for not being able to safely express this pattern? But you include OpenMP, which doesn’t even have these guarantees in the first place? How is that not favoring lack of safety checks as seamlessness?

                                    1. 1

                                      I see you’re bringing up OpenMP suddenly, when our conversation so far hasn’t been comparing C to Rust, it has been about safe languages.

                                      Perhaps our conversation’s context didn’t make it clear enough, so I’ll be more specific: Rust cannot support seamless fearless structured concurrency, but the article shows that we can have seamless fearless structured concurrency in another language, such as Vale.

                                      I’m sure you’re discussing in good faith, and not intentionally straw-manning what I’m saying, so I’m happy to keep clarifying. I always enjoy exploring what’s possible in language design with those who are honest and curious.

                                      1. 1

                                        I’m just contesting what can be called “seamless”, which is more of argument about semantics of the word, and tangential to the Vale language, so it’s probably not worth digging this further. Thank you for your responses.

                    2. 1

                      Yeah, it would be interesting to have trait objects follow the same rules as deriving traits, same way that new types are automatically Send+Sync+Drop when they can be unless you specify otherwise. ie, saying dyn Trait usually means dyn Trait+Send+Sync+Drop. It would probably be a breaking change to do that, so will probably never happen, but I wonder whether it’s one of those things would abruptly spiral out of control and make everything in the world break, or whether it would be a minor inconvenience to implement and then mostly just work everywhere.