Reading Rust theads on /g/
is an endless source of astonishment . We live in the age of enshitffication of knowledge and even information, where literal genens are obsessed with emotionally charged narratives about make-believe problems. Anyway, whatever.
The real and fundamental innovation behind Rust is this.
The great well-educated by studding the underlying principles of everything around them “old-timers”, like Joe Armstrong (R.I.P.), discovered the mathematical /fact that sharing and mutation cannot coexist in principle. The fundamental mathematical property of “referential transparency” is necessary to make any guarantee about the correctness of a program. The notion is that everything is an expression and any expression is “pure”, and only denotes a value (to which it can be eventually reduced or evaluated to. Thus, any expression can be substituted by a value it denotes (eventually evaluated to) without changing the meaning (semantics) of the any other expression within a program. This is the essence of functional programming, and it is a fundamental property of all correct programs.
Imperative programming cannot have this in principle – It is “command”, not expression, based, it operates on variables, not immutable bindings – a named box-like mutable memory locations, and it over-writes the content of these boxes (memory locations), instead of producing new values. The procedures (misleadingly called “functions”) do not always produce same output for the same input, thus breaking the referential transparency in principle (in one more way).
Because every binding is immutable (and the new ones are always being produced), there is no explicit notion of a “reference” (or a pointer) – every binding is a “symbolic pointer” within an “environment”, which is a higher level (a lookup-table or even a function-like) abstraction over raw memory.
The first innovation of Rust was to formalize and meaningfully restrict the behavior or references, to come close to the necessary ideal of mathematical purity.
Actually, no. The first innovation was to switch to the move semantics by default and make Copy
-able types a “minority” that need to be defined explicitly, just an opposite of what C and especially C++ do (where the moves must be explicitly and verbosely defined). This makes function’s parameters passing and returning to be a move (unless it is a value of a machine type – a plain, not-nested sequence of bits which denotes a machine representation), so no elicit moves on return and other C++ “magic”. Everything is much more uniform and consistent, which is absolutely required for a well-designed languages (a category to which C++ is an exact opposite). This “consistency and uniformity” allowed all other features to be complementary and mostly orthogonal to each, while together provide fundamental compile-time guarantees (compile-time type-level proofs of correctness) no other imperative language could have in principle. Restrictions on the languages semantics (reducing the set of all possible “constructs”) is what was necessary.
The best they could have is the less strict notion “borrowed” from the realm of Functional Programming – to have at most one mutable reference to a value at a time, or any number of immutable references. To achieve this, Rust introduced the concept of a “lifetime” , which is a compile-time notion of how long references are valid.
The notion of a “lifetime” is a simple concept. This is essentially, to superimpose a “time” dimension on the “space” of the program. A “time-interval” of arbitrary units is associated with every variable and reference, and the compiler checks that references of the same value do not overlap “in time”, i.e. that they are /not used at the same time. This “emulates” just one aspect necessary for the mathematical property of referential transparency, and this is exactly how ref
types (which are mere ADTs) have been implemented in all functional languages.
The most beautiful part of it is that the “time” here is not a real time, but a conceptual time scale – a super-imposed “time scale” on the program, which is conceptually sliced into non-overlapping intervals. What is actually “measured” and “compared’ to be “at least long” and “non-overlapping” is the length of nested scopes. Every variable and reference is defined within an implicit scope, at the end of which it will be “dropped”, unless being explicitly “moved” into another scope, to which this one has been nested into.
What is being compared are these lengths of nested scopes (super-imposed onto each other, as the time-duration bars in the fucking MS Project) in which each variable and reference are “living” (until it is drooped), so this is why it is intuitively but misleadingly called “lifetime”. That “not used at the same time” just hides a less intuitive “mutual exclusion constraint”, as in a turnstile. But there is, if course, no time and no clocks. There is, in principle, no notion of time in mathematics and in a serious programming, only super-imposed abstract “scales” of equally-spaced “notches” to count.
The non-overaping is a fundamental concept, and this is, in essence, is serialization of time intervals associated with references of two different kinds. Once a mutable reference is created, no other mutable references can be created until the first one is dropped, and all existed immutable reference are being invalidated. Conceptually, this is a compile-time “exclusive lock” on the value, which is being held by the mutable reference. Read this again – it is a compiler-enforced, semantic level compile-time exclusive lock on a value by an at most onemutable reference at a time.
A super-imposed “time scale” (a bunch of equally-spaced “marks”, just like the number scale) being “sliced” into non-overlapping intervals. This is a very “natural” and elegant way to implement the “exclusive lock” concept. The compiler checks that no two mutable references overlap in time, and that no mutable reference overlaps with any immutable reference.
This does not make an imperative language reverentially transparent, but it formalized the behavior of references to mimic the fundamental property of immutable bindings in a functional languages. It allows to have a small “functional subset” of Rust, which is based on expressions, algebraic types and pattern matching, and this subset is technically reverentially transparent/, and can be reasoned about in a mathematical way.
No other imperative language have such semantic properties, and this is the only reason to choice Rust – it brings some sanity and mathematical rigor to the fucking mess of an imperative programming, which is otherwise fundamentally flawed and cannot be made mathematically sound in principle. These are just facts of life.
Notice, that the implementation is still PHP-sque, meaning that overzealous but grossly uneducated and ignorant amateurs code most of the stuff in busts of unwarranted and over-confident enthusiasm. But the mathematical theory behind it is simple and sound.
The second (related) innovation of Rust is the concept of “ownership” and “move semantics”. This is a way to enforce the notion of “who owns what” in the program, and to ensure that values are not duplicated or shared in an unsafe way. The ownership model is based on the idea that every value has a single owner, and when the owner goes out of scope, the value is automatically dropped (deallocated). This prevents memory leaks and dangling references.
Ownership is enforced at compile time, and it ensures that values cannot be shared or duplicated in a way that would lead to data invalidation or data races. This makes concurrency in Rust safe by default, as the ownership model provides some basic guarantees about how data is accessed and modified.
These are an evolution step from RAII, “move semantics” and “owning smart pointers” in C++ and other languages, but Rust formalized the concepts and made them a fundamental part of the language. This is the second reason to choose Rust – it brings some sanity and mathematical rigor to the fucking mess of an imperative programming, which is otherwise fundamentally flawed and cannot be made mathematically sound in principle.
To summarize so far. The type system of Rust has been extended with explicit notions of
- a lifetime for every value and every reference.
- for a unique (at most one) owner of every value.
Under the hood the compiler makes sure that:
- no mutable reference lifetimes overlap.
- no lifetime of any reference is longer (literally) than the lifetime of a value it references.
It is that simple – no “crosses”, and “at least as long”
These enforced “invariants” (on the semantics of the code) provide so called “fundamental memory-safety guarantees”, from which (the enforced semantics) “safe concurrency” simply follows.
The guarantees are:
- no “use after free” (guaranteed that all the references are gone)
- no “dangling references” (guaranteed that no reference outlives its value)
- no “data races” (at most one mutable reference at any given time)
- no “use after move” (enforced invalidation of all references)
- no “data invalidation” (the direct consequence of the above)
Having them together allows to write a carefully chosen subset of Rust code to be sort of “mostly functional”, as, let’s say, in Ocaml, which allows limited mutability of refs
. Notice that “mostly”.
What is required to have the FP-like semantic properties is to create new values instead of mutating existing ones, passing them and the “contexts” explicitly as parameters and return values of composed (nested) pure functions, and to use the “move semantics” to transfer ownership of values between (nested) scopes, which, if you pause and think for a second, is a very natural way to write code (which is FP Chads do, except “moves”, which does not exist in the realm where every binding is immutable.
The third innovation of Rust is the concept of “traits” and “trait bounds”, which are a shameless rip-off of Haskell type-classes (which his good). Traits are a way to define shared behavior across different types (also known as “ducktyping”), at the level of abstract interfaces, which is the only right way to type. They allow for polymorphism and code reuse, enabling developers to write generic code that can work with any type that implements a specific trait (“walks like a duck”).
The missing part is a Monad-like abstraction barriers (out of Traits, of course) to partition the code, as in Haskell, and keep all the destructive mutations being performed within a given ADT inside of such a “Monad”. Notice that this is about a proper code-structure and types, not the strict language feature (as in Haskell IO Monad). It just adds some more simple guarantees (that mutations never occur outside of some private interface, and that these mutations are properly composed or “serialized”).
And yes, this is a good thing, and it is a fundamental property of all correct programs. This is the third reason to choose Rust – it brings some sanity and mathematical rigor to the fucking mess of an imperative programming, which is otherwise fundamentally flawed and cannot be made mathematically sound in principle.
The problem is, of course, that almost zero crates (library code) has been written in this way, because the majority of the Rust code is written by amateurs, who are not educated in the underlying principles of program semantics and underlying mathematics. The majority of the crates are written in a PHP-sque way, which is a shame, but this is a problem with the community, not with the language itself.
And no, I do not support or endorse that biological fraud – an appearance-based deception – which /g/
degens associate with Rust.