1) Rust's type system or something as expressive 2) C#-level performance 3) GC by default 4) Ecosystem that is as big as Rust
The closest thing seems to be TypeScript (weirdly).
1) Rust's type system or something as expressive 2) C#-level performance 3) GC by default 4) Ecosystem that is as big as Rust
The closest thing seems to be TypeScript (weirdly).
Also, a garbage collector that is state of the art (bump allocation, generational, and compacting) is faster overall typically (throughput-wise, but with less predictable latency) than naïve Rc/Arc all over the place (due to usage of "free list" allocator and counter bumps). That isn't to say blisteringly fast Rust isn't possible to write, and in fact it is pretty easy to write by avoiding Rc/Arc except where necessary and using the stack with the borrow checker, but this entails a writing style that is more "low level" and thus takes a bit more thinking.
1. Interrupting the program to sweep, resulting in unpredictable performance.
2. Possibility that a circularly linked group of variables could be impossible to sweep, resulting in memory leaks.
3. Need to check reference counts (along with bounds checks) that degrade performance.
Lack of GC (and bounds checking) are factors that make C/C++ performant and then lead to the kind of bugs that result in programs that don't do what they're supposed to do (and at worst, result in security vulnerabilities.)
I thought a key goal of Rust was to fix these problems without sacrificing performance.
See my comment regarding "less predictable latency" (although new advances are making for much more predictable latency - see some of the work done in Java GCs for example)
> 2. Possibility that a circularly linked group of variables could be impossible to sweep, resulting in memory leaks.
That is not possible in a precise tracing collector, only in reference counting (Rc/Arc)
> 3. Need to check reference counts (along with bounds checks) that degrade performance.
I think you are referring to reference counting again. Both Rust and C++ have reference counting types as an add on.
State of the art tracing collectors don't collect/check dead objects, they compact live ones, and often don't use reference counting directly. Most objects die young.
> Lack of GC (and bounds checking) are factors that make C/C++ performant and then lead to the kind of bugs that result in programs that don't do what they're supposed to do (and at worst, result in security vulnerabilities.)
Replace "performant" with "predictable performance"
> I thought a key goal of Rust was to fix these problems without sacrificing performance.
Rust, like C/C++, wants you to be able to predict performance and latency and without the baggage of a runtime. Nothing more than trade offs - neither is better or worse. GCs often do perform better with the trade off of latency predictability, but as always it depends on use case as to what is appropriate. Rust likely made the right choice for its domain.
1) this is generally true, but many part of this can be done concurrently, and if we want to improve latency at the cost of some throughput than there are low-lat GCs that maximize the pause times (it is basically independent from the heap size), so in practice you have similar interrupts as you would from the OS alone.
2) this is not a problem with tracing GCs, only with refcounting (most well known is Python, ObjC and Swift for this perhaps). You can still leak memory everywhere by e.g. storing them in a huge list forever, though, but that is a much rarer and easy to debug bug.
3) this is again only true for RC, and it is the reason why it is slower than tracing GCs, especially when it is multithreaded and the increment/decrement has to be an atomic operation.
Without a GC (i.e. rust), in order to be able to make guarantees about things that it cannot determine at compile time (this is a mathematical impossibility) it restricts the set of programs you can write.
Whether a variable goes out of scope is trivial in many languages. The problem is with determining whether the lifetime of a value ends.
For example:
func foo(items, moreItems) {
b = new bar()
if coinflip() {
items.append(b)
}
if coinflip() {
moreItems.append(b)
}
// ‘b’ goes out of scope here, but its value
// may live on inside ‘items’ and/or moreItems
// and will have to be destroyed when it’s no
// longer stored in either.
}
In general, once you allow for dynamic allocation and references that can be copied (so that multiple objects ‘know’ of the allocated object), it can be very difficult to determine exactly when the last reference to such an object ceases to exist.Depending on what you mean by kind: yes. That is, Rust does limit your code’s architecture to a subset that is fine for most things, but not everything.
* Type system is even accidentally Turing complete
* Very good perf, but language doesn't help by being indirection-friendly. Value types will help a lot.
* SOTA GCs
* Ecosystem big
* Cheap threads now. Don't do async! Just block.
* Structured concurrency soonish
A turing complete type system is easier to stumble into than to avoid.
That doesn’t mean the type system is expressive. Turing tarpits are turing complete by definition, and nobody would call Thue, Iota, or the average OISC expressive.
Since, as I feel it, Rust was/is started as an attempt to bring roughly the ML (Hindley-Milner) type system to the area of `systems` (non-garbage collected) development.
In the early days of Rust I thought of it as type inference & algebraic data types meets C++ (now kiss!). But then the borrow checker stuff went in there and it took a different turn.
And I think you'd be surprised by how large the OCaml package community is.
People who work in Rust would likely find OCaml very familiar after a week or two of hacking.
The other option is Swift, but it's pretty ghetto-ized into the Apple ecosystem.
- absence of null pointers
- proper sum types which can be statically checked for exhaustiveness
- statically controlled mutability (yes, borrow-checking is still useful if you have a GC!)
Also no exceptions, but that's not a type system feature.
The compiler will ensure you check for null before de-referencing a nullable type.
C# is my main language. I consider it a very good all-round language.
Rust's type system has some advantages over C# tho, for example Sum Type, Option (C# has ? but it was added later so you need to be careful when interacting with old code, kinda like TypeScript <-> JavaScript to a lesser extent), exhaustive enum, etc.
Another thing I don't like about C# is the runtime startup time which prevents me from using it for command line tools (Yes I prefer static typed languages even for "scripting"). I think Go has proven that you can have both GC and extremely fast startup time.
- sum types via sealed classes (not great, admittedly)
- enum "when" expressions are exhaustive
- option type via built-in nullable types (honestly the superior solution)
JIT:
dotnet publish -c release -o publish -p:PublishSingleFile=true -p:PublishTrimmed=true
AOT: dotnet publish -c release -o publish -p:PublishAot=true
Either one has really good startup time (below ~100ms and 20-30ms respectively depending on what you do), compact binary size and require no external dependencies. Just like in Go except without all shortcomings of Go :)p.s.: AOT on macOS requires .NET 8 preview (will be released in November)
Additionally, https://github.com/c-blake/cligen makes decent CLI tools a real breeze. If you like some of Go's qualities but the language seems too limited, you might like Nim: https://nim-lang.org. I generally find getting good performance much less of a challenge with Nim, but Nim is undeniably less well known with a smaller ecosystem and less corporate backing.
EDIT: I make only observations here, not demands. Another observation on the same machine is `python-3.11.5 </dev/null` with an empty environment taking 7.85 +- 0.02 ms.
EDIT: Also, it's misleading to bundle Nim with C and C's many & storied footguns. While "low-level" is somewhat subjective and you can opt-in to go as low as C (if you so desire), most Nim code is as high-level as Python or C# with various choices in automatic memory management, and the language has very high-level capabilities. E.g., Nim has user-defined operators like Julia. Want to add `=~` or `~` for regex pattern matching? No problemo. In that aspect, Nim is arguably higher-level than C#.
Then the answer is F#. OCaml has everything except the big ecosystem. Haskell has a way bigger Ecosystem than OCaml, but is still not comparable to Rust.
Pick up some F# then. Both can interop, I've built many things in a combo of the two languages, plus it'll make you a better C# developer and your type system power greatly increases. Be careful though you might not want to go back to C#.
In reality, it seems like the python developers and toolchain are embracing rust enough to reduce the benefits to a new alternative.
Sum types are built-in [1] for formal parameters. `nil` is only for `ref|ptr` types. In much code you can just use stack allocated value types and there is neither GC concern nor nil concern, but there is also a mode to help: https://nim-lang.github.io/Nim/manual_experimental_strictnot...
Nim has an easy-ish to use Lisp-like syntax macro system where you just receive & process an AST. So, to do the rest you can make libraries adding the feature without relying upon upstream compiler: such as https://github.com/beef331/sumtypes for variables with sum types or pattern matching libs like https://andreaferretti.github.io/patty/ | https://github.com/alehander92/gara.
Probably the closest would be Kotlin. It has the huge JVM ecosystem, it uses GC, programs run fast, they can be compiled to native binaries using at least two different native compilers (kotlin/native and graalvm). The type system is fairly expressive, albeit not quite as much as TypeScript. It makes up for it with just being a much cleaner and more logical language in general, as it didn't inherit much historical baggage.
There's also a compiler plugin API which is maybe the closest equivalent of macros. It's not really documented or stable in Kotlin 1.x, they're fixing that for Kotlin 2, but there are already a bunch of useful plugins that add various features via compile-time generation and reflection.
Sun types, affine types, traits.
1. ADTs with exhaustive pattern matching
2. No exceptions
3. Zero cost generics
4. Traits
5. No nulls
6. No inheritance
As a rule of thumb, languages that loudly claim not to have exceptions actually use them in some way (see POSIX C, Perl, Go).
In idiomatically written Rust you will never obtain an exception/panic (unless there is a bug), and exceptions are not used for control flow. This is not the case in Java or C++ or many other languages.
/// The compiler currently unwinds with a special sentinel value to abort
/// compilation on fatal errors. This function catches that sentinel and turns
/// the panic into a `Result` instead.
That's clearly using unwinding as a control flow primitive.There's another example in src/tools/rustfmt/src/lib.rs, in format_snippet, where panics apparently are just suppressed.
I expect application servers for Rust to handle panics in a similar way: abort a specific request, but not terminate the whole process.
F# is not magical. Yes it's an ML, but frankly, C# is better in every way, to the point we're slowly moving away from F# entirely.
The main reason is perf, it's really easy to shoot yourself in the foot with performance in c# (e.g. huge allocations, accidentally evaluating seqs twice, etc). Also IDE support for F# sucks when you get to the hundreds of project solutions like we have.
For side projects, sure use F#. For everything else, stick with C#.
1. No null pointers 2. No exceptions
C++/CLI covers some of these aspects, but I haven't used it and don't know how large the vcpkg ecosystem is.
Tons of languages have better type systems.