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.
What I disagree with is that it's the fault of Typescript that the href assignment bug is not caught. I don't think that has anything to do with Typescript. The bug is that it's counter-intuitive that setting href defers the location switch until later. You could imagine the same bug in Rust if Rust had a `set_href` function that also deferred the work:
set_href('/foo');
if (some_condition) {
set_href('/bar');
}
Of course, Rust would never do this, because this is poor library design: it doesn't make sense to take action in a setter, and it doesn't make sense that assigning to href doesn't immediately navigate you to the next page. Of course, Rust would never have such a dumb library design. Perhaps I'm splitting hairs, but that's not Rust vs TypeScript - it's Rust's standard library vs the Web Platform API. To which I would totally agree that Rust would never do something so silly.A 'setter' should never ever cause an action to be triggered, and especially not immediately inside the setter.
At the least change the naming, like `navigate_to(href)`.
But in the browser environment it's also perfectly clear why it is not happening immediately, your entire JS code is essentially just a callback which serves the browser event loop and tells it what to do next. A function which never returns to the caller doesn't fit into the overall picture.
> A function which never returns to the caller doesn't fit into the overall picture.
Hmm, not sure about this. On the node side, you can process.exit() out of a callback. If setting href worked like that, I think it would be less confusing.
The point I was trying to make is that Rust's ownership model would allow you to design an api where calling `window.set_href('/foo')` would take ownership of `window`. So you would not be able to call it twice. This possibility doesn't exist at all in TypeScript, because it doesn't track lifetimes.
Of course, TypeScript can't do anything here either way. Even if it had knowledge of lifetimes, the JavaScript API already existed before and it would not be possible to introduce an ownership model on top of it, because there are just too many global variables and APIs.
I wanted more to demonstrate how Rust's whole set of features neatly fits together and that it would be hard to get the same guarantees with "just types".
let win = window.set_href("/foo")
win.set_href("/bar")
You might say "why would you ever do that" but my point is that if it's really the lack of move semantics that cause this problem (not the deferred update), then you should never be able to cause an issue if you get the types correct. And if you do have deferred updates, maybe you do want to do something after set_href, like send analytics in a finally() block.In fact, Typescript does have a way to solve this problem - just make `setHref` return never[1]! Then subsequent calls to `setHref`, or in fact anything else at all, will be an error. If I understand correctly, this is similar to how `!` works in Rust.
So maybe TS is not so bad after all :)
[1]: https://www.typescriptlang.org/play/?ssl=9&ssc=1&pln=9&pc=2#...
I'm scared of Ruby because I catch bugs at runtime all the time, but here's the thing: it ends up working before a commit and it was easy enough to get there and it's satisfying to read and edit the code. Now wether I can keep going like this if the project become bigger is the question.
The location.href issue is really a javascript problem that has been inherited by TS. Because JS allows to modify attributes, the browser kind of has to take the change into account. But it's not like Ruby's exit keyword. The page is still there until the next page loads and this makes total sense once you know it.
set_href('/foo');
let future = doSomethingElse()
block_on(future)
if (some_condition) {
set_href('/bar');
}
This code makes the bug clearer. doSomethingElse is effectively allowing the page to exit. this would be no different in many apps, even in rust.The browser does not start a process when you set `window.location.href`. It starts a process after your code exits and lets the event loop run other tasks. The `await` in the example code is what allow other tasks to run, including the task to load a new page, (or quit an app, etc..) That task that was added when you set `window.location.href`
If that's not clear
// task 1
window.location.href = '/foo' // task2 (queues task2 to load the page)
let content = await response.json(); // adds task3 to load json
// which will add task4
// to continue when finished
// task4
if (content.onboardingDone) {
window.location.href = "/dashboard";
} else {
window.location.href = "/onboarding";
}
task2 runs after task1. task1 exits at the `await`. task2, clears out all the tasks. task3 and task4 never run.The the author expects the side-effect -- navigation to a new page -- of the window.location.href setter to abort the code running below it. This obviously won't happen because there is no return in the first if-statement.
I specifically mean this part: "Rust would never have such a dumb library design".
One could then also say that Rust programmers would never make such a cyclical argument.
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".
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.
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.
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?
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.
This assignment has a significant side-effect of leaving the page, assuming this is immediate rather than a scheduled asynchronous action is not unfair (I’m pretty sure I assumed the same when I saw or did that).
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
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.
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.
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.
Do you think Zig is a valid challenger to Rust for this kind of programming?
I like to call it getting "union-pilled" and it's really hard to accept otherwise statically-typed languages once you become familiar.
C is statically typed, but its type system tracks much less.
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.
https://fsharpforfunandprofit.com/posts/designing-with-types...
... and I would suggest actively playing around with the code examples, extending them, breaking them intentionally etc. somewhere like https://dotnetfiddle.net/
I think the article is brilliant but as I recall I wasn't able to grasp the concepts from reading alone.
That same website has a separate article on building a calculator (in code) that I recall was also excellent.
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).
[1] not trying to take away anything from the designers, getting it right in combination with all the other features is a huge feat!
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.
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.
Well, the compiler is guaranteed that no mistakes will happen. It's the programmer who looses his guarantees in this case.
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.
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.
Is it frequently? Generics are definitely not as nice as they could be, but they are surprisingly "sufficient" for almost any library, e.g. a full on type-safe SQL DSL like JOOQ. Unsafe casts are very rare, and where you do need Object and stuff are very dynamic stuff where it's impossible to extend compile-time guarantees even theoretically (e.g. runtime code generation, dynamic code loading, etc - people often forget about these use cases, not everything can work in a closed universe)
The concurrency/safety/memory story is only valid in a few rare cases and I wish people didn't try to sell Rust for these features.
https://www.rocksolidknowledge.com/articles/locking-asyncawa...
That is incorrect. Java enforces that a monitor lock (or Lock) must be released by the same thread that acquired it. Attempting to unlock from a different thread throws IllegalMonitorStateException.
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.
There is very little research comparing PL productivity, because it is very hard to do them properly.
On the other hand, that strictness is precisely what leads people to end up with generally reasonable code.
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.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.
It's just so brittle. How can anyone think this is a good idea?
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”.
What's happening is that compiler knows the two errors come from disjoint error set, but it promotes them both to anyerror
Details at https://github.com/ziglang/zig/issues/25046
(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)
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.)
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.
You can call functions inside your function Main, but these function can't call any functions anymore (exception being flat helper functions defined inside your function).
I think it would save a huge chunk of time by just having all programs really nice and flat. You'd naturally gravitate towards mechanisms that make programs flat.
This is wild. I assume there's at least the tooling to catch this kind of errors right?
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...
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.
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`.
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...
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.
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?
That's not a "Typescript" or language issue, that's a DOM/browser API weirdness
The accessibility of Python is overrated. It's a language with warts and issues just like the others. Also the lack of static typing is a real hindrance (yes I know about mypy).
Are we talking about the same Python? Have you seen the Python documentation? There's a reason it ranks so badly on Google.
Also you forgot the abysmal performance and laughably janky tooling (until uv saved us from that clusterfuck anyway).
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.
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.)
Use a type checker! Pyright can get you like 80% of Rust's type safety.
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]Sort of like closing 80% of a submarine's hatches and then diving.
(But if you must use Python then definitely use Pyright.)
And that's assuming the codebase and all dependencies have correct type annotations.
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.
Rust even has tooling (clippy) that warns about unidiomatic code.
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.
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.
https://en.wikipedia.org/wiki/Programming_in_the_large_and_p...
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.
Related: https://man7.org/linux/man-pages/man2/futex.2.html#:~:text=%...
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.
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.
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.
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.
This is an internal tool that isn't our main product so I don't see any value in spending extra time writing needless long-winded "did the user pass the right thing" tests or getting a static analyzer going so I just chose a language with as little build system/static analyzer/built-in unit testing/separate runtime installation headaches as possible to get things going easier.
That was three years ago and the tool is about 4x more featureful than when it started.
They are very productive for first drafts. Then the code tends to “melt” as more people work on it or you do refactors because there is no static type system to catch obvious problems or enforce any order.
Its always funny when i see these kinds of posts.
That's the main reason why I chose to compare it with TypeScript, that is also a statically typed language, but just can't catch some issues that Rust can.
I kind of wish Haxe would have taken TypeScripts place as it is simply a better language overall.
What's funny about it?
Its a form of re-discovery, and im happy people "find" the joy of it.
Rust borrows heavily from the other ml's and thats why many like it, not realizing there is decades of research behind the whys.
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.
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).
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.
That is usually why you have tests for your code. But if you have no tests a programming language with a strict compiler is of course more helpful. But the best is to write tests. Then you can also refactor code with confidence written in "sloppy" programming languages.
Tests should be used where you can't prove correctness statically. But it's better if you can.
The ultimate end point of this is formal verification, where you need very few - if any - runtime tests. But formally verifying software is extremely difficult, so you can't usually do that.
But for some reason writing tests always reminds me with xkcd "Standards" (https://xkcd.com/927/). Instead of "fixing standards by create another standard", now it's catching code bug with more code.
At least for type system, it gets maintained by language maintainers, not project maintainers.
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.
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
Rust can use that type information and lifetimes to figure out when it's safe and when not.
- number of different projects you work on
- time since you last worked on the project
- number of different people working on the project
without the lines changing much.
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.)
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.
Time spent debugging is halved and you can focus on the actual product
About the article itself: the part of wrapping a structure in a mutex because it is accessed concurrently is a bit of a red flag. If you are really working in large codebases, you'd like to avoid having to do that: you'd much rather encapsulate that structure in a service and make sure all access to the structure is queued. Much simpler and less chance for nasty deadlocks.
Wouldn't that be unfair to them? They still use null or Nullable. They don't have ADTs, their thread-safe invariant is maintained by docs, etc.