6 ms·
I think the explanation is: When you mutate variables it implicitly creates an ordering dependency - later uses of the variable rely on previous mutations. Howe
by rendaw 1y ago
I think the explanation is: When you mutate variables it implicitly creates an ordering dependency - later uses of the variable rely on previous mutations. However, this is an implicit dependency that isn't modeled by the language so reordering won't cause any errors.
With a very basic concrete example:
x = 7
x = x + 3
x = x / 2
Vs
x = 7
x1 = x + 3
x2 = x1 / 2
Reordering the first will have no error, but you'll get the wrong result. The second will produce an error if you try to reorder the statements.
Another way to look at it is that in the first example, the 3rd calculation doesn't have "x" as a dependency but rather "x in the state where addition has already been completed" (i.e. it's 3 different x's that all share the same name). Doing single assignment is just making this explicit.
- deleted 1y ago[deleted]
- skeezyjefferson 1y agowhats the difference between immutable and constant, which has been in use far longer? why are you calling it mutable?
- Thorrez 1y agoImmutable and constant are the same. rendaw didn't use the word mutable. One reason someone might use the word "mutable" is that it's a succinct way of expressing an idea. Alternative ways of expressing the same idea are longer words (changeable, non-constant).
- kgwxd 1y agoThey aren't the same for object references. The reference can't be changed, but the properties can.
- Thorrez 1y agoDepends on the language. In C++ const std::vector<int>& foo = bar.GetVector(); foo is a constant object reference cannot have its properties changed (and also cannot be changed to refer to a new object). std::vector<int>& foo = bar.GetVector(); Is an object reference that can have its properties changed (but cannot be changed to refer to a new object).
- a4isms 1y agoIn languages like JavaScript, immutable and constant may be theoretically the same thing, but in practice "const" means a variable cannot be reassigned, while "immutable" means a value cannot be mutated in place. They are very, very different semantically, because const is always local. Declaring something const has no effect on what happens with the value bound to a const variable anywhere else in the program. Whereas, immutability is a global property: An immutable array, for example, can be passed around and it will always be immutable. JS has always hade 'freeze' as a kind of runtime immutability, and tooling like TS can provide for readonly types that provide immutability guarantees at compile time.
- everforward 1y agoArrays are a very notable example here. You can append to a const array in JS and TS, even in the same scope it was declared const. That’s always felt very odd to me.
- raddan 1y agoThat's because in many languages there is a difference between a stored reference being immutable and the contents of the thing the reference points to being immutable.
- deleted 1y ago[deleted]
- DemocracyFTW2 1y agoI think JavaScript has a language / terminology problem here. It has to be explained constantly (see) to newcomers that `const a = []` does not imply you cannot say `a.push( x )` (mutation), it just keeps you from being able to say `a = x` further down (re-binding). Since in JavaScript objects always start life as mutable things, but primitives are inherently immutable, `const a = 4` does guarantee `a` will be `4` down the line, though. The same is true of `const a = Object.freeze( [] )` (`a` will always be the empty list), but, lo and behold, you can still add elements to `a` even after `const a = Object.freeze( new Set() )` which is, shall we say, unfortunate. The vagaries don't end there. NodeJS' `assert` namespace has methods like `equal()`, `strictEqual()`, `deepEqual()`, `deepStrictEqual()`, and `partialDeepStrictEqual()`, which is both excessive and badly named (although there's good justification for what `partialDeepStrictEqual()` does); ideally, `equal()` should be both `strict` and `deep`. That this is also a terminology problem is borne out by explanations that oftentimes do not clearly differentiate between object value and object identity. In a language with inherent immutability, object value and object identity may (conceptually at least) be conflated, like they are for JavaScript's primitive values. You can always assume that an `'abc'` over here has the same object identity (memory location) as that `'abc'` over there, because it couldn't possibly make a difference were it not the case. The same should be true of an immutable list: for all we know, and all we have to know, two immutable lists could be stored in the same memory when they share the same elements in the same order.
- skeezyjefferson 1y agobut we already had the word variable for values that can change. on both counts it seems redundant
- Thorrez 1y agoOh, good point. I misunderstood your previous question. Is there a name that refers to the broader group that includes both constants and variables? In practice, and in e.g. C++, "variable" is used to refer to both constants and actual variables, due to there not being a different common name that can be used to refer to both.
- inanutshellus 1y ago"Constant" implies a larger context. As in - it's not very "constant" if you keep re-making it in your loop, right? Whereas "immutable" throws away that extra context and means "whatever variable you have, for however long you have it, it's unchangeable."
- skeezyjefferson 1y ago> As in - it's not very "constant" if you keep re-making it in your loop, right? you cant change a constant though
- veilrap 1y agoHe’s implying that the variable it’s being defined within the loop. So, constant, but repeatedly redefined.
- ghurtado 1y agoThat's the opposite of what any reasonable engineer means by "constant".
- fnordsensei 1y agoThat’s the point, you’re just haggling about scopes now. All the way from being new per program invocation to new per loop. Immutability doesn’t have this connotation.
- davrosthedalek 1y agoHow? I think the same argument applies: If it's changing from loop to loop, seems mutable to me.
- fnordsensei 1y agoI think you’re after something other than immutability then. You’re allowed to rebind a var defined within a loop, it doesn’t mean that you can’t hang on to the old value if you need to. With mutability, you actively can’t hang on to the old value, it’ll change under your feet. Maybe it makes more sense if you think about it like tail recursion: you call a function and do some calculations, and then you call the same function again, but with new args. This is allowed, and not the same as hammering a variable in place.
- scott_w 1y agoIn plenty of languages, there's not really a difference. In Rust, there is a difference between a `let var_name = 10;` and `const var_name: u64 = 10;` in that the latter must have its value known at compile-time (it's a true constant). > why are you calling it mutable? Mostly just convention. Rust has immutable by default and you have to mark variables specifically with `mut` (so `let mut var_name = 10;`). Other languages distinguish between variables and values, so var and val, or something like that. Or they might do var and const (JS does this I think) to be more distinct.
- munificent 1y ago"Constant" is ambiguous. Depending on who you ask, it can mean either: 1. A property known at compile time. 2. A property that can't change after being initially computed. Many of the benefits of immutability accrue properties whose values are only known at runtime but which are still known to not change after that point.
- throwaway2037 1y agoDotNet/C# makes this distinction in the form of (1) const and (2) readonly.
- jstimpfle 1y agoThe immutable approach doesn't conflate the concepts of place, time, and abstract identity, like in-place mutation does. In mutating models, typically abstract (mathematical / conceptual) objects are modeled as memory locations. Which means that object identity implies pointer identity. But that's a problem when different versions of the same object need to be maintained. It's much easier when we represent object identity by something other than pointer identity, such as (string) names or 32-bit integer keys. Such representation allows us to materialize us different versions (or even the same version) of an object in multiple places, at the same time. This allows us to concurrently read or write different versions of the same abstract object. It's also an enabler for serialization/deserialization. Not requiring an object to be materialized in one particular place allows saving objects to disk or sending them around.
- SpaceNoodled 1y agoThe hardware that these programs are running on store objects in linear memory, so it doesn't not make sense to treat it as such.
- wtallis 1y agoDRAM is linear memory. Caches, less so. Register files really aren't. CPUs spend rather a lot of transistors and power to reconcile the reality of how they manipulate data within the core against the external model of RAM in a flat linear address space.
- jstimpfle 1y agoCan you clarify?
- repstosb 1y agoModern CPUs do out-of-order execution, which means they need to identify and resolve register sharing dependencies between instructions. This turns the notional linear model of random-access registers into a DAG in practice, where different instructions that might be in flight at once actually read from or write to different "versions" of a named register. Additionally, pretty much every modern CPU uses a register renaming scheme, where the register file at microarchitecture level is larger than that described in the software-level architecture reference, i.e. one instruction's "r7" has no relationship at all to another's r7". Caches aren't quite as mix-and-match, but they can still internally manage different temporal versions of a cache line, as well as (hopefully) mask the fact that a write to DRAM from one core isn't an atomic operation instantly visible to all other cores. Practice is always more complicated than theory.
- EastLondonCoder 1y agoI agree that the explicit timeline you get with immutability is certainly helpful, but I also think its much easier to understand the total state of a program. When an imperative program runs you almost always have to reproduce a bug in order to understate the state that caused it, fairly often in Clojure you can actually deduct whats happening.
- deleted 1y ago[deleted]
- ryandv 1y agoThat's right - immutability enables equational reasoning, where it becomes possible to actually reason through a program just by inspection and evaluation in one's head, since the only context one needs to load is contained within the function itself - not the entire trace, where anything along the thread of execution could factor into your function's output, since anybody can just mutate anybody else's memory willy-nilly. People jump ahead using AI to improve their reading comprehension of source code, when there are still basic practices of style, writing, & composition that for some reason are yet to be widespread throughout the industry despite already having a long standing tradition in practice, alongside pretty firm grounding in academics.
- adrianN 1y agoIn theory it’s certainly right that imperative programs are harder to reason about. In practice programmers tend to avoid writing the kind of program where anything can happen.
- ryandv 1y ago> In practice programmers tend to avoid writing the kind of program where anything can happen. My faith in this presumption dwindles every year. I expect AI to only exacerbate the problem. Since we are on the topic of Carmack, "everything that is syntactically legal that the compiler will accept will eventually wind up in your codebase." [0] [0] https://www.youtube.com/watch?v=1PhArSujR_A&t=15m54s https://www.youtube.com/watch?v=1PhArSujR_A&t=15m54s
- ape4 1y agoI would be nicer if you gave x1 and x2 meaningful names
- catlifeonmars 1y agoWhat would those names be in this example?
- ape4 1y agoIn a real application meaningful names are nearly always possible, eg: const pi = 3.1415926 const 2pi = 2 * pi const circumference = 2pi * radius
- tmtvl 1y agoCalling tau 2pi is the most cursed thing I've seen all day. Appropriate for Halloween.
- smrq 1y agoIf you call a variable tau in production code then you're being overly cute. I know what it means, because I watch math YouTube for fun, but $future_maintainer in all likelihood won't.
- lock1 1y agoWhere do you draw the line then? Stopping at `tau` just because `$future_maintainer` might get confused feels like an arbitrary limit to me. What about something like `gamma`? Lorentz factor? Luminance multiplier? Factorial generalization? Why not just use the full sentence rather than assign it to an arbitrary name/symbol `gamma` and leave it dependent on the context? And it's not that hard to add an inline comment to dispel the confusion const tau = 2*pi; // Alternate name for 2pi is "tau"
- catlifeonmars 1y agoAgree in real life you can come up with meaningful names (and should when the names are used far away from the point of assignment), but it doesn’t make sense for GPs example, where the whole point was to talk about assignments in the abstract.
- Tarean 1y agoSometimes keeping a fixed shape for the variable context across the computation can make it easier to reason about invariants, though. Like, if you have a constraint is_even(x) that's really easy to check in your head with some informal Floyd-Hoare logic. And it scales to extracting code into helper functions and multiple variables. If you must track which set of variables form one context x1+y1, x2+y2, etc I find it much harder to check the invariants in my head. These 'fixed state shape' situations are where I'd grab a state monad in Haskell and start thinking top-down in terms of actions+invariants.
- raincole 1y agoYet even Rust allows you to shadow variables with another one with the same name. Yes, they are two different variables, but for a human reader they have the same name. I think that Rust made this decision because the x1, x2, x3 style of code is really a pain in the ass to write.
- suspended_state 1y agoOr they got inspired by how this is done in OCaml, which was the host language for the earliest versions of Rust. Actually, this is a behaviour found in many FP languages. Regarding OCaml, there was even a experimental version of the REPL where one could access the different variables carrying the same name using an ad-hoc syntax.
- airstrike 1y agoI do find shadowing useful. If you're writing really long code blocks in which it becomes an issue, you are probably doing too much in one place.
- wongarsu 1y agoIn idiomatic Rust you usually shadow variables with another one of the same name when the type is the only thing meaningfully changing. For example let x = "29" let x = x.parse::<i32>() let x = x.unwrap() These all use the same name, but you still have the same explicit ordering dependency because they are typed differently. The first is a &str, the second a Result<i32, ParseIntError>, the third an i32, and any reordering of the lines would provide a compiler error. And if you add another line `let y = process(x)` you would expect it to do something similar no matter where you introduce it in these statements, provided it accepts the current type of x, because the values represent the "same" data. Once you actually "change" the value, for example by dividing by 3, I would consider it unidiomatic to shadow under the same name. Either mark it as mutable for preferably make a new variable with a name that represents what the new value now expresses
- ymyms 1y agoAnother idiomatic pattern is using shadowing to transform something using itself as input: let x = Foo::new().stuff()?; let x = Bar::new(x).other_stuff()?; So with the math example and what the poster above said about type changing, most rust code I write is something like: let x: plain_int = 7 let x: added_int = add(x, 3); let x: divided_int = divide(x, 2); where the function signatures would be fn add(foo: plain_int, int); fn divide(bar: added_int, int); and this can't be reordered without triggering a compiler error.
- alain94040 1y agoThat example is too simple for me to grasp it. How would you code a function that iterates over an array to compute its sum. No cheating with a built-in sum function. If you had to code each addition, how would that work? Curious to learn (I probably could google this or ask Claude to write me the code).
- supergarfield 1y agoCarmack gives updating in a loop as the one exception: > You should strive to never reassign or update a variable outside of true iterative calculations in loops. If you want a completely immutable setup for this, you'd likely have to use a recursive function. This pattern is well supported and optimized in immutable languages like the ML family, but is not super practical in a standard imperative language. Something like def sum(l): if not l: return 0 return l[0] + sum(l[1:]) Of course this is also mostly insensitive to ordering guarantees (the compiler would be fine with the last line being `return l[-1] + sum(l[:-1])`), but immutability can remain useful in cases like this to ensure no concurrent mutation of a given object, for instance.
- bmacho 1y agoYou don't have to use recursion, that is, you don't need language support for it. Having first class (named) functions is enough. For example you can modify sum such that it doesn't depend on itself, but it depends on a function, which it will receive as argument (and it will be itself). Something like: def sum_(f, l): if not l: return 0 return l[0] + f(f, l[1:]) def runreq(f, *args): return f(f, *args) print(runreq(sum_, [1,2,3]))
- DemocracyFTW2 1y ago> You don't have to use recursion You're using recursion. `runreq()` calls `sum_()` which calls `sum()` in `return l[0] + f(f, l[1:])`, where `f` is `sum()`
- bmacho 1y ago
- zerd 1y agoIt's funny that converting the first example to the second is a common thing a compiler does, Static single assignment [0], to make various optimizations easier to reason about. [0] https://en.wikipedia.org/wiki/Static_single-assignment_form https://en.wikipedia.org/wiki/Static_single-assignment_form
- amy_petrik 1y agoGuys, guys, I don't think we're on the same page here. The conversation I'm trying to have is "stop mutating all the dynamic self-modifying code, it's jamming things up". The concept of non-mutating code, only mutating variables, strikes me as extremely OCD and overly bureaucratic. Baby steps. Eventually I'll transition from my dynamic recompilation self-modifying code to just regular code with modifying variables. Only then can we talk about higher level transcendental OOP things such as singleton factory model-view-controller-singleton-const-factories and facade messenger const variable type design patterns. Surely those people are well reasoned and not fanatics like me