An intuitive understanding – approximate, imprecise but not wrong in the principles. A level of vague abstractions which capture the right underlying relationships – nothing essential is missed, nothing imaginary and non-existent is added.
Everything “just naturally follows” from (can be explained by) the “right [intuitive] understanding”. This is what our ancient ancestors used before they have evolved the “scientific method”.
Almost everything, however, can be “explained” (to a fool) by an arbitrary super-natural make-believe nonsense, so we have to be very suspicious to what the “talking heads” are saying and question everything.
The ultimate goal is to build up your own “inner” intuitive understanding right from the first principles, and to use this understanding for explanation and decision making.
Yes, yes, this is a “theory-building”, and the view of programming (as well as of mathematics itself) as [an abstracted from irrelevant details and particulars] general (and generalized) theory building.
Fortunately, computers give us the necessary “experimental testing and validation” for our intuitive abstract theories, and the feedback loops from the environment show how well their parts corresponds to the actual aspects of What Is.
So, the intuitive (the Right) understanding of the principles behind Rust is this.
General principles:
- A variable (a named box-like memory location) “owns” a value.
- Each “box” (for a value of a particular type) has an address (an offset).
- Each value has single “owner” at a time (responsible for cleaning up).
- A value “actually moves” around – “changes hands” or “ownership”.
- The “sources” (right-hand-sides) become invalidated (“emptied”).
- Can be re-used after being explicitly re-initialized to a new value.
- References are non-owning pointers, either mutable or immutable.
- They point to a value’s location, distinguishing from the value itself.
- No reference (an address of a “box”) “outlives” its value in principle.
- Taking “by reference” does not move anything anywhere.
- Taking a mutable reference exclusively locks a value (its box).
- Anything “by value” actually moves (changes hands or ownership).
- “Fat pointers” manage heap-allocated data during moves (zero copying).
- Blocks of code (local nested scopes) take ownership (moved into).
- For loops and clauses are such “blocks”. And any function, of course.
- The actual formal parameters and return values are passed “by value”.
- Unless explicitly passed by references, to functions with require them.
- Iterators are just sets of interfaces (traits) implemented by functions.
- Smart pointers encapsulate resource management, wrapping the data.
- So all the general principles apply to any higher-level construct.
- Consistency and uniformity of the behavior is the key to “stability”.
Relations to FP:
- Mutable references is a “necessary evil” of procedural imperative programming.
- in FP no value or a binding is ever modified, new ones are always created instead.
- The
ref
types of FP are just proper closures over its “data”. - They are always created anew and being passed around like any other value.
- The referential transparency does not break (mutated
ref
content do not leak). - The compile-time proven guarantee of one and only one mutable reference
- is semantically equivalent to explicitly passing a
ref
(invalidating all other references). - Compound types (“owning smart pointers”) have the same underlying /constraints.
- This is as close as imperative programming can come close to the ideal of FP.
- At the cost of extra verbosity to explicitly specify the “borrowing” aspects.
Now try to visualize this “classical mechanics” of programming:
- A value “moves” to new locations, leaving behind a DAG of all its previous states.
- It moves through a very real DAG of all potential pathways, defined in the code.
- It is pre-defined and fixed, not ever-expanding before it, as in the actual Universe.
- Values being “accumulated” at some nodes, and “used” at the other.
- The DAG structure “everywhere” (at all levels of abstraction) is not accidental.
- The “isomorphism” extends as far as to the actual brain neural structures and synaptic “gaps”.
- This is the hint that the way to program is to construct and move through such DAGs.
- The FP sages of the golden age of programming intuitively realized this fact.
- This “isomorphism” is mine. No one writes like I do.
Move semantics:
- Shallow “moves” of “fat pointers” between “owners” (variables).
- Keeping the “owned” (nested) heap-allocated data intact.
- The simple “atomic” machine types are exception (via the
Copy
trait)
A better RAII:
- Everything becomes invalidated after a “move” or at the end of its scope.
- Parameters and return values are passed by value (hence moved)
- Drops at the end of a scope, unless moved up into the caller’s scope.
- Use references to avoid any transfer of ownership (actual moves).
References:
- A non-owning “atomic” pointer to a value (in a particular memory location).
- Either at most one
&mut r
or at least one&r
(two distinct kinds). - A reference [to a “box”] is not the same as the value itself (in its box)
- A “borrow” [of a value] is a crappy, misleading ill-defined concept.
- Nothing is being “moved out of the box or “returned back into it”.
- A reference is still an address (an offset) of a value’s “box” in memory.
Lifetimes:
- An abstract scale super-imposed on nested scopes (code blocks).
- The mathematical notions “as long as” or “encloses” (properly nested).
- Every variable has an implicit lifetime, implicitly defined by its scope.
- Every reference has an explicit /lifetime, which is tied to a value it references.
- A compile-time proofs of mutual-exclusion guarantees (for references).
- At most one and only one
&mut
at a time (all other refs are invalidated).
“Smart” pointers:
- Just an ADT (an implicit lambda) with some additional nested structure.
- Has an associated set of constraints (informally defined as a set of rules).
- A wrapper around an address only (
Rc
) - or an address together with some data, (
Box
,Cell
) - “Owns”, “cleans up” and manages its “resources”.
- Encapsulates the “logic” for providing a specific behavior.
“Smart” constructors:
- The way to make “invalid/inconsistent states unrepresentable”.
- By establishing specific representation invariants at a construction time.
- Unfortunately, is not enforced by the compiler, so will be broken by degens.