Unexpected productivity boost of Rust

(lubeno.dev)

332 points | by bkolobara 13 hours ago ago

306 comments

  • nneonneo 8 hours ago ago

    Last year I ported a virtio-host network driver written in Rust, entirely changing its backend, interrupt mechanism and converting it from a library to a standalone process. This is a complex program that handles low-level memory mappings, VM interrupts, network sockets, multithreading, etc.

    What’s remarkable is that (a) I have very little Rust experience overall (mostly a Python programmer), (b) very little virtio experience, and (c) essentially no experience working with any of the libraries involved. Yet, I pulled off the refactor inside of a week, because by the time the project actually compiled, it worked perfectly (with one minor Drop-related bug that was easily found and fixed).

    This was aided by libraries that went out of their way to make sure you couldn’t hold them wrong, and it shows.

    • mmastrac 6 hours ago ago

      I've been writing Rust code for a while and generally if it compiles, it works. There are occasional deadlocks and higher-level ordering issues from time-to-time, but modulo bugs, the compiler succeeding generally means it'll run a decent amount of your project.

      • jabwd 3 hours ago ago

        Though my code complexity is far FAR from what you've been writing it has been a similar experience for me. There are a few footguns and a bug in chrono I still have to find the energy to report or fix; which has been causing a bi-yearly issue, but apart from that happy lil programmer.

  • merdaverse 10 hours ago ago

    Code written below your line gets executed if you don't return early. More breaking news at 8.

    Seriously, why would you think that assigning a value would stop your script from executing? Maybe the Typescript example is missing some context, but it seems like such a weird case to present as a "data race".

    • Arch-TK 10 hours ago ago

      Assigning to `window.location.href` has a side effects. The side effect is that your browser will navigate to wherever you assigned, as if you had clicked a link. This is already a surprising behaviour, but given that this assignment is effectively loading a new page in-place, kind of like how `execve` does for a process, I can totally see how someone would think that JS execution would stop immediately after a link is clicked.

      It's obviously not a good idea to rely on such assumptions when programming, and when you find yourself having such a hunch, you should generally stop and verify what the specification actually says. But in this case, the behaviour is weird, and all bets are off. I am not at all surprised that someone would fall for this.

      • stouset 10 hours ago ago

        Part of the problem is that we unknowingly make a million little assumptions every day in the course of software development. Many of them are reasonable, some of them are technically unreasonable but fine in practice, and some of them are disasters waiting to happen. And it's genuinely hard to not only know which are which, but to notice even a fraction of them in the first place.

        I'm sure I knew the href thing at one point. It's probably even in the documentation. But the API itself leaves a giant hole for this kind of misunderstanding, and it's almost certainly a mistake that a huge number of people have made. The more pieces of documentation we need to keep in our heads in order to avoid daily mistakes, the exponentially more likely it is we're going to make them anyway.

        Good software engineering is, IMHO, about making things hard to hold the wrong way. Strong types, pure functions without side effects (when possible), immutable-by-default semantics, and other such practices can go a long way towards forming the basis of software that is hard to misuse.

      • ngruhn 9 hours ago ago

        I use JavaScript for ~15 years. I thought it worked like that.

        • svieira 9 hours ago ago

          I'm pretty sure it did used to work the other way. Even if it didn't something changed recently so that the "happen later" behavior was significantly more likely to be encountered in common browsers.

      • dkarl 9 hours ago ago

        > when you find yourself having such a hunch, you should generally stop and verify what the specification actually says

        It greatly heartens me that we've made it to the point where someone writing Javascript for the browser is recommended to consult a spec instead of a matrix of browsers and browser versions.

        However, that said, why would a person embark on research instead of making a simple change to the code so that it relies on fewer assumptions, and so that it's readable and understandable by other programmers on their team who don't know the spec by heart?

      • hsbauauvhabzb 25 minutes ago ago

        Is this a JavaScript wart or a browser wart though? JavaScript is communicating to the browser via an API and rust would need to do the same.

    • Humphrey 4 hours ago ago

      Whether you think that or not is not the issue - the fix is very obvious once pointed out to you. The arguement the author is making is that a bug like that TS issue can be very difficult and time consuming to track down and is not picked up on by the compiler.

    • lights0123 10 hours ago ago

      exit(), execve(), and friends do immediately stop execution—I could understand why you'd think a redirect would as well.

      • JoshTriplett 10 hours ago ago

        Exactly. Given that JavaScript runs in the context of a page, redirecting off of the page seems like it should act like a "noreturn" function...but it doesn't. That seems like a very easy mistake to make.

      • dvt 10 hours ago ago

        The redirect is an assignment. In no language has a variable assignment ever stopped execution.

        • JoshTriplett 10 hours ago ago

              $ python3
              Python 3.13.7 (main, Aug 20 2025, 22:17:40) [GCC 14.3.0] on linux
              Type "help", "copyright", "credits" or "license" for more information.
              >>> class MagicRedirect:
              ...     def __setattr__(self, name, value):
              ...         if name == "href":
              ...             print(f"Redirecting to {value}")
              ...             exit()
              ... 
              >>> location = MagicRedirect()
              >>> location.href = "https://example.org/"
              Redirecting to https://example.org/
              $
          • dvt 7 hours ago ago

            You're overloading a setter here. It's cute, I did it in JS as well, but I don't really think it's a counterexample. It would be odd to consider this the norm (per the thought process of the original blog post).

            • dminik 7 hours ago ago

              But window.location.href is already an overloaded setter. It schedules a page navigation.

            • rowanG077 7 hours ago ago

              This is not some weird thing. Here is a run of the mill example where python can have properties settting do anything at all. And it's designed like that.

                  import sys
                  
                  class Foo:
                      @property
                      def bar(self):
                          return 10
                          
                      @bar.setter
                      def bar(self, value):
                          print("bye")
                          sys.exit()
                  
                  foo = Foo()
                  foo.bar = 10
              
              Or in C# if you disqualify dynamic languages:

                  using System;
              
                  class Foo
                  {
                      public int Bar
                      {
                          get { return 10; }
                          set
                          {
                              Console.WriteLine("bye.");
                              Environment.Exit(0);
                          }
                      }
                  }
              
                  class Program
                  {
                      static void Main()
                      {
                          Foo obj = new Foo();
                          obj.Bar = 10;
                      }
                  }
              
              
              This is not some esoteric thing in a lot of programming languages.
              • dvt 5 hours ago ago

                You're also overriding a setter. Maybe I'm going against the grain here, but it's absolutely esoteric. The assignment operator is not supposed to have side-effects, and maybe this is the logician in me, but the implication that we should be aware that weird stuff might be happening when we do `x = 5` is fundamentally bonkers.

                • JoshTriplett 5 hours ago ago

                  You started with "In no language has a variable assignment ever stopped execution", and now you're saying "The assignment operator is not supposed to have side-effects". location.href is a counterexample, and there are many counterexamples throughout various tools and languages and libraries. Deciding how you think things should work does not affect how things do work, and it's important to understand the latter. (I do agree it's bad practice, but it happens and people do not always fully control the environments they must work with.)

                  And given that location.href does have a side effect, it's not unreasonable for someone to have assumed that that side effect was immediate rather than asynchronous.

                  That said, if you don't like working with such languages, that's all the more reason to select languages where that doesn't happen, which comes back to the point made in the article.

                  • dvt 5 hours ago ago

                    > You started with "In no language has a variable assignment ever stopped execution",

                    The irony is that I'm still technically correct, as literally every example (from C++, to C#, to Python, to JS) have been object property assignments abusing getters and setters—decidedly not variable assignments (except for the UB example).

                    • kketch 3 hours ago ago

                      Technically no. Producing side effects from a setter is not unexpected, even if it often the best idea to have a setter have a lot of unexpected side effects. However producing side effects from getters is definitely unexpected and should not be done. Interestingly it's one of the areas where rust is really useful, it forces you express your intent in terms of mutability and is able to enforce these expectations we have.

                      • galangalalgol 2 hours ago ago

                        Overloading assignment operators to maintain an invariant is one thing, but this particular case of it running off and effectively doing ionis weird to me coming from an embedded c++ background. I don't like operator overloading and think it should be avoided, just to make my bias obvious. I don't code c++ anymore either, rust and no looking back for a few years now.

                    • rowanG077 4 hours ago ago

                      The entire discussion is about a property assignment. Which in colloquial usage is also called variable assignment. Which is obvious since nobody corrected you on that. You now trying to do a switcheroo is honestly ridiculous.

                      • dvt 4 hours ago ago

                        The entire discussion is about “=“ doing weird stuff, which in 99.9% of cases it does not do. And my point was that no language, without doing weird stuff (like overloading), does not let “=“ do weird stuff (and thus is pure). The counterarguments all involve nonstandard contracts. Therefore, thinking that using “=“ will have some magical side-effect is absolutely never expected by default.

                        • scheme271 26 minutes ago ago

                          So, with anything that isn't a primitive type (e.g. int, bool, etc), there's a chance that assignment is going to require memory allocation or something similar. If that's the case then there's a chance of bad things happening (e.g. a out of memory error and the program being killed).

                          More commonly, if you look at things like c++'s unique_ptr, assignment will do a lot of things in the background in order to keep the unique_ptr properties consistent. Rust and other languages probably do similar things with certain types due to semantic guarantees.

                        • JoshTriplett 3 hours ago ago

                          > The counterarguments all involve nonstandard contracts. Therefore, thinking that using “=“ will have some magical side-effect is absolutely never expected by default.

                          That sounds like a recipe for having problems every time you encounter a nonstandard contract. Are you actually saying you willfully decide never to account for the possibility, or are you conflating "ought not to be" with "isn't"?

                          If I'm programming in a language that has the possibility of properties, it's absolutely a potential expectation at any time. Which is one reason I don't enjoy programming in such languages as much.

                          To give a comparable example: if I'm coding in C, "this function might actually be a macro" is always a possibility to be on guard against, if you do anything that could care about the difference (e.g. passing the function's name as a function pointer).

                • kketch 3 hours ago ago

                  assignments are side effects, even more so when they are done through a setter on an object / class instance

        • AdieuToLogic 3 hours ago ago

          > The redirect is an assignment. In no language has a variable assignment ever stopped execution.

          Many languages support property assignment semantics which are defined in terms of a method invocation. In these languages, the method invoked can stop program execution if the runtime environment allows it to do so.

          For example, source which is defined thusly:

            foo.bar = someValue
          
          Is evaluated as the equivalent of:

            foo.setBar (someValue)
        • ordu 9 hours ago ago

          Try this in C:

          *(int*)0 = 0;

          Modern C compilers could require you to complicate this enough to confuse them, because their approach to UB is weird, if they saw an UB they could do anything. But in olden days such an assignment led consistently to SIGSEGV and a program termination.

          • DannyBee 9 hours ago ago

            Unless you were on systems that mapped address 0 to a writable but always zero value so they could do load and store speculation without worry.

            IBM did this for a long time

            • mabster 7 hours ago ago

              My favourite were older embedded systems where 0 was an address you actually do interact with. So for some portion of the code you WANT null pointer access. I can't remember the details but I do remember jumping to null to reset the system being pretty common.

              • marshray 6 hours ago ago

                Probably the system interrupt table. Index 0 might reference the handler for the non-maskable interrupt NMI, often the same as a power-on reset.

                I recall that on DOS, Borland Turbo C would detect writes to address 0 and print a message during normal program exit.

        • kji 2 hours ago ago

          In Blink setHref is automatically bound to C++ code [1]. I think it's fair to say that anything goes.

          [1]: https://source.chromium.org/chromium/chromium/src/+/main:thi...

        • dminik 10 hours ago ago

          That doesn't seem that obvious to me. You could have a setter that just calls exit and terminates the whole program.

          • dvt 10 hours ago ago

            Yeah, this is actually a good point, could have a custom setter theoretically that simply looks like assignment, but does some fancy logic.

                const location = {
                  set current(where) {
                    if (where == "boom") {
                        throw new Error("Uh oh"); // Control flow breaks here
                    }
                  }
                };
            
                location.current = "boom" // exits control flow, though it looks like assignment, JS is dumb lol
        • lock1 10 hours ago ago

          You could overload operator=() in C++ with a call to exit(), which fulfills "variable assignment that halts the program".

          • BobbyJo 7 hours ago ago

            idk if I'd consider overloading the assignment operator to call a function, then using it, actually an assignment in truth.

          • dvt 10 hours ago ago

            I was ignoring these kinds of fancy overload cases, but even in JS you can mess with setters to get some unexpected behavior (code below).

    • love2read 10 hours ago ago

      It seems weird to shame someone for talking about their own experience?

    • drdrey 9 hours ago ago

      OP thought the redirect was synchronous, not that it would stop the script from executing

      • jibal 9 hours ago ago

        No, you're mistaken. Read the other comments under the parent. If it were synchronous then it would have stopped the script from executing, much the way POSIX exec() works. If the OP didn't think that the script would stop, then why would he let execution fall through to code that should not execute ... which he fixed by not letting it fall through?

        • drdrey 7 hours ago ago

          I see, thanks

    • jemiluv8 10 hours ago ago

      This is more a control flow issue than a data race issue. I've seen this countless times. And it is often a sign that you don't spend too much time writing JavaScript/Typescript. You get shot in the foot by this very often. And some linters will catch this - most do actyally

    • IshKebab 6 hours ago ago

      Whenever someone talks about a surprising paper cut like this you always see misguided "this is obvious" comments.

      No shit. It's obvious because you literally just read a blog post explaining it. The point is if you sprinkle dozens of "obvious" things through a large enough code based, one of them is going to bite you sooner or later.

      It's better if the language helps you avoid them.

  • BinaryIgor 11 hours ago ago

    Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

    I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

    • pornel 10 hours ago ago

      To a large extent yes, but Rust adds more dimensions to the type system: ownership, shared vs exclusive access, thread safety, mutually-exclusive fields (sum types).

      Ownership/borrowing clarifies whether function arguments are given only temporarily to view during the call, or whether they're given to the function to keep and use exclusively. This ensures there won't be any surprise action at distance when the data is mutated, because it's always clear who can do that. In large programs, and when using 3rd party libraries, this is incredibly useful. Compare that to that golang, which has types for slices, but the type system has no opinion on whether data can be appended to a slice or not (what happens depends on capacity at runtime), and you can't lend a slice as a temporary read-only view (without hiding it behind an abstraction that isn't a slice type any more).

      Thread safety in the type system reliably catches at compile time a class of data race errors that in other languages could be nearly impossible to find and debug, or at very least would require catching at run time under a sanitizer.

      • bpicolo 2 hours ago ago

        I think tagged unions with exhaustive type checking and no nulls are the two killer features for correctness

      • zelphirkalt 10 hours ago ago

        What annoys me about borrowing is, that my default mode of operating is to not mutate things if I can avoid it, and I go to some length in avoiding it, but Rust then forces me to copy or clone, to be able to use things, that I won't mutate anyway, after passing them to another procedure. That creates a lot of mental and syntactical overhead. While in an FP language you are passing values and the assumption is already, that you will not mutate things you pass as arguments and as such there is no need to have extra stuff to do, in order to pass things and later still use them.

        Basically, I don't need ownership, if I don't mutate things. It would be nice to have ownership as a concept, in case I do decide to mutate things, but it sucks to have to pay attention to it, when I don't mutate and to carry that around all the time in the code.

        • vlovich123 9 hours ago ago

          It sounds like you may not actually know Rust then because non-owning borrow and ownership are directly expressible within the type system:

          Non-owning non mutating borrow that doesn’t require you to clone/copy:

              fn foo(v: &SomeValue)
          
          Transfer of ownership, no clone/copy needed, non mutating:

              fn foo(v: SomeValue)
          
          Transfer of ownership, foo can mutate:

              fn foo(mut v: SomeValue)
          
          
          AFAIK rust already supports all the different expressivity you’re asking for. But if you need two things to maintain ownership over a value, then you have to clone by definition, wrapping in Rc/Arc as needed if you want a single version of the underlying value. You may need to do more syntax juggling than with F# (I don’t know the language so I can’t speak to it) but that’s a tradeoff of being a system engineering language and targeting a completely different spot on the perf target.
          • zelphirkalt 9 hours ago ago

            Can you give examples of the calls for these procedures? Because in my experience when I pass a value (not a reference), then I must borrow the value and cannot use it later in the calling procedure. Passing a reference of course is something different. That comes with its own additional syntax that is needed for when you want to do something with the thing that is referred to.

            • Tuna-Fish 8 hours ago ago

              > Because in my experience when I pass a value (not a reference), then I must borrow the value and cannot use it later in the calling procedure.

              Ah, you are confused on terminology. Borrowing is a thing that only happens when you make references. What you are doing when you pass a non-copy value is moving it.

              Generally, anything that is not copy you pass to a function should be a (non-mut) reference unless it's specifically needed to be something else. This allows you to borrow it in the callee, which means the caller gets it back after the call. That's the workflow that the type system works best with, thanks to autoref having all your functions use borrowed values is the most convenient way to write code.

              Note that when you pass a value type to a function, in Rust that is always a copy. For non-copy types, that just means move semantics meaning you also must stop using it at the call site. You should not deal with this in general by calling clone on everything, but instead should derive copy on the types for which it makes sense (small, value semantics), and use borrowed references for the rest.

              • zelphirkalt 8 hours ago ago

                It is not possible then to pass a value (not a reference) and not implement or derive Copy or Clone, if I understand you correctly. That was my impression earlier. Other languages let you pass a value, and I just don't mutate that, if I can help it. I usually don't want to pass a reference, as that involves syntactical "work" when wanting to use the referenced thing in the callee. In many other languages I get that at no syntactical cost. I pass the thing by its name and I can use it in the callee and in the caller after the call.

                What I would prefer is, that Rust only cares about whether I use it in the caller after the call, if I pass a mutable value, because in that case, of course it could be unsafe, if the callee mutates it.

                Sometimes Copy cannot be derived and then one needs to implement it or Clone. A few months ago I used Rust again for a short duration, and I had that case. If I recall correctly it was some Serde struct and Copy could not be derived, because the struct had a String or &str inside it. That should a be fairly common case.

                • steveklabnik 7 hours ago ago

                  Yes, Rust will not automatically turn a value into a reference for you. A reference is the semantic you desire. If you have a value, you’re gonna have to toss & on it. That’s the idiomatic way to do this, not to pass a value and clone it.

                  &str is Copy, String is not.

                • Tuna-Fish 7 hours ago ago

                  You can pass a value that is neither copy or clone, but then it gets moved into the callee, and is no longer available in the caller.

                  Note that calling by value is expensive for large types. What those other languages do is just always call by reference, which you seem to confuse for calling by value.

                  Rust can certainly not do what you would prefer. In order to typecheck a function, Rust only needs the code of that function, and the type defitions of everything else, the contents of the functions don't matter. This is a very good rule, which makes code much easier to read.

                • vlovich123 7 hours ago ago

                  > Other languages let you pass a value, and I just don't mutate that, if I can help it

                  How do they do that without either taking a reference or copying/cloning automatically for you? Would be helpful if you provide an example.

                  • zelphirkalt 4 hours ago ago

                    I did not state, that they don't automatically copy or clone things.

                    I might be wrong what they actually do though. It seems I merely dislike the need to specify & for arguments and then having to deal with the fact, that inside procedures I cannot treat them as values, but need to stay aware, that they are merely references.

                    • const_cast 2 hours ago ago

                      C++ auto copies as well, it's just a feature of value semantics. References must be taken manually - versus Java or C#, where we assume reference and then have to explicitly say copy. Rust, I believe, usually moves by default - not copy, but close - for most types.

                      The nice thing about value semantics is they are very safe and can be very performant. Like in PHP, if we take array that's a copy. But not really - it's secretly COW under the hood. So it's actually very fast if we don't mutate, but we get the safety of value semantics anyway.

                • theLiminator 5 hours ago ago

                  The pattern you're looking for is:

                  ``` fn operate_on_a(a: A) -> A { // do whatever as long as this scope still owns A a } ```

            • vlovich123 7 hours ago ago

              If all you're doing is immutable access, you are perfectly free to immutably borrow the value as many times as you want (even in a multithreaded environment provided T is Send):

                  let v = SomeValue { ... }
                  foo(&v);
                  foo(&v);
                  eprintln!("{}", v.xyz);
              
              You have to take a reference. I'm not sure how you'd like to represent "I pass a non-reference value to a function but still retain ownership without copying" - like what if foo stored the value somewhere? Without a clone/copy to give an isolated instance, you've potentially now got two owners - foo and the caller of foo which isn't legal as ownership is strictly unique. If F# lets you do this, it's likely only because it's generating an implicit copy for you (which Rust is will do transparently for you when you declare your type inheriting Copy).

              But otherwise I'm not clear what ownership semantics you're trying to express - would be helpful if you could give an example.

        • pornel 10 hours ago ago

          Borrowing isn't for mutability, but for memory management and limiting data access to a static scope. It just happens that there's an easy syntax for borrowing as shared or exclusive at the same time.

          Owned objects are exclusively owned by default, but wrapping them in Rc/Arc makes them shared too.

          Shared mutable state is the root of all evil. FP languages solve it by banning mutation, but Rust can flip between banning mutation or banning sharing. Mutable objects that aren't shared can't cause unexpected side effects (at least not any more than Rust's shared references).

        • arnsholt 10 hours ago ago

          Ownership serves another important purpose: it determines when a value is freed.

          • zelphirkalt 9 hours ago ago

            I guess it is then a necessary complication of the language, as it doesn't have garbage collection, and as such doesn't notice, when values go out of scope of all closures that reference them?

            • steveklabnik 7 hours ago ago

              Yes, but it’s more subtle than that. What Rust does is track when the object goes out of scope, and will make sure that any closures that reference it live for a shorter time than that. Sort of backwards of what you’re asking.

        • timeon 10 hours ago ago

          > While in an FP language you are passing values

          By passing values do you mean 'moving'? Like not passing reference?

          • zelphirkalt 9 hours ago ago

            Yes, I guess in Rust terms, that is called moving. However, when I have some code that "moves" the value into another procedure, then the code after that call, can no longer use the moved value.

            So I want to move a value, but also be able to use it after moving it, because I don't mutate it in that other function, where it got moved to. So it is actually more like copying, but without making a copy in memory.

            It would be good, if Rust realized, that I don't have mutating calls anywhere and just lets me use the value. When I have a mutation going on, then of course the compiler should throw error, because that would be unsafe business.

            • asa400 8 hours ago ago

              I'm not sure how what you're describing is different from passing an immutable/shared reference.

              If you call `foo(&value)` then `value` remains available in your calling scope after `foo` returns. If you don't mutate `value` in foo, and foo doesn't do anything other than derive a new value from `value`, then it sounds like a shared reference works for what you're describing?

              Rust makes you be explicit as to whether you want to lend out the value or give the value away, which is a design decision, and Rust chooses that the bare syntax `value` is for moving and the `&value` syntax is for borrowing. Perhaps you're arguing that a shared immutable borrow should be the default syntax.

              Apologies if I'm misunderstanding!

            • NIckGeek 8 hours ago ago

              Couldn't you just pass a reference to your value (i.e. `&T`)? If you absolutely _need_ ownership the function you call could return back the owned value or you could use one of the mechanisms for shared ownership like `Rc<T>`. In a GC'd functional language, you're effectively getting the latter (although usually a different form of GC instead of reference counting)

              • zelphirkalt 8 hours ago ago

                I think I could. But then I would need to put & in front of every argument in every procedure definition and also deal with working with references inside the procedure, with the syntax that brings along.

                • Mond_ 7 hours ago ago

                  Fair to be annoyed by this, but not very interesting: This is just a minor syntactical pattern that exists for a very good reason.

                  Syntax is generally the least interesting/important part of languages.

                • pepa65 3 hours ago ago

                  When you pass &variable, I don't think it affects the syntax inside the called function, does it?

                  • asa400 2 hours ago ago

                    Correct. If you then want to subsequently re-reference or dereference that reference (this happens sometimes), you'll need to accordingly `&` or `*` it, but if you're just using it as is, the bare syntactical `name` (whatever it happens to be) already refers to a reference.

      • w10-1 10 hours ago ago

        Readers would benefit from distinguishing effects systems from type systems - error handling, async code, ownership, pointers escaping, etc. are all better understood as effects because they pertain to usage of a value/type (though the usage constraints can depend on the type properties).

        Similarly, Java sidesteps many of these issues in mostly using reference types, but ends up with a different classes of errors. So the C/pointer family static analysis can be quite distinct from that for JVM languages.

        Swift is roughly on par with Rust wrt exclusivity and data-race safety, and is catching up on ownership.

        Rust traits and macros are really a distinguishing feature, because they enable programmer-defined constraints (instead of just compiler-defined), which makes the standard library smaller.

        • ModernMech 10 hours ago ago

          Swift has such a long way to go in general ergonomics of its type system, it's so far behind compared to Rust. The fact that the type checker will just churn until it times out and asks the user to refactor so that it can make progress is such a joke to me, I don't understand how they shipped that with a straight face.

          • vlovich123 9 hours ago ago

            If you solve 80% of the problems by spending 20% of the time, is it worth spending the 80% to solve 20% of the problems? Or even if it is, is it more valuable to ship the 80% complete solution first to get it into the hands of users while you work on the thornier version?

            • Mond_ 7 hours ago ago

              If someone else ships a 100% solution, or a solution that doesn't have the problems your potentially half-baked "80% solution" does, then you might be in trouble.

              There's a fine line here: it matters a lot whether we're talking about a "sloppy" 80% solution that later causes problems and is incredibly hard to fix, or if it's a clean minimal subset, which restricts you (by being the minimal thing everyone agrees on) but doesn't have any serious design flaws.

              • vlovich123 7 hours ago ago

                Sure. And I'm not sure the type checker failing on certain corner cases and asking you to alter the code to be friendlier is a huge roadblock if it rarely comes up in practice for the vast majority of developers.

        • timeon 10 hours ago ago

          Swift even if catching up a bit is probably not going to impose strict border between safe and unsafe.

    • arwhatever 10 hours ago ago

      I might suspect that if you are lumping all statically-typed languages into a single bucket without making particular distinction among them, then you might not have fully internalized the implications of union (aka Rust enum aka sum) typed data structures combined with exhaustive pattern matching.

      I like to call it getting "union-pilled" and it's really hard to accept otherwise statically-typed languages once you become familiar.

      • JoshTriplett 10 hours ago ago

        Or the fact that Rust's type system includes things like Send and Sync, which aren't tracked and enforced in many otherwise-statically-typed languages.

        C is statically typed, but its type system tracks much less.

        • 1718627440 9 hours ago ago

          My interpretation is, that C doesn't have data types, but memory layout types.

      • b_e_n_t_o_n 9 hours ago ago

        Afaik they aren't true unions but sum types, which have different implications.

        And fwiw I've used unions in typescript extensively and I'm not convinced that they're a good idea. They give you a certain flexibility to writing code, yes, does that flexibility lead to good design choices, idk.

        • kibwen 8 hours ago ago

          TypeScript unions are very different from Rust enums, and they lead to different design decisions. IMO Rust-style tagged unions are essential in any new language coming out today, it's really a pity that people didn't pick up on this in the 70s.

      • ModernMech 10 hours ago ago

        enums + match expressions + tagged unions are the secret sauce of Rust.

        • mixmastamyk 2 hours ago ago

          Maybe I need to read it again, but I remember the Rust book saying… you can use enums like C, but what if instead you used them in this more concise way? (Match on members, without the container.) Ok, was able to proceed but don’t feel like I understand what they really are.

    • jauntywundrkind 8 hours ago ago

      One other major factor I'd throw on the heap: traits / implementation traits. They act as both an interface system and as a sort of Extension Method system (as seen in c#).

      But where-as with interfaces, typically they require you early define what your class implements. Rust gives you a late-bound-ish (still compile time but not defined in the original type) / Inversion of Control way to take whatever you've got and define new things for it. In most languages what types a thing has are defined by the library, but Rust not just allows but is built entirely around taking very simple abstract thing and constructing bigger and bigger toolkits of stuff around them. Very Non-zero sum in ways that languages rarely are.

      There's a ton of similarity to Extension Methods, where more can get added to the type. But traits / impls are built much more deeply into rust, are how everything works. Extension Methods are also, afaik, just methods, where-as with Rust you really adding new types that an existing defined-elsewhere thing can express.

      I find it super shocking (and not because duh) that Rust's borrow checking gets all the focus. Because the type system is such a refreshing open ended late-defined reversal of type system dogma, of defining everything ahead of time. It seems like such a superpower of Rust that you can keep adding typiness to a thing, keep expanding what a thing can do. The inversion here is, imo, one of the real largely unseen sources of glory for why Rust keeps succeeding: you don't need to fully consider the entire type system of your program ahead of time, you can layer in typing onto existing types as you please, as fits, as makes sense, and that is a far more dynamic static type system than the same old highly constrained static type dreck we've suffered for decades. Massive break forward: static, but still rather dynamic (at compile time).

      • dwaltrip an hour ago ago

        Any good articles or blog posts that go deeper on this? Sounds very interesting

    • marcosdumay 10 hours ago ago

      > Don't most of the benefits just come down to using a statically typed and thus compiled language?

      Doesn't have to be compiled to be statically typed... but yeah, probably.

      > Be it Java, Go or C++;

      Lol! No. All static type systems aren't the same.

      TypeScript would be the only one of your examples that brings the same benefit. But the entire system is broken due to infinite JS Wats it has to be compatible with.

      > it's harder to learn and arguably to read

      It's easier to learn it properly, harder to vibe pushing something into it until it seems to works. Granted, vibe pushing code into seemingly working is a huge part of initial learning to code, so yeah, don't pick Rust as your first language.

      It's absolutely not harder to read.

    • saghm 3 hours ago ago

      Yes, all four of them will have some checks that won't be present in a dynamic language, but the differences between them are large enough to be significant. Riding a bike and driving a car are both much faster than going on foot, but if you only view this as a "benefit that comes down to using wheels", you're missing some pretty meaningful details.

    • lmm 4 hours ago ago

      > Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

      No. You have to have a certain amount of basic functionality in your type system; in particular, sum types, which surprisingly many languages still lack.

      (Note that static typing does not require compilation or vice versa)

      > I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

      ML-family languages are generally easier to learn and read if you start from them. It's just familiarity.

    • ModernMech 10 hours ago ago

      > statically typed and thus compiled

      Statically typed does not imply compiled. You can interpret a statically typed language, for instance. And not every compiled language is all that static.

      For example, C is statically typed, but also has the ability to play pointer typecasting trickery. So how much can the compiler ever guarantee anything, really? It can't, and we've seen the result is brittle artifacts from C.

      Rust is statically-typed and it has all kinds of restrictions on what you can do with those types. You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler outright. So Rust code has to meet a higher bar of "static" than most languages that call themselves "static".

      Type casting is just one way Rust does this, other ways have been mentioned. They all add up and the result is Rust artifacts are safter and more secure.

      • tialaramex 10 hours ago ago

        > You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler

        You can't safely do this yourself. That is, you couldn't write safe Rust which performs this operation for two arbitrary things. But Rust of course does do this, actually quite a lot, because if we're careful it's entirely safe.

        That famous Quake 3 Arena "Fast inverse square root" which involves type puns? You can just write that in safe Rust and it'll work fine. You shouldn't - on any even vaguely modern hardware the CPU can do this operation faster anyway - but if you insist it's trivial to write it, just slower.

        Why can you do that? Well, on all the hardware you'd realistically run Rust on the 32-bit integer types and the 32-bit floating types are the exact same size (duh), same bit order and so on. The CPU does not actually give a shit whether this 32-bit aligned and 32-bit sized value "is" an integer or a floating point number, so "transforming" f32 to u32 or u32 to f32 emits zero CPU instructions, exactly like the rather hairier looking C. So all the Rust standard library has to do is promise that this is OK which on every supported Rust platform it is. If some day they adopted some wheezing 1980s CPU where that can't work they'd have to write custom code for that platform, but so would John Carmack under the same conditions.

        • ModernMech 10 hours ago ago

          > because if we're careful it's entirely safe.

          The thesis of Rust is that in aggregate, everyone can't be careful, therefore allowing anyone to do it (by default) is entirely unsafe.

          Of course you can do unsafe things in Rust, but relegating that work to the room at the back of the video store labeled "adults only" has the effect of raising code quality for everyone. It turns out if you put up some hoops to jump through before you can access the footguns, people who shouldn't be wielding them don't, and average code quality goes up.

      • 1718627440 9 hours ago ago

        > So how much can the compiler ever guarantee anything, really?

        Well, the compiler is guaranteed that no mistakes will happen. It's the programmer who looses his guarantees in this case.

    • ViewTrick1002 11 hours ago ago

      Neither Go, Java or C++ would catch that concurrency bug.

      • Const-me 10 hours ago ago

        C# would catch the bug at compile time, just like Rust.

        https://www.rocksolidknowledge.com/articles/locking-asyncawa...

        • IshKebab 6 hours ago ago

          I don't know C# but it looks like they added a specific check just for locks, which is far less powerful than Rust's Send/Sync.

        • notfed 9 hours ago ago

          It's almost as if this post was written in direct response to TFA to brag about how far ahead C# has been for over a decade.

    • rvz 3 hours ago ago

      > Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

      Yes. The type systems of these modern compiled languages are more sound than anything that Javascript and Typescript can ever provide.

      Anyone using such languages that have a totally weak type system and a dynamic typing system as well is going to run into hundreds of headaches - hence why they love properly typed-systems such as Rust which actually is a well designed language.

    • BobbyJo 8 hours ago ago

      IMO, most of the terse syntax in Rust comes from the sugar they've added for error handling.

    • adamnemecek 9 hours ago ago

      Rust is way more productive than any of the listed languages.

    • keybored 10 hours ago ago

      You ask a question in your first paragraph which you answer in the second.

    • stocksinsmocks 5 hours ago ago

      Yes, but more importantly writing a program that compiles in Rust guarantees you a front page spot on HN.

  • raphinou 12 hours ago ago

    Though it's not the only benefit, I enjoy rust and fsharp's typesystems most when refactoring code. Fearless refactoring is the right expression here.

    • estebank 10 hours ago ago

      The only issue with it is that Rust's aversion to half-baked code means that you can't have "partially working code" during the refactor: you either finish it or bail on it, without the possibility to have inconsistent codebase state. This is particularly annoying for exploratory code.

      On the other hand, that strictness is precisely what leads people to end up with generally reasonable code.

      • tialaramex 10 hours ago ago

        I find a healthy dose of todo!() is excellent for this.

            match foo {
              (3...=5, x, BLABLABLA) => easy(x),
              _ => todo!("I should actually implement this for non-trivial cases"),
            }
        
        The nice thing about todo!() is that it type checks, obviously it always diverges so the type match is trivial, but it means this compiles and, so long as we don't cause the non-trivial case to happen, it even works at runtime.
        • estebank 9 hours ago ago

          The thing is I want an equivalent to `todo!()` for the type-system. A mode of operation where if you have some compile errors, you can still run tests. Like for example, if you have

              fn foo() -> impl Display {
                  NotDisplay::new()
              }
          
          and a test references `foo`, then it gets replaced for the purposes of the test with

              fn foo() -> impl Display {
                  panic!("`NotDisplay` doesn't `impl Display`")
              }
          
          This should not be part of the language, but part of the toolchain.

          Same thing for the borrow-checker.

          • tialaramex 9 hours ago ago

            So maybe a proc macro which lets you #[dummy(Clone,Eq,PartialEq)] instead of #[derive(Clone,Eq,PartialEq)] ?

            Although you said "mode of operation" and I can't get behind that idea, I think the choice to just wrap overflow by default for the integer types in release builds was probably a mistake. It's good that I can turn it off, but it shouldn't have been the default.

          • oasisaimlessly 8 hours ago ago

            Sounds like 'Typed Holes'[1] in the Haskell ecosystem.

            [1]: https://downloads.haskell.org/~ghc/7.10.3-rc1/users_guide/ty...

          • empath75 9 hours ago ago

            can't you just put the panic in foo()? That returns !

      • Cthulhu_ 9 hours ago ago

        It's a tradeoff, reminds me of Go not compiling if you have an unused variable; the strictness is a feature and basically locks out sloppy / half baked code.

        I personally see Rust as an ideal "second system" language, that is, you solve a business case in a more forgiving language first, then switch (parts) to Rust if the case is proven and you need the added performance / reliability.

  • akshay-deo 21 minutes ago ago

    Comparing anything with JS or TS would make it shine. The biggest problem I personally have with Rust is that there are a hundred ways (not literally ) to do one thing, which throws readability out the window, especially when you have developers at different levels of expertise.

  • veber-alex 12 hours ago ago

    I find the Zig example to be shocking.

    It's just so brittle. How can anyone think this is a good idea?

    • tialaramex 11 hours ago ago

      I assume it's a bug. However because this is an auteur language if you want the bug fixed it will be important to ensure the auteur also thinks it is a bug. If they get it into their head that it's supposed to be like this, they'll double down and regardless of how many people are annoyed they're insist on it.

      • Ericson2314 7 minutes ago ago

        I'm smirking big time reading this :), well said

        (To be clear to others, it's not even that this is 100% a bad thing, but people love to shit on "design by committee" so much, it helps to have a bit of the opposite)

      • ozgrakkurt 10 hours ago ago

        Tbh you would just use ErrorTypeName.ErrorKind to check equality and not error.ErrorKind if you are worried about this.

        It is a tradeoff between making some things easier. And probably compiler is not mature enough to catch this mistake yet but it will be at some point.

        Zig being an auteur language is a very good thing from my perspective, for example you get this new IO approach which is amazing and probably wouldn’t happen if Andrew Kelley wasn’t in the position he is in.

        I have been using Rust to write storage engines past couple years and it’s async and io systems have many performance mistakes. Whole ecosystem feels like it is basically designed for writing web servers.

        An example is a file format library using Io traits everywhere and using buffered versions for w/e reason. Then you get a couple extra memcopy calls that are copying huge buffers. Combined with global allocation everywhere approach, it generates a lot of page faults which tanks performance.

        Another example was, file format and just compute libraries using asyncio traits everywhere which forces everything to be send+sync+’static which makes it basically impossible to use in single thread context with local allocators.

        Another example is a library using vec everywhere even if they know what size they’ll need and generating memcopies as vec is getting bigger. Language just makes it too easy.

        I’m not saying Rust is bad, it is a super productive ecosystem. But it is good that Zig is able to go deeper and enable more things. Which is possible because one guy can just say “I’ll break the entire IO API so I can make it better”.

        • tialaramex 9 hours ago ago

          > if you are worried about this

          Obviously nobody knows they've made this mistake, that's why it is important for the compiler to reject the mistake and let you know.

          I don't want to use an auteur language, the fact is Andrew is wrong about some things - everybody is, but because it's Andrew's language too bad that's the end of the discussion in Zig.

          I like Rust's `break 'label value`. It's very rarely the right thing, but sometimes, just sometimes, it's exactly what you needed and going without is very annoying. However IIUC for some time several key Rust language people hated this language feature, so it was blocked from landing in stable Rust. If Rust was an auteur language, one person's opinion could doom that feature forever.

      • veber-alex 10 hours ago ago

        Knowing how the Zig developers operate it's 100% not a bug and it's exactly the case you described.

      • jibal 9 hours ago ago

        Why would you assume that? It's very intentional design decision.

    • kouteiheika 10 hours ago ago

      Every language has questionable design decisions that lead to brittle code, although some more than others.

      Like, how can anyone think that requiring the user to always remember to explicitly write `mutex.unlock()` or `defer mutex.unlock()` instead of just allowing optional explicit unlock and having it automatically unlock when it goes out of scope by default is a good idea? Both Go and Zig have this flaw. Or, how can anyone think that having a cast that can implicitly convert from any numeric type to any other in conjunction with pervasive type inference is a good idea, like Rust's terrible `as` operator? (I once spent a whole day debugging a bug due to this.)

      • veber-alex 10 hours ago ago

        You are right, but it doesn't mean we can't complain about it :)

        As a side note, I hate the `as` cast in Rust. It's so brittle and dangerous it doesn't even feel like a part of the language. It's like a JavaScript developer snuck in and added it without anyone noticing. I hope they get rid of it in an edition.

        • JoshTriplett 10 hours ago ago

          > As a side note, I hate the `as` cast in Rust. It's so brittle and dangerous it doesn't even feel like a part of the language. It's like a JavaScript developer snuck in and added it without anyone noticing. I hope they get rid of it in an edition.

          Rust language hat on: I hope so too. We very much want to, once we've replaced its various use cases.

          • bigstrat2003 9 hours ago ago

            As a Rust user, I hope that "as" never goes away. It's way too useful to get rid of, e.g. deliberately truncating numbers to a smaller number of bits. Can you do that in other ways? Absolutely. But they are all less ergonomic than "as". At most I would be ok with restricting "as" to an unsafe block, though that isn't a perfect solution because unsafe isn't really meant to apply to such cases.

            • JoshTriplett 9 hours ago ago

              I'm hoping that we provide ergonomic alternatives for all the individual use cases. The brevity of `as` is a major reason we haven't removed it yet.

              We have `.into()` for lossless conversions like u32 to u64.

              We need to fix the fact that `usize` doesn't participate in lossless conversions (e.g. even on 64-bit you can't convert `usize` to `u64` via `.into()`).

              We need to fix the fact that you can't write `.into::<u64>()` to disambiguate types.

              And I'm hoping we add `.trunc()` for lossy conversion.

              And eventually, after we provide alternatives for all of those and some others, we've talked about changing `as` to be shorthand for `.into()`.

              • tczMUFlmoNk 30 minutes ago ago

                > And eventually, after we provide alternatives for all of those and some others, we've talked about changing `as` to be shorthand for `.into()`.

                Whoa, that could be awesome. It's always felt a bit unfortunate that you can't write `val.into::<SomeExplicitType>::()`—because the type parameter is on the trait, not the method. Of course, `SomeExplicitType::from` works, but sometimes that slightly upsets the flow of code.

                Having just `val as SomeExplicitType` might be really nice for that common case. I do wonder if it'd feel too magic… but I'm optimistic to see what the lang team comes up with.

              • Rusky 7 hours ago ago

                A method call like `.trunc()` is still going to be abysmally less ergonomic than `as`. It relies on inference or turbofish to pick a type, and it has all the syntactic noise of a function call on top of that.

                Not to mention this sort of proliferation of micro-calls for what should be <= 1 instruction has a cost to debug performance and/or compile times (though this is something that should be fixed regardless).

                • JoshTriplett 5 hours ago ago

                  > A method call like `.trunc()` is still going to be abysmally less ergonomic than `as`. It relies on inference or turbofish to pick a type, and it has all the syntactic noise of a function call on top of that.

                  If `as` gets repurposed for safe conversions (e.g. u32 to u64), there's some merit to the more hazardous conversions being slightly noisier. I'm all for them being no noisier than necessary, but even in my most conversion-heavy code (which has to convert regularly between usize and u64), I'd be fine writing `.into()` or `.trunc()` everywhere, as long as I don't have to write `.try_into()?` or similar.

                  > Not to mention this sort of proliferation of micro-calls for what should be <= 1 instruction has a cost to debug performance and/or compile times (though this is something that should be fixed regardless).

                  I fully expect that such methods will be inlined, likely even in debug mode (e.g. `#[inline(always)]`), and compile down to the same minimal instructions.

                  • Rusky 5 hours ago ago

                    > I'd be fine writing `.into()` or `.trunc()`

                    Yes, this is specifically what I'm disagreeing with.

                    > I fully expect that such methods will be inlined, likely even in debug mode (e.g. `#[inline(always)]`), and compile down to the same minimal instructions.

                    That's the cost to compile time I mentioned.

                    • JoshTriplett 4 hours ago ago

                      Many things in the language theoretically go through a trait as well, except that we have special cases in the compiler to handle those traits more efficiently. If this were a performance issue, there's no reason we couldn't do the same for `.trunc()` or `.into()`.

                • int_19h 5 hours ago ago

                  The compiler doesn't have to implement a call as a call; having "magic functions" calls to which are special-cased by the code generator is an old and time-honored tradition.

                  • Rusky 5 hours ago ago

                    Yes, that's how it should work. It is not how it works in today's rustc.

              • muvlon 3 hours ago ago

                > We need to fix the fact that you can't write `.into::<u64>()` to disambiguate types.

                Yes, that would be great. In the meantime, if you can't wait but want something like this, you can DIY it via an extension trait.

                It's very easy to write it yourself, this is all it takes:

                  pub trait To {
                      fn to<T>(self) -> T where Self: Into<T> {
                          <Self as Into<T>>::into(self)
                      }
                  }
                
                Now whenever this trait is in scope, you get to simply do .to::<u64>() and it does exactly what Into does. If you prefer adding a tiny dependency over copy-pasting code, I've also published a crate that provides this: https://crates.io/crates/to_method
              • ChadNauseam 7 hours ago ago

                > We need to fix the fact that you can't write `.into::<u64>()` to disambiguate types.

                This confused me too at first. You have to do `u64::from(_)` right? It makes sense in a certain way, similar to how you have to do `Vec::<u64>::new()` rather than `Vec::new::<u64>()`, but it is definitely more annoying for `into`.

                • JoshTriplett 5 hours ago ago

                  Yeah, you either need `u64::from` or `let x: u64 = expr.into()` or similar.

                  It does "make sense" but it's obnoxious, and we should have something better.

                  • estebank 4 minutes ago ago

                    If only we had type ascription on expressions... ducks

          • 1718627440 9 hours ago ago

            How are people supposed to use a language, that changes syntax?

            • sunshowers 8 hours ago ago

              Rust's edition system allows the language frontend to be changed every few years.

              • JoshTriplett 8 hours ago ago

                In particular, note that new Rust compiles code with both old and new editions. Upgrading Rust does not require you to move to the new edition. And one project can pull in crates from multiple editions.

                (Imagine if Python 3 let you import Python 2 modules seamlessly.)

                • 1718627440 7 hours ago ago

                  That's nice, but I think at some point they will cut the old implementation, as otherwise they would end with hundreds of versions.

                  And it doesn't exactly help to compile newer software on an older OS.

                  • sunshowers 7 hours ago ago

                    No, there's a commitment to not cut old editions. A new edition/frontend comes out every 3 years, so Rust (and maybe humanity) is probably going to be completely dead long before it has hundreds of versions.

                  • steveklabnik 6 hours ago ago

                    To elaborate on what sunshowers said, because editions only impact the early parts of the compiler, most of the code isn’t edition specific, which makes maintenance easy.

        • jcranmer 9 hours ago ago

          As much as I hate 'as' and try to avoid it, it also covers several things that are impossible otherwise (integer <-> float casts are impossible without it, e.g.). I've found that sometimes just being able to express a coerce-to-type-damn-the-consequences is useful.

          Another painful bugbear is when I'm converting to/from usize and I know that it is really either going to be a u64 or maybe u32 in a few cases, and I don't care about breaking usize=u128 or usize=u16 code. Give me a way to say that u32 is Into<usize> for my code!

        • Starlevel004 7 hours ago ago

          As someone working with bitfields and enums-to-ints a lot, you can take ``as`` out of my cold dead hands.

    • zparky 12 hours ago ago

      I didn't even see the error after the first glance until I read your comment.

      • WD-42 10 hours ago ago

        I saw this comment first and then read the zig and still couldn’t find the error until I read the explanation below it. This has to be a bug.

      • veber-alex 12 hours ago ago

        Yeah, I had to do a double take while reading the article because I missed the error completely while looking at the code.

    • geysersam 4 hours ago ago

      Can't a linter catch that you're referring to an error that doesn't exist anywhere else in your system and warn you about that and suggest you use switch instead of if?

    • Phil_Latio 7 hours ago ago

      I would've assumed the error set is generated based on function signatures. Sick stuff.

  • agentultra an hour ago ago

    This is why I like strong, sound static type systems with interesting features.

    I’ve had similar experiences working with a large 1m+ SLOC Haskell codebase. It was straightforward to make large refactors because of the type system.

    And we weren’t even using fancy things like linear types. Just plain old Haskell with a sprinkling of dependent types in core, critical sections.

  • suriya-ganesh 6 hours ago ago

    I vibe with this entirely.

    Rust to me makes a lot more sense. The compiler gives reasonable errors, the code structure is clean and it's obvious where everything should go.

    I just can't deal with Typescript at all. There is a sense of uncertainty in TypeScript that is just unbearable. It is really hard to be confident about the code.

  • aeve890 12 hours ago ago

    >The code will compile just fine. The Zig compiler will generate a new number for each unique 'error.*'

    This is wild. I assume there's at least the tooling to catch this kind of errors right?

    • TUSF 11 hours ago ago

      There's no special tooling to catch this, because nobody catches an error with if-else—it's simply not idiomatic. Everyone uses switch statements in the catch block, and I've never seen anybody using anything other than switch, when catching an error.

      • dminik 10 hours ago ago

        Both the Zig standard library as well as several third party projects do check errors like this.

        I already commented on Zig compiler/stdlib code itself, but here's Tigerbeetle and Bun, the two biggest(?) Zig codebases:

        https://github.com/search?q=repo%3Atigerbeetle%2Ftigerbeetle...

        https://github.com/search?q=repo%3Aoven-sh%2Fbun%20%22%3D%3D...

      • veber-alex 11 hours ago ago

        But why?

        If I just need to check for 1 specific error and do something why do I need a switch?

        In Rust you have both "match" (like switch) and "if let" which just pattern matches one variant but both are properly checked by the compiler to have only valid values.

      • arwalk 11 hours ago ago

        The real problem is not about the if-else, its that he's comparing to the global error set, and not to the FileError error set he created specifically to define AccessDenied.

        • love2read 10 hours ago ago

          The real problem is that this compiles without error

      • love2read 10 hours ago ago

        “There’s no specific tooling to catch this, because nobody[…]”. So? This is a standard library/language feature, which is usually the first place people go for features in the language. To say that nobody uses it is stupid.

    • arwalk 11 hours ago ago

      The example is a bit dubious. Sure, it compiles just fine, because the author is not using errors properly in zig. Here, he uses the global error set with `error. AccessDenid`, and as stated, it compiles just fine because when you reach the global error set, it's integers all the way down.

      If the author had written `FileError.AccessDenid`, this would not have compiled, as it would be comparing with the `FileError` error set.

      The global error set is pretty much never used, except when you want to allow a user to provide his own errors, so you allow the method to return `anyerror`.

      • dminik 10 hours ago ago

        You say never, but even the Zig stdlib does this occasionally.

        Like here in `std/tar.zig`: https://github.com/ziglang/zig/blob/50edad37ba745502174e49af...

        Or here in `main.zig`: https://github.com/ziglang/zig/blob/50edad37ba745502174e49af...

        And in a bunch of other places: https://github.com/search?q=repo%3Aziglang%2Fzig+%22%3D%3D+e...

      • veber-alex 11 hours ago ago

        Why does the compiler decay the FileError and the global error set to an integer? If they were unique types, the if statement would not have compiled.

        • arwalk 10 hours ago ago

          `FileError.AccessDenied` is a unique value in a unique error set. `error.AccessDenid` has not been defined anywhere and hence is just given an integer value at some point by the compiler.

          As I stated before, this error wouldn't even exist in the first place in no codebase ever: look how the method that fails returns a `FileError` and not an `anyerror`

          It could be rightly argued that it still shouldn't compile though.

      • empath75 10 hours ago ago

        All examples of this type come down to "the user made a mistake", but that is kind of the the entire point. It is not possible to make that mistake in rust.

        • arwalk 10 hours ago ago

          I'm not saying that zig has the same level of safety than rust, i'm just saying that grabbing a knife by the blade is not an argument for using a spoon.

          The error presented in this example would not be written by any zig developer. Heck, before this example i didn't even knew that you could compare directly to the global error set, and i maintain a small library.

          zig and rust do not have the same scope. I honestly do not think they should be compared. Zig is better compared to C, and rust is better compared to C++.

          • NobodyNada 10 hours ago ago

            Rust and Zig are the two most prominent new systems programming languages. It's only natural for people to compare them, from perspectives such as "I'm starting a project and need to choose what language to use".

            The languages are very different in scope, scale, and design goals; yes. That means there's tradeoffs that might make one language or the other more suitable for a particular person or project, and that means it can be interesting and worthwhile to talk about those tradeoffs.

            In particular, Rust's top priority is program correctness -- the language tries hard not to let you write "you're holding it wrong" bugs, whereas Zig tends to choose simplicity and explicitness instead. That difference in design goals is the whole point of the article, not a reason to dismiss it.

            • arwalk 10 hours ago ago

              Sure. Did i dismiss the article? Or was I only saying that the example was bad?

              • NobodyNada 10 hours ago ago

                I was specifically responding to the statement "Zig and rust do not have the same scope. I honestly do not think they should be compared".

                I don't know enough Zig to have a qualified opinion on the particular example (besides being very surprised it compiled). However, I thought this post from the front page the other day had more practical and thoughtful examples of this kind of thing: https://www.openmymind.net/Im-Too-Dumb-For-Zigs-New-IO-Inter...

          • love2read 10 hours ago ago

            comparing these examples to “grabbing a knife by the blade” equates writing code that looks right to picking up a sharp and jagged object with your hands that would obviously hurt and will hurt once you hold it.

            It’s more like picking up a fork and being surprised to find out that it’s burning hot without any visible difference.

          • jibal 9 hours ago ago

            > The error presented in this example would not be written by any zig developer.

            No True Scotsman fallacy. It was written by the Zig developer who wrote it.

            • rmunn 4 hours ago ago

              No True Scotsman is only a fallacy when it involves changing your definitions after counterexamples are presented. But it can also be insisting on a non-standard definition.

              E.g., "No true Scotsman would hate haggis!" "You're wrong, my friend Angus hates haggis, and he's a Scotsman through and through." "Well, if he hates haggis, then he isn't a true Scotsman!"

              The first speaker isn't changing his definitions, so he's not actually engaging in the fallacy. Rather, he's insisting on his own idiosyncratic definition of what standards you must meet to be considered a "true" Scotsman, and insisting that Angus doesn't meet his standard.

              But that's enough digression on "No True Scotsman". We now return you to your regularly-scheduled arguing over code. :-)

              • jibal 33 minutes ago ago

                > is only a fallacy when it involves changing your definitions after counterexamples are presented

                That is false.

                • rmunn a few seconds ago ago

                  Then you might want to update the Wikipedia article which claims that it's a fallacy "in which one modifies a prior claim in response to a counterexample by asserting the counterexample is excluded by definition", citing three links to support that: https://iep.utm.edu/fallacy/, http://www.fallacyfiles.org/scotsman.html, and https://archive.org/details/godphilosophy0000flew/page/104/m....

                  To say "No true Scotsman would dislike haggis" is to assert "If A, then B": If you are a true Scotsman, then you will like haggis. The response "Angus doesn't like haggis" is asserting "not B". To which the response "therefore he's not a true Scotsman" is asserting "not A". But "if A, then B" logically implies "if not B, then not A". Therefore when the person's definitions don't change, *it is not a fallacy*. It might be wrong — his definition of a "true" Scotsman might be a false premise — but the conclusion logically follows from the premise, so it is not a fallacy.

  • neandrake 7 hours ago ago

    Rust caught the lock being held across an await boundary, but without further context I'd hedge there's still a concurrency issue there if the solution was to release the lock before the await.

    Presumably the lock is intended to be used for blocking until the commit is created, which would only be guaranteed after the await. Releasing the lock after submitting the transaction to the database but before getting confirmation that it completed successfully would probably result in further edge cases. I'm unfamiliar with rust's async, but is there a join/select that should be used to block, after which the lock should be unlocked?

    • lilyball 6 hours ago ago

      If you need to hold a lock across an await point, you can switch to an async-aware mutex. Both the futures crate and the tokio crate have implementations of async-aware mutexes. You usually only want this if you're holding it across an await point because they're more expensive than blocking mutexes (the other reason to use this is if you expect the mutex to be held for a significant amount of time, so an async-aware lock will allow other tasks to progress while waiting to take the lock).

    • Cyph0n 6 hours ago ago

      You can use a async-aware mutex if you require it to be held across await points. For example, if using the Tokio runtime: https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html.

  • munchler 3 hours ago ago

    > I have found that Rust's strong safety guarantees give me much more confidence when touching the codebase. With that extra confidence I'm much more willing to refactor even critical parts of the app, which has a very positive effect on my productivity, and long-term maintainability.

    That's great, but the graph at the top shows your productivity more than doubling as the size of the project increases, which seems very dubious. Perhaps this is just intended as visual hyperbole, but it triggers my BS detector.

  • koakuma-chan 12 hours ago ago

    I encourage every one to at least stop writing code in Python.

    • nilslindemann 12 hours ago ago

      People who recommend that other people stop using one of the best documented languages on the planet with a huge library ecosystem, a friendly user base, a clean syntax, excellent reference documentation, intuitive function names, readable tracebacks, superb editor and AI support.

    • jvanderbot 12 hours ago ago

      The benefits realized can be mostly attributed to strong type checking.

      I'm a rust dev full time. And I agree with everything here. But I also want people to realize it's not "Just Rust" that does this.

      In case anyone gets FOMO.

      • koakuma-chan 12 hours ago ago

        Do you know a language other than Rust that has alternative for docs.rs? In JavaScript and Python they never bother to have any documentation or reference, one notable example that gets me frustrated these days is the openai SDK for TypeScript. There is no documentation for it. I have to go look at the source code to figure out what the hell goes on.

        • cbm-vic-20 10 hours ago ago

          javadoc.io is similar to (but not as good as) docs.rs for the Java ecosystem. It pulls the JavaDoc out of packages that are published to the Maven Central repository. It doesn't have good discoverability like docs.rs, though, and it's dependent on the publishers actually including the javadoc files.

          • love2read 10 hours ago ago

            javadoc.io is similar in the fact that it lists things I guess? It looks worse (objective) but is also depended on a lot less so is just by default going to be a second thought. It’s also not auto-generated like docs.rs like you said.

        • 9question1 12 hours ago ago

          These days isn't the solution to this just "ask <insert LLM of choice here>" to read the code and write the documentation"?

          • bigstrat2003 9 hours ago ago

            If you want to not reliably know anything about the code, sure. But if you want to have useful knowledge, using a stochastically unreliable tool isn't going to cut it.

          • koakuma-chan 11 hours ago ago

            Yes, you can have Claude Code go through the code and make an .md file with documentation for all the public APIs. I do that for everything that doesn't provide llms.txt.

    • lynndotpy 10 hours ago ago

      Pythons power is in the REPL. It's good for when you want to do something ad-hoc that's too much for Bash or a graphing calculator. It has a large enough ecosystem that you can make a PoC for basically anything in a day.

      It's really, really good for <1000 LoC day projects that you won't be maintaining. (And, if you're writing entirely in the REPL, you probably won't even be saving the code in the first place.)

      • love2read 10 hours ago ago

        the problem is that when you write enough <1kloc projects, a couple of them are useful enough to stay used in which case you are now maintaining python in prod.

        • hnaccount_rng 10 hours ago ago

          Yes but the alternative is "not having those projects at all" not "having them in a 'better' language"

          • love2read 9 hours ago ago

            Of course not, the alternate is writing these projects in a more maintainable language

        • lynndotpy 8 hours ago ago

          I don't think that's inevitable, you can just not do that. I've been doing this for 15 years and it hasn't happened to me.

    • veber-alex 12 hours ago ago

      Here is some actual useful advice:

      Use a type checker! Pyright can get you like 80% of Rust's type safety.

      • deathanatos 12 hours ago ago

        I've not tried Pyright, but mypy on any realistic, real-world codebase I've thrown at it emits ~80k errors. It's hard to get started with that.

        mypy's output is, AFAICT, also non-deterministic, and doesn't support a programmatic format that I know of. This makes it next to impossible to write a wrapper script to diff the errors to, for example, show only errors introduced by the change one is making.

        Relying on my devs to manually trawl through 80k lines of errors for ones they might be adding in is a lost cause.

        Our codebase also uses SQLAlchemy extensively, which does not play well with typecheckers. (There is an extension to aid in this, but it regrettably SIGSEGVs.)

        Also this took me forever to understand:

          from typing import Dict
        
          JsonValue = str | Dict[str, "JsonValue"]
        
          def foo() -> JsonValue:
              x: Dict[str, str] = {"a": "b"}
              return x
        
          x: JsonValue = foo()
        
        That will get you:

          example.py:7: error: Incompatible return value type (got "dict[str, str]", expected "str | dict[str, JsonValue]")  [return-value]
        • FreakLegion 10 hours ago ago

          Everyone stubs their toe on container invariance once, then figures it out and moves on. It's not unique to Python and developers should understand the nuances of variance.

        • zelphirkalt 10 hours ago ago

          I used mypy just fine for a previous job. If you are getting 80k errors, that means you are either starting very late to use the type checker and have done many dubious things before, or you didn't exclude your venv from being type checked by mypy.

        • veber-alex 11 hours ago ago

          I don't use mypy so I can't comment on it but at least from what I have seen pyright is deterministic in it's output and get output json.

          Regarding the ~80k errors. Yeah, nothing to do here besides slowly grinding away and adding type annotations and fixes until it's resolved.

          For the code example pyright gives some hint towards variance but yes it can be confusing.

          https://pyright-play.net/?pyrightVersion=1.1.403&code=GYJw9g...

          • mixmastamyk 2 hours ago ago

            I recommend first starting with pyflakes and later ruff on a huge old Python project. Do file by file.

            When done, do typing similarly.

        • koakuma-chan 12 hours ago ago

          > There is an extension to aid in this, but it regrettably SIGSEGVs.

          Love this.

      • koakuma-chan 12 hours ago ago

        I don't agree that Python can be saved by a type checker. The language itself is flawed irreversibly, as well as its ecosystem. It's a big big mess. Can't build reliable software in Python.

        • grep_it 12 hours ago ago

          Is this rage-bait? A language alone doesn't dictate reliability. There are tons of large scale systems out there running on Python. As for the language being, "flawed irreversibly", I'd be curious to hear you expand on that with examples.

          • nicce 10 hours ago ago

            All programming languages are Turing complete and you can arque with that. But you reach similar results with varying pre-knowledge and effort. Sometimes it is even impossible. All programs can be bug-free but most are not, if we reverse the argument.

        • dkdcio 12 hours ago ago

          you can and this is a juvenile position. there is reliable software in any language as popular and widespread as Python. every language is flawed

        • ninetyninenine 12 hours ago ago

          It can't be 100% saved, but like the OP said it's 80% saved.

          It's not true you can't build reliable software in python. People have. There's proof of it everywhere. Tons of examples of reliable software written in python which is not the safest language.

          I think the real thing here is more of a skill issue. You don't know how to build reliable software in a language that doesn't have full type coverage. That's just your lack of ability.

          I'm not trying to be insulting here. Just stating the logic:

             A. You claim python can't build reliable software.
             B. Reliable Software for python actually exists, therefore your claim is incorrect
             C. You therefore must not have experience with building any software with python and must have your hand held and be baby-sitted by rusts type checker.
          
          Just spitting facts.
          • koakuma-chan 11 hours ago ago

            If you know some secret behind building reliable software in a programming language without types, with nulls, and with runtime exceptions, I'm all ears. I admit that a blanket statement "can't build reliable software" is going overboard, but the intention was to be dramatic, not factually correct. You can probably build reliable software in Python if you write everything from scratch, but I wouldn't want to do that to myself. I would rather use a programming language that has a type system, etc, and a better cultured ecosystem.

            • ninetyninenine 10 hours ago ago

              I prefer types too.

              But I can build reliable software without types as well. Many people can. This isn’t secret stuff that only I can do. There are thousands and thousands of reliable software built on Python, ruby and JavaScript.

              • mixmastamyk 2 hours ago ago

                Indeed, I write very reliable Python every day. A lot faster than the Rust straitjacket too.

                We had sentry installed so I know exactly how many exceptions were happening, rare to zero. Lots of tests/constraints on the database as well.

                That said I like a nice tight straitjacket at other times. Just not every day. ;-).

                P.S. Python doesn’t have the billion-dollar-mistake with nulls. You have to purposely set a variable to None.

        • dangus 2 hours ago ago

          lol, Instagram is written in Python.

      • munificent 10 hours ago ago

        > 80% of Rust's type safety.

        Sort of like closing 80% of a submarine's hatches and then diving.

        • mixmastamyk 2 hours ago ago

          Very few bugs are life-threatening.

      • jaza 3 hours ago ago

        Exactly! I code in Python every day, I haven't done so without a type checker (usually mypy) for years, I don't push a single change without type-checking it first, it catches stupid mistakes and saves me from breaking production all the time.

      • guitarbill 12 hours ago ago

        Sorry, it's not even close to Rust. Or even C# or Java. It can't provide the same "fearless refactoring". It is better than being blind/having to infer everything manually. That's not saying much.

        And that's assuming the codebase and all dependencies have correct type annotations.

      • kragen 12 hours ago ago

        What are the major pros and cons of Pyright and Mypy relative to one another?

  • loeg 12 hours ago ago

    The first save is from a failure of Rust's own making: async Rust. It's an awful footgun of a concept that is made vaguely bearable by the rest of the type system.

    • love2read 10 hours ago ago

      (but is still one of the most capable languages for async programming)

      • loeg 5 hours ago ago

        Maybe. I think async as a concept is kind of bad in general, but if you want it, GC makes it more ergonomic.

  • marcosdumay 10 hours ago ago

    On that graph, both curves should go monotonically down, just one faster than the other.

  • jmull 10 hours ago ago

    The javascript browser bug has nothing to do with the language.

    It’s just a logic bug.

    E.g., the code doesn’t match their own English description of the logic: “If yes, redirect to the specific page. If not, go to the dashboard or onboarding page.”

    The code is missing the “if not” (probably best expressed using an “else” clause following the if block).

    • notfed 9 hours ago ago

      I wouldn't say nothing. I mean, we're essentially talking about a global variable that's specified into the language.

      • jmull 8 hours ago ago

        window and window.location aren't part of javascript. They are part of the javascript api that standard browsers provide.

        It's fair to point out that browser api is confusing. You might not think of setting a property as kicking off an asynchronous operation, especially if it seems to has instantaneous effect at first.

        But the basic control flow logic of that code is wrong. Confusion about whether a side-effect from an api call might bail you out from your error is beside the point.

        • byw 5 hours ago ago

          I'm curious if Rust (WASM through JS interop?) also has access to `window.location`, and if the API has some more safety mechanisms.

          • steveklabnik 4 hours ago ago

            web_sys lets you call window.location, which returns a Location, which has href and set_href methods. They do the same thing the native APIs do, as that is their purpose.

      • pwdisswordfishz 9 hours ago ago
  • 0x1ceb00da 12 hours ago ago

    The typescript/javascript example is a little dishonest. Nothing will save you if you don't know how the runtime/environment/domain semantics work.

    • 9question1 12 hours ago ago

      I love Typescript but I think I disagree with this. The point of the post seems to be that features of the Rust compiler help enforce that you use certain runtime / environment / domain semantics in ways that eliminate common classes of errors. That's never going to prevent all errors, but preventing large groups of common errors so that you only have to manually remember a smaller set of runtime/environment/domain semantics could have some value.

      • 0x1ceb00da 11 hours ago ago

        It isn't typescript's fault. Borrow checker won't save you from bugs in your SQL queries that you send to the DBMS. Typescript doesn't care about the browser just like rust doesn't care about SQL

        • bryanlarsen 11 hours ago ago

          SeaORM and similar crates in Rust will catch some common SQL bugs through the type system.

          • morcus 10 hours ago ago

            Typescript also has ORMs.

            The problem is not with TypeScript or even JavaScript but an odd Browser API where mutating some random value of an object results in a redirect on the page, but not synchronously.

            Even if the language of the browser were Rust, there's nothing about the type system specifically that would have caught this bug (as far as I can tell, anyways. Presumably there's something in the background periodically reading the value of `href` and updating the page accordingly, but since that background job only would have needed read and not write access to the variable, I don't think the borrow checker would have helped here)

  • antirez 10 hours ago ago

    I always thought that the POSIX threads semantics that forces the thread acquiring the lock to be the same thread that releases it, is too strict and not needed. In certain use cases it forces you to redesign the code in more complicated ways.

    • murderfs 10 hours ago ago

      It isn't too strict. Releasing a pthread_mutex has the semantics of being a release memory barrier, which means that any writes on that thread will be visible by other threads that issue a subsequent acquire memory barrier (e.g. by acquiring the mutex).

      If you want this behavior, it's relatively simple to implement your own mutex on top of futex, but no one is going to expect the behavior it provides.

  • jaccola 10 hours ago ago

    I would love to see a web app with an interface where I select two programming languages and it shows me a really clean snippet of code in language A and says "Look how terribly hard and unclean this is to achieve in language B". (and then you could reverse to see where B outshines A).

    Languages are a collection of tradeoffs so I'm pretty sure you could find examples for every two languages in existence. It also makes these kinds of comparisons ~useless.

    • prerok 9 hours ago ago

      For one, snippets is exactly where you don't get good language comparisons.

      For example, python is AFAIK the lead language to get something done quickly. At least as per AoC leaderboards. It's a horrible language to have in production though (experienced it with 700k+ LOC).

      Rust is also ok to do for AoC, but you will (based on stats I saw) need about 2x time to implement. Which in production software is definitely worth it, because of less cost in fixing stupid mistakes, but a code snippet will not show you that.

    • guywithabike 7 hours ago ago

      Rosetta Code is what you're looking for, I believe: https://rosettacode.org/

    • jibal 9 hours ago ago

      Actually, these sorts of comparisons are extremely useful, and an argument based on imagining an unknown case that makes the argument work is fallacious.

  • whazor 12 hours ago ago

    I agree with the premise of more sophisticated compilers give you a productivity boost. But compiling with cargo can be very slow, and having a strong workstation can be worth it.

  • pkolaczk 10 hours ago ago

    Rust is one of a very few languages where I can make a total mess in my code by refactoring, have like 100+ compilation errors in multiple modules, think I’m never going to fix it, yet at the end of the day I eventually get everything to compile fine, I run the tests and… they all pass from the first go. All green. Hard to believe but it happened many times to me.

    • ChadNauseam 9 hours ago ago

      Same. I often have the experience of seeing my app suddenly work again and thinking "wait... I'm done?"

  • gedy 12 hours ago ago

    > Assigning a value to 'window.location.href' doesn't immediately redirect you, like I thought it would.

    That's not a "Typescript" or language issue, that's a DOM/browser API weirdness

    • ChadNauseam 7 hours ago ago

      You could argue that the DOM API being weird is partially because typescript didn't exist when they came up with the API, so the API wasn't designed with typescript in mind. If the DOM API was written against Rust, the API could have been designed against Rust's type system to make this error more difficult.

    • gavmor 11 hours ago ago

      Jeeze, why should attribute assignment have side-effects, anyway? That'd be gross!

      • scheme271 2 minutes ago ago

        I think it's unavoidable for any sufficiently complicated object or variable. Think of c++'s unique_ptr that guarantees that only a single reference to memory exists so that that the memory can be free'd safely when it goes out of scope. Or thing of a complicated object that has references to allocated memory. You either allocate a bunch of memory and copy values over or have two objects refer to the same bit of allocated memory. Each solution potentially has major downsides. Your objects don't even have to be all that exotic, think of a vector holding 1024 fp64 values or a 8kb long string.

    • love2read 10 hours ago ago

      *though it could have been fixed by typescript had they cared.

      • morcus 10 hours ago ago

        How could Typescript have fixed this?

        • love2read 9 hours ago ago

          they could have provided a seperate api over top of this and error’d on use? It’s a clearly error-prone api.

          • morcus 9 hours ago ago

            I genuinely don't understand - how could Typescript provide a separate API on top of this? I usually do not expect Typescript to start generating code and APIs for me.

            Also, Typescript is adding types on top of the JavaScript language, not the DOM API.

  • jemiluv8 10 hours ago ago

    The Typescript example given is an obvious bug to me even before I knew how the window redirect thing works. It was an obvious control flow issue

    And I find the event loop vrs concurrency via mutexes to be like an apples to oranges comparison. They both do some form of concurrency but not nearly in the same way

    Again, Typescript is hardly considered a language. It is just a tool used to keep javascript under some control on large projects. Again, the comparison between rust and Typescript on a language level is not a great match.

    As for fearless refactoring, don't get me started. I experienced this the first time I was porting a vanilla js backend to a typescript version. It was awesome. I won't say it works in much the same way as rust does but man, if you ever ported a rest api written in javascript to Typescript - you'd experience a similar effect.

  • pjmlp 12 hours ago ago

    Most of these productivity gains are achievable in any Standard ML influenced type system.

    • bryanlarsen 12 hours ago ago

      The main difference between Rust and other languages with a Standard ML influenced type system is that Rust has features that can let you get executive sign off for switching languages.

      • pjmlp 12 hours ago ago

        Not really, at my job Scala, F#, Swift and Kotlin are possible, and most likely will never do Rust, other than using JavaScript tools written using Rust, just because.

        There is nothing in our domain of distributed systems based on SaaS products, mobile OSes, and managed cloud environments, that would profit from a borrow checker.

      • binary132 12 hours ago ago

        what is the main advantage of Rust over OCaml for most applications in this respect?

        • maleldil 5 hours ago ago

          Ecosystem. It's much easier to find libraries for Rust.

    • wk_end 12 hours ago ago

      The example that the article gives relies on the borrow checker, which is not part of the usual Standard ML type system.

      • pjmlp 12 hours ago ago

        It is called affine type system, and there are ML descendents with it.

        You can even go more crazy with linear types, effects, formal proofs or dependent types.

        What Rust has achieved, was definitely make these ideas more mainstream.

      • user____name 12 hours ago ago

        Can the type system catch such things across translation unit boundaries? I know this is a big limit of C like compilers without whole program compilation.

        • tialaramex 11 hours ago ago

          Ultimately the answer is just "Yes". You know how an array of six integers is a different type from an array of three integers even though they're both arrays of integers ? If you're unclear on that it's worth a moment to go refresh, if your only experience is in C it's a deep dive - no trouble it's valuable knowledge.

          In Rust lifetimes for references are part of the type, so &'a str and &'b str could be different types, even though they're both string slice references.

          Beyond that, Rust tracks two important "thread safety" properties called Sync and Send, and so if your Thing ends up needing to be Send (because another thread gets given this type) but it's not Send, that's a type error just as surely as if it lacked some other needed property needed for whatever you do with the Thing, like it's not totally ordered (Ord) or it can't be turned into an iterator (IntoIterator)

    • phkahler 10 hours ago ago

      >> any Standard ML influenced type system

      Which languages are those?

    • love2read 10 hours ago ago

      does this actually mean anything more than using a type system?

  • ChadNauseam 9 hours ago ago

    This post mirrors my experience 100%. The effect is even stronger when there are multiple people involved on a project, or when you're maintaining a project that you didn't start. There, the benefits of static typing and domain modelling with ADTs become essential. Take take GHC - a haskell project, not a rust one, but it's been under active development by many different people since 1990 and is still going strong. Is there any doubt that the guarantees provided by Haskell's type system were beneficial to making this possible? Not to say that it's strictly necessary – the linux kernel is an even more impressive project and it's in C – but something that is not necessary for success can still increase your odds of success.

    You can ask any professional python programmer how much time they've spent trying to figure out the methods that are callable on the object returned by some pytorch function, and they will all tell you it's a challenge that occurs at least weekly. You can ask any C++ programmer how much time they've spent debugging segfaults. You can ask any java programmer how much time they've spent debugging null pointer exceptions. These are all common problems that waste an incredible amount of time, that simply do not occur to anywhere close to the same extent in Rust.

    It's true that you can get some of these benefits by writing tests. But would tests have prevented the issue that OP mentioned in his post, where acquiring a mutex from one thread and releasing it from another is undefined? It's highly doubtful, unless you have some kind of intensive fuzz-testing infrastructure that everyone talks about and no one seems to actually have. And what is more time-efficient: setting up that infrastructure, running it, seeing that it detects undefined behavior at the point of the mutex being released, and realizing that it happened because the mutex was sent to a different thread? Or simply getting a compile error the moment you write the code that says "hey pal, mutex guards can't be moved to a different thread". Plus, everyone who's worked on a codebase with a lot of tests can tell you that you sometimes end up spending more time fixing tests than you do actually writing code. For whatever reason, I spend much less time fixing types than fixing tests.

    There is a compounding benefit as well. When you can refactor easily (and unit tests often do not make refactoring much easier...), you can iterate on your code's architecture until you find one that meshes naturally with your domain. And when your requirements change and your domain evolves, you can refactor again. If refactoring is too expensive to attempt, your architecture will become more and more out-of-sync with your domain until your codebase is unmaintainable spaghetti. If you imagine a simple model where every new requirement either forces you into refactoring your code or spaghettifying your code, and assume that each instance of spaghettification induces a 1% dev speed slowdown, you can see that these refactors become basically essential. Because 100 new requirements in the future, the spaghetti coder will be operating at 36% the productivity of the counterfactual person who did all the refactors. Seen this way, it's clear that you have to do the refactors, and then a major component of productivity is whether you can do them quickly. An area where it's widely agreed rust excels at.

    There are plenty of places we can look at Rust and find ourselves wanting more. But that doesn't mean we shouldn't be proud of what Rust has accomplished. It has finally brought many of the innovations of ML and Haskell to the masses, and innovated new type-system features on top of that, leading to a very productive and pleasantly-designed language.

    (I also left this comment on reddit, and am copying it here.)

  • dvt 10 hours ago ago

    Using sync locks in an async environment is a code smell, so the Rust code is bad right off the bat. Just stick with async locks and take the tiny performance hit unless you know exactly what you're doing.

    • ozgrakkurt 10 hours ago ago

      If I remember correctly tokio Mutex docs mentions you should use std Mutex unless you are holding the lock across an await

  • tomovo 9 hours ago ago

    The graph has no units or scale. What is the project size at the crossover point? The article says "certain size" which is unclear.

  • Spivak 12 hours ago ago

    How do you encode the locking issue in the type system, it seems magical? Can you just never hold any locks when calling await, is it smart enough to know that this scheduler might move work between threads?

    • vlovich123 12 hours ago ago

      Presumably the author is using tokio which requires the future constructed (e.g the async function) to be Send (either because of the rules of Rust or annotated as Send) since tokio is a work-stealing runtime and any thread might end up executing a given future (or even start executing and then during a pause move it for completion on another thread). std::sync::MutexGuard intentionally isn't annotated with Send because there are platforms that require the acquiring thread be the one to unlock the mutex.

      One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code. You should be using tokio sync primitives (e.g. tokio Mutex) which can yield to the reactor when it needs to block. Otherwise the thread that's running the future blocks forever waiting for that mutex and that reactor never does anything else which isn't how tokio is designed).

      So the compiler is warning about 1 problem, but you also have to know to be careful to know not to call blocking functions in an async function.

      • maplant 12 hours ago ago

        > using a normal std Mutex within an async environment is an antipattern and should not be done

        This is simply not true, and the tokio documentation says as much:

        "Contrary to popular belief, it is ok and often preferred to use the ordinary Mutex from the standard library in asynchronous code."

        https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#wh...

        • vlovich123 9 hours ago ago

          I think that’s a dangerous recommendation and ignores the consequence of doing this within a single threaded reactor (your app will hang permanently if the lock is contended) or assumes you won’t have N threads trying to acquire this lock where N is the size of your thread pool which is entirely possible on a heavily accessed server if the core state that needs to be accessed by every request is locked with said mutex.

          • maplant 9 hours ago ago

            If you're using a single threaded reactor you don't need a mutex at all; a Rc<RefCell<_>> will do just fine. And if you want other tasks to yield until you're done holding the borrow, the solution is simple: don't await until you're done. https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...

            there are absolutely situations where tokio's mutex and rwlock are useful, but the vast majority of the time you shouldn't need them

            • vlovich123 7 hours ago ago

              As you know Rc<RefCell> won't work for a data structure that's being accessed by other threads (e.g. spawn_blocking or explicitly accessed threads). Just because you are using the single-threaded reactor does not mean you don't have other threads to access the data.

              • maplant 7 hours ago ago

                It seems like you've found a very specific situation where tokio's Mutex is helpful but it is simply not common to be structuring an async rust application with a single threaded reactor and copious uses of spawn blocking. The advice to go for the std Mutex first is generally good

      • rcxdude 12 hours ago ago

        It can be an issue, but only if you have long enough contention periods for the mutex to matter. A Mutex which is only held for short time periouds can work perfectly fine in an async context. And it's only going to cause deadlocks if it would cause deadlocks in a threaded context, since if Mutexes can only be held between yield points then it'll only ever be running tasks that are contending for them.

        • vlovich123 9 hours ago ago

          Imagine if every thread tries to acquire this mutex on every HTTP request. And then you also lock it in a spawn_blocking. On a heavily contended lock, all the reactor threads will end up stuck waiting for the spawn_blocking to finish and the service will appear hung since even requests that don’t even need that lock can’t get processed. And if spawn_blocking is doing synchronous network I/O or reading a file from a hung NFS mount, you’re completely hosed.

          • rcxdude 8 hours ago ago

            You would be in a similarly bad situation if such a lock was async as well. You want to keep locks as short as possible, regardless, and a hung task with a lock is generally going to propagate issues throughout the system until it's dealt with.

            • vlovich123 7 hours ago ago

              If you have an async lock that can be held across await points, your motivation to spawn_block decreases. Additionally, even though the long running lock would be a problem, APIs that aren't taking that lock continue to work vs the entire HTTP service being hung which is a huge difference (e.g. health monitoring would time out making the service seem unresponsive vs 1 highly contended task just waiting on a lock).

              • rcxdude 3 hours ago ago

                >e.g. health monitoring would time out making the service seem unresponsive vs 1 highly contended task just waiting on a lock

                If anything that's a disadvantage. You want your health monitoring to be the canary, not something that keeps on trucking even if the system is no longer doing useful work. (See the classic safety critical software fail of 'I need a watchdog... I'll just feed it regularly in an isolated task')

      • sunshowers 12 hours ago ago

        Using a Tokio mutex is even more of an antipattern :) come to my RustConf talk about async cancellation next week to find out why!

        • vlovich123 9 hours ago ago

          Most people on this forum are not attending RustConf. It might be helpful to post at least the abstract of your idea.

          • sunshowers 9 hours ago ago

            The big thing is that futures are passive, so any future can be cancelled at any await point by dropping it or not polling it any more. So if you have a situation like this, which in my experience is a very common way to use mutexes:

              let guard = mutex.lock().await;
              // guard.data is Option<T>, Some to begin with
              let data = guard.data.take(); // guard.data is now None
            
              let new_data = process_data(data).await;
              guard.data = Some(new_data); // guard.data is Some again
            
            Then you could cancel the future at the await point in between while the lock is held, and as a result guard.data will not be restored to Some.
            • vlovich123 7 hours ago ago

              I'm not sure this introduces any new failure though:

                  let data = mutex.lock().take();
                  let new_data = process_data(data).await;
                  *mutex.lock() = Some(new_data);
              
              Here you are using a traditional lock and a cancellation at process_data results in the lock with the undesired state you're worried about. It's a general footgun of cancellation and asynchronous tasks that at every await boundary your data has to be in some kind of valid internally consistent state because the await may never return. To fix this more robustly you'd need the async drop language feature.
              • sunshowers 7 hours ago ago

                True! This is the case with std mutexes as well. But holding a std MutexGuard across an await point makes the future not Send, and therefore not typically spawnable on a Tokio runtime [1]. This isn't really an intended design decision, though, as far as I can tell -- just one that worked out to avoid this footgun.

                Tokio MutexGuards are Send, unfortunately, so they are really prone to cancellation bugs.

                (There's a related discussion about panic-based cancellations and mutex poisoning, which std's mutex has but Tokio's doesn't either.)

                [1] spawn_local does exist, though I guess most people don't use it.

                • vlovich123 7 hours ago ago

                  You argument then is that the requirement to acquire the lock multiple times makes it more likely you'll think about cancellation & keeping it in a valid interim state? Otherwise I'm not sure how MutexGuards being send really makes it any more or less prone to cancellation bugs.

                  • sunshowers 6 hours ago ago

                    Right, in general a lot of uses of mutexes are to temporarily violate invariants that are otherwise upheld while the mutex is released, something that can usually be reasoned out locally. Reasoning about cancellation at an await point is inherently non-local and so is much harder to do. (And Rust is all about scaling up local reasoning to global correctness, so async cancellation feels like a knife in the back to many practitioners.)

                    The generally recommended alternative is message passing/channels/"actor model" where there's a single owner of data which ensures cancellation doesn't occur -- or, at least that if cancellation happens the corresponding invalid state is torn down as well. But that has its own pitfalls, such as starvation.

                    This is all very unsatisfying, unfortunately.

            • neandrake 7 hours ago ago

              Same would be true for any resource that needs cleaned up, right? Referring to stop-polling-future as canceling is probably not good nomenclature. Typically canceling some work requires cleanup, if only to be graceful let alone properly releasing resources.

              • sunshowers 7 hours ago ago

                Yes, this is true of any resource. But Tokio mutexes, being shared mutable state, are inherently likely to run into bugs in production.

                In the Rust community, cancellation is pretty well-established nomenclature for this.

                Hopefully the video of my talk will be up soon after RustConf, and I'll make a text version of it as well for people that prefer reading to watching.

                • neandrake 7 hours ago ago

                  Thank you, I look forward to watching your presentation.

      • dilawar 12 hours ago ago

        > One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code.

        True. I used std::mutex with tokio and after a few days my API would not respond unless I restarted the container. I was under the impression that if it compiles, it's gonna just work (fearless concurrency) which is usually the case.

        • maplant 11 hours ago ago

          It sounds like you have a deadlock somewhere in your code and it's unlikely that the choice of Mutex really fixed that

          • vlovich123 9 hours ago ago

            Or you do something super expensive while holding the mutex.

            Or your server is heavily contended enough that all worker threads are blocked on this mutex and no reactor can make forward progress.

      • benmmurphy 11 hours ago ago

        original poster was 'lucky' that he was using a work stealing engine. if the engine was not moving tasks between threads then i think the compiler would have been happy and he could have had the fun of debugging what happens when the same thread tries to lock the same mutex twice. the rust compiler won't save you from this kind of bug.

        > The exact behavior on locking a mutex in the thread which already holds the lock is left unspecified. However, this function will not return on the second call (it might panic or deadlock, for example).

      • Spivak 12 hours ago ago

        Thank you! It seems so simple in hindsight to have a type that means "can be moved to another thread safely." But the fact that using a Mutex in your function changes the "type" is really novel. It becoming Send once the lock is released before await is just fantastic.

        • NobodyNada 11 hours ago ago

          The way that this fully works together is:

          - The return type of Mutex::lock() is a MutexGuard, which is a smart pointer type that 1) implements Deref so it can be dereferenced to access the underlying data, 2) implements Drop to unlock the mutex when the guard goes out of scope, and 3) implements !Send so the compiler knows it is unsafe to send between threads: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html

          - Rust's implementation of async/await works by transforming an async function into a state machine object implementing the Future trait. The compiler generates an enum that stores the current state of the state machine and all the local variables that need to live across yield points, with a poll function that (synchronously) advances the coroutine to the next yield point: https://doc.rust-lang.org/std/future/trait.Future.html

          - In Rust, a composite type like a struct or enum automatically implements Send if all of its members implement Send.

          - An async runtime that can move tasks between threads requires task futures to implement Send.

          So, in the example here: because the author held a lock across an await point, the compiler must store the MutexGuard smart pointer as a field of the Future state machine object. Since MutexGuard is !Send, the future also is !Send, which means it cannot be used with an async runtime that moves tasks between threads.

          If the author releases the lock (i.e. drops the lock guard) before awaiting, then the guard does not live across yield points and thus does not need to be persisted as part of the state machine object -- it will be created and destroyed entirely within the span of one call to Future::poll(). Thus, the future object can be Send, meaning the task can be migrated between threads.

        • VWWHFSfQ 12 hours ago ago

          Rust's marker traits (Send, Sync, etc.) + RAII is really what makes it so magical to me.

    • mrtracy 12 hours ago ago

      Rust uses the traits “Send” and “Sync” to encode this information, there is a lot of special tooling in the compiler around these.

      A type is “Send” if it can be moved from one thread to another, it is “Sync” if it can be simultaneously accessed from multiple threads.

      These traits are automatically applied whenever the compiler knows it is safe to do so. In cases where automatic application is not possible, the developer can explicitly declare a type to have these traits, but doing so is unsafe (requires the ‘unsafe’ keyword and everything that entails).

      You can read more at rustinomicon, if you are interested: https://doc.rust-lang.org/nomicon/send-and-sync.html

    • jvanderbot 12 hours ago ago

      Yes Rust has ways of verifying single access to locks through the borrow checker and lifetimes.

      • Spivak 12 hours ago ago

        Well yes but this seems more complicated. If the code was executed on a single thread it would be correct. The compiler somehow knows that await might move the work to another thread in which the code would still be correct if it weren't for the pesky undefined behavior. At all points it seems like it would be single access and yet it catches this.

        • veber-alex 12 hours ago ago

          The compiler doesn't know anything about threads.

          The compiler knows the Future doesn't implement the Send trait because MutexGuard is not Send and it crosses await points.

          Then, tokio the aysnc runtime requires that futures that it runs are Send because it can move them to another thread.

          This is how Rust safety works. The internals of std, tokio and other low level libraries are unsafe but they expose interfaces that are impossible to misuse.

        • 0x1ceb00da 12 hours ago ago

          An async function call returns a future which is an object holding the state of the async computation. Multithreaded runtimes like tokio require this future to be `Send`, which means it can be moved from one thread to another. The future generated by the compiler is not Send if there is a local variable crossing an await boundary that is not `Send`.

        • ViewTrick1002 12 hours ago ago

          That comes from the where the tokio task is spawned annotating the future with send.

          https://docs.rs/tokio/latest/tokio/task/fn.spawn.html

          If you want to run everything on the same thread then localset enables that. See how the spawn function does not include the send bound.

          https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html

    • synalx 12 hours ago ago

      More the latter. The lock guard is not `Send`, so holding it across the await point makes the `impl Future` returned by the async function also not `Send`. Therefore it can't be passed to a scheduler which does work stealing, since that scheduler is typed to require futures to be `Send`.

    • bkolobara 12 hours ago ago

      Yes, if you use a scheduler that doesn't move work between threads, it will not require the task to be Send, and the example code would compile.

      Rust can use that type information and lifetimes to figure out when it's safe and when not.

  • ModernMech 11 hours ago ago

    I had to explain this to my students today using XKCD #1987, that although Python is considered a universally "simple" and "easy to use" language, a lot of the complexity in that ecosystem is paid for on the backend.

    With Rust, they frontload the complexity, so it's considered to be "hard to learn". But I've got to say, Rust's "complexities" have allowed me to build a taller software tower than I've ever been able to build before in any other language I've used professionally (C/C++/Java/Swift/Javascript/Python).

    And that's the thing a lot of people don't get about Rust, because you can only really appreciate it once you've climbed the steep learning curve.

    At this point I've gone through several risky and time-consuming (weeks) refactors of a substantial Rust codebase, and every time it's worked out I'm amazed it wasn't the kind of disaster I've experienced refactoring in other languages, where the refactor has to be abandoned because it got so hairy and everyone lost all hope and motivation.

    They don't tell you about that kind of pain in the Python tutorial when they talk about how easy it is to not have to type curly braces and have dynamic types everywhere. And you don't really find that pleasure in Rust until you've built enough experience and code to have to do a substantial refactor. So I can understand why the Rust value proposition is dubious for people who are new to the language and programming in general.

    [1] https://xkcd.com/1987

    • lmm 4 hours ago ago

      > I had to explain this to my students today using XKCD #1987, that although Python is considered a universally "simple" and "easy to use" language, a lot of the complexity in that ecosystem is paid for on the backend.

      No, that's just an unrelated coincidence. Python happens to be a good language with awful tooling, but there are also good languages with good tooling and awful languages with awful tooling.

  • torginus 7 hours ago ago

    The Rust example that you point out might not trigger compile time warnings in other languages, but most OS mutexes (certainly the ones in Windows and Linux) will complain if you release them on a different thread than you acquired them.

    • IshKebab 6 hours ago ago

      You've misunderstood somehow. The point is that Rust detects the mistake at compile time so you don't get to those runtime errors (which might be very rare or context-dependent).