> I can express a lot in Python, but I don't trust the code as much without robust tests.
This is a major part of why I like languages like rust. I can do some pretty fearless refactoring that looks something like:
- Oh hey, there’s a string in this struct but I really need an enum of 3 possible values. Lemme just make that enum and change the field.
- Cargo tells me it broke call sites in these 5 places. This is now my todo list.
- At each of the 5 places, figure out what the appropriate value is for the enum and send that instead of the string.
- Oh, one of those places needs more context to know what to send, so I’ll add another parameter to the function params
- That broke 3 other places. That’s now my to-do list.
Repeat until it compiles, and 99.9% of the time you’re done.
With non-statically-typed languages you’re on your own trying to find the todo list above. If you have 100% test coverage you may be okay but even then it may miss edge cases that the type checker gets right away. Oh and even then, it’s likely that your 100% test coverage is spent writing a ton of useless tests that a type checker would give you automatically.
As nice as weakly/dynamically typed languages are to prototype greenfield code in, they lose very quickly once you have to maintain/refactor.
And if say one of your enum variants expects a string reference (pointer), the borrow checker will guide you through ensuring that the reference you pass in is valid at all callsites.
Importantly, no tests are required to guarantee that the refactor is safe - although no guarantees that it’s logically correct.
On the other hand, doing this exercise in a different low-level language involves a lot more “thinking” instead of just following the compiler’s complaints :)
With rust, I treat compiler errors as ultra-fast unit tests. I share the same experience: once it compiles, 99.9% it works fine on first try. It's a wonderful development experience.
I completely agree, if it is really 100% Rust, or some great high-level bindings. Else it just becomes C++ with nicer syntax imo, and if my code isn't anything too fancy I could just write it in Python which likely has even more ergonomic bindings.
In my free time I code 90+% in Rust, but for some areas, like OR (SAT, MILP, CSP), ML or CAS Python seems to be the better choice because types don't matter too much and if your code works, it works.
By default, strictness is opt-in with TypeScript, and many JS APIs, especially older ones, don't even have types yet.
Having a type system from the start that cannot be disabled and that forces you to always think of types instead of allowing sprinkling 'as any' when the code works but doesn't compile which is a major annoyance, is a huge benefit in my opinion.
All JS APIs are typed (browser, nodejs, etc.), if you meant libraries, yes, not all of them are typed. But the vast majority have community types in DefinitelyTyped. Also, it is trivial to type an unknown library yourself, or at least type only the relevant parts for your work.
Exactly, if they are used enough that someone declared the types in a @types subrepo. Sometimes these are excellent. However, I sometimes work with code in fairly niche domains written in pure JS that can pretty much return anything depending on the input (not necessary even input types), rendering even these bindings very hard to write and not ergonomic at all.
And this sometimes holds for even fairly popular libraries, like d3.js which I sometimes use for visualization. The idiosyncratic API design for object manipulation, selecting DOM nodes by string id and doing stuff based on their associated data, just doesn't really work in a strongly-typed context without 50% of the code being unreadable casts. And d3 is still trying at least to be somewhat type-safe, unlike other libraries.
For what it's worth, I get the same experience you're describing with statically typed python wired up to mypy. Not that rust and python have the same feature set in other ways.
With Visual Studio and C#, you can do that with a built-in wizard without even having to compile.
It scares me how good C# is these days. Every killer feature of Rust and Lisp is already in C# or started there. Visual Studio makes VSCode look like a 90s shareware tool. Even the governance, by MS of all entities, is somehow less controversial than Rust’s.
It's really not true that "every killer feature of Rust and Lisp is already in C#" although it's certainly true that C# is a nicer language today than it was twenty years ago.
Sum types are a must-have for me. I don't want to write software without sum types. In C# you can add third party libraries to mostly simulate sum types, or you can choose a style where you avoid some of the worst pitfalls from only having product types and a simple enumeration, but either is a poor shadow to Rust having them as a core language feature.
Also VS is a sprawling beast, I spend almost as much time in the search function of Visual Studio finding where a solution I've seen lives as I do hand solving a similar problem in Vim. I spend the time because in Vim the editor won't get in my way when I solve it by hand, while VS absolutely might "helpfully" insert unrelated nonsense as I type if I don't use the "proper" tools buried in page 4 of tab 6 of a panel of the Option->Config->Preferences->Options->More Options->Other section or whatever.
Visual Studio is what would happen if Microsoft asked 250 developers each for their best idea for a new VS feature and then did it, every year for the past several decades, without fail. No need for these features to work together or make sense as a coherent whole, they're new features so therefore the whole package is better, right? It's like a metaphor for bad engineering practice for every Windows programmer to see.
Use VSC instead. The higher-level alternative to Rust is more so F# than C# as it has comparably powerful type system with different tradeoffs (gradual typing and full type inference across function boundaries - it's less verbose to write than Rust because of this). Otherwise C# is not tied to VS at all because all the tools that it provides have alternatives either in Rider/JB suite and/or in a self-contained CLI way together with just using VS Code. .NET's CLI is very similar to Cargo in either case.
Visual Studio ONLY works well on Windows, the mac version is not the same, and certain features just does NOT work as well as the Windows version. And having a language tight down to an IDE which is tight down to a proprietary OS is a deal breaker for me! No matter how good it is these days with every killer feature of Rust and Lisp.
I would rather use a more raw unrefined version of tech that is open source. So my code and DX is not at wimp of some corporate over lord! And given MS's track record I do
C# with Rider is a more comprehensive experience than Rust + RA + VSC. C# in VSC is about on the same level with Rust regardless of the platform.
I always considered C#, F# and Rust as languages complementary to each other since each has their own distinct ___domain and use cases despite a good degree of overlap. Much less so than Java/Kotlin and Golang or any interpreted language (except Python and JS/TS in front-end) which are made obsolete by using the first three.
c# has been my go to language for everything except frontends for the past 15 years, but there are still some things I really miss from Rust.
The top one is probably pattern matching. Sure C# has something similar with switch expressions, but with them you must assign something, and they cannot contain code blocks. Related to this something like enum variants is also missing, and therefore making something similar to Result or Option is not really feasible without it being quite hacky.
Also being able to create new types from existing ones with eg struct Years(i64); and pass it around typed is quite nice in Rust (F# has something similar, however there it will then always also be assignable to i64, so is not very helpful for catching incorrect usage.
I’d be eager to hear folks poke holes in this opinion, but it seems like C# has made a wild mess of class/record initialization in recent versions, whereas in Rust fields are either simply required or are optional.
Properties in C# follow a similar pattern. Constructors can have their own arguments which can also be mandatory or optional. In Rust this logic simply lives in a function instead.
The only issue in C# is that structs come with a. default parameterless constructor (which can be overridden) and b. can be default-intialized (default(T)) without the compiler complaining by default. These two aspects are not ideal and cannot be walked back as they were introduced in ancient times but are rarely an issue in practice (and you can further use an analyzer to disallow either).
F# is more strict about it however and does not have such gaps.
There’s also (in C#) init, readonly, required, and quite a few more keywords and techniques, governing property mutability, private class field mutability, and so on.
And then none of those techniques work as well as manually typing out a required constructor which hard-enforces that required data be priced upon object initialization.
I understand required vs optional immediately a la Rust and F# (ignoring for a moment F#’s null awareness) but as a 17 year C# dev, I’ve had to create an initialization chart to keep straight all of the C# techniques.
> Also being able to create new types from existing ones with eg struct Years(i64); and pass it around typed is quite nice in Rust (F# has something similar, however there it will then always also be assignable to i64, so is not very helpful for catching incorrect usage.
> This is a major part of why I like languages like rust. I can do some pretty fearless refactoring that looks something like:
The process you describe (with “compile” replaced with “typecheck”) works fine for me in Python, with Pylance (and/or mypy) in VSCode.
> With non-statically-typed languages you’re on your own trying to find the todo list above.
This would be more accurately “in workflows without typechecking”, it’s not really about language features except that, long ago, it was uncommon for languages where running the code didn't rely on a compilation step that made use of type information to have typechecking tools available for use in development environments, and lots of people seem to be stuck in viewpoints anchored in that past.
I don’t see a need to get so hung up on the nomenclature… it should be obvious that if you’re using a separate type checker in python, then of course you’ll also get the benefits I describe. The distinction I’m drawing is of course “has type checking” vs “does not hand type checking”, and it seems like you simply agree with me.
The problem with python, ruby, JavaScript, and similar languages is that while yes, they have optional type checkers you can use… they were invented after the fact, and not everyone uses them, and it’s not mandatory to use them. The library you want to use may not have type information, etc. It’s a world of difference when the language has it mandatory from the start.
And that’s not even getting into how (a) damned good rust’s type checker is (b) the borrow checker, which makes the whole check process at least twice as valuable as type checking alone.
I think that's still a different process. I really love pylance, but the issue is that while it can make your code almost as good as it can be with a compiled (statically typed) language if you use strict typechecking, it still can't make up for the issues that come with any library you use. Some popular packages are well annotated but some aren't, meaning that it's just not as good as soon as you start using 3rd party packages.
This is a major part of why I like languages like rust. I can do some pretty fearless refactoring that looks something like:
- Oh hey, there’s a string in this struct but I really need an enum of 3 possible values. Lemme just make that enum and change the field.
- Cargo tells me it broke call sites in these 5 places. This is now my todo list.
- At each of the 5 places, figure out what the appropriate value is for the enum and send that instead of the string.
- Oh, one of those places needs more context to know what to send, so I’ll add another parameter to the function params
- That broke 3 other places. That’s now my to-do list.
Repeat until it compiles, and 99.9% of the time you’re done.
With non-statically-typed languages you’re on your own trying to find the todo list above. If you have 100% test coverage you may be okay but even then it may miss edge cases that the type checker gets right away. Oh and even then, it’s likely that your 100% test coverage is spent writing a ton of useless tests that a type checker would give you automatically.
As nice as weakly/dynamically typed languages are to prototype greenfield code in, they lose very quickly once you have to maintain/refactor.