12 ms·
After a 2 year Clojure stint I find it very hard to explain the clarity that comes with immutability for programmers used to trigger effects with a mutation. I
by EastLondonCoder 11mo ago
After a 2 year Clojure stint I find it very hard to explain the clarity that comes with immutability for programmers used to trigger effects with a mutation.
I think it may be one of those things you have to see in order to understand.
- oldpersonintx2 11mo ago[dead]
- deleted 11mo ago[deleted]
- m_rpn 11mo agosalutes from a WestLondonCoder
- emil0r 11mo agoThe way I like to think about is that with immutable data as default and pure functions, you get to treat the pure functions as black boxes. You don't need to know what's going on inside, and the function doesn't need to know what's going on in the outside world. The data shape becomes the contract. As such, localized context, everywhere, is perhaps the best way to explain it from the point of view of a mutable world. At no point do you ever need to know about the state of the entire program, you just need to know the data and the function. I don't need the entire program up and running in order to test or debug this function. I just need the data that was sent in, which CANNOT be changed by any other part of the program.
- DrScientist 11mo agoSure modularity, encapsulation etc are great tools for making components understandable and maintainable. However, don't you still need to understand the entire program as ultimately that's what you are trying to build. And if the state of the entire programme doesn't change - then nothing has happened. ie there still has to be mutable state somewhere - so where is it moved to?
- maleldil 11mo ago> there still has to be mutable state somewhere - so where is it moved to? This is one way of thinking about it: https://news.ycombinator.com/item?id=45701901 https://news.ycombinator.com/item?id=45701901 (Simplify your code: Functional core, imperative shell)
- raddan 11mo agoIn functional programs, you very explicitly _do not_ need to understand an entire program. You just need to know that a function does a thing. When you're implementing a function-- sure, you need to know what it does. But you're defining it in such a way that the user should not know _how_ it works, only _what_ it does. This is a major distinction between programs written with mutable state and those written without. The latter is _much_ easier to think about. I often hear from programmers that "oh, functional programming must be hard." It's actually the opposite. Imperative programming is hard. I choose to be a functional programmer because I am dumb, and the language gives me superpowers.
- DrScientist 11mo agoI think you missed the point. I understand that if you writing a simple function with an expected interface/behaviour then that's all you need to understand. Note this isn't something unique to a functional approach. However, somebody needs to know how the entire program works - so my question was where does that application state live in a purely functional world of immumutables? Does it disappear into the call stack?
- MetaWhirledPeas 11mo agoIt didn't disappear; there's just less of it. Only the stateful things need to remain stateful. Everything else becomes single-use. Declaring something as a constant gives you license to only need to understand it once. You don't have to trace through the rest of the code finding out new ways it was reassigned. This frees up your mind to move on to the next thing.
- rendaw 11mo agoI 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 11mo ago[deleted]
- skeezyjefferson 11mo agowhats the difference between immutable and constant, which has been in use far longer? why are you calling it mutable?
- Thorrez 11mo 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 11mo agoThey aren't the same for object references. The reference can't be changed, but the properties can.
- Thorrez 11mo 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).
- zelphirkalt 11mo agoMade a similar experience with Scheme. I could tell people whatever I wanted, they wouldn't really realize how much cleaner and easier to test things could be, if we just used functions instead of mutating things around. And since I was the only one who had done projects in an FP language, and they only used non-FP languages like Java, Python, JavaScript and TypeScript before, they would continue to write things based on needless mutation. The issue was also, that using Python it can be hard to write functional style code in a readable way too. Even JS seems to lend itself better to that. What's more is, that one will probably find oneself hard pressed to find the functional data structures one might want to use and needs to work around recursion due to the limitations of those languages. I think it's simply the difference between the curious mind, who explores stuff like Clojure off the job (or is very lucky to get a Clojure job) and the 9 to 5 worker, who doesn't know any better and has never experienced writing a FP codebase.
- SoftTalker 11mo agoJS is much more of a functional language than it was given credit for a long time. It had first-class functions and closures from day one if I'm not mistaken.
- inopinatus 11mo agoI’m fond of saying “JS is a Lisp”. It’s not a hill I’d bother dying on, however.
- DemocracyFTW2 11mo agoI'm really afraid that the weak point of the argument is really Scheme having a Lisp syntax. One might say syntax is the most superficial thing about a language but as a matter of fact it's the mud pool in front of the property where everybody's wheels get stuck and they feel their only option is to go into reverse and maybe try another day, or never. The same happens with APL; sure it's a genius who invented it and tic-tac-toe in a single short line of code is cool—doesn't mean many people get over the syntax. FWIW I believe that JS for one would greatly benefit from much better support for immutable data, including time- and space-efficient ways to produce modified copies of structured data (like you don't think twice when you do `string.replace(...)` where you do in fact produce a copy; `list.push(...)` could conceivable operate similarly).
- ndr 11mo agoClojure also makes it very easy, it'd require too much discipline to do such a thing in Python. Even Carmack, who I think still does python mostly by himself instead of a team, is having issues there.
- MetaWhirledPeas 11mo ago> it'd require too much discipline to do such a thing in Python Is Python that different from JavaScript? Because it's easy in JavaScript. Just stop typing var and let, and start typing const. When that causes a problem, figure out how to deal with it. If all else fails: "Dear AI, how can I do this thing while continuing to use const? I can't figure it out."
- codethief 11mo agoI agree that Python is not too different and in general I treat my Python variables as const. One thing, however, where I resort to mutating variables more often than I'd like is when building lists & dictionaries. Lambdas in Python have horrible DX (no multi-line, no type annotations, bad type checker support even in obvious cases), which is why the functional approach to build your list, using map() and filter() is much more cumbersome than in JS. As a result, whenever a list comprehension becomes too long, you end up building your list the old-fashioned way, using a for loop and the_list.append().
- ndr 11mo agoIn python there's no let, var nor const. So yes.
- filoeleven 11mo agoJavascript only enforces reassignments to const. So this, const arr = [] arr.push(“grape nuts”] is just peachy in JS and requires the programmer to avoid using it. More importantly, because working immutably in JS is not enforced, trying to use it consistently either limits which libraries you can use and/or requires you to wrap them to isolate their side effects. ImmerJS can help a lot here, since immutability is its whole jam. I’d rather work in a language where I get these basic benefits by default, though.
- ErroneousBosh 11mo agoI guess I'm not that good a programmer, because I don't really understand why variables that can't be varied are useful, or why you'd use that. How do you write code that actually works?
- mleo 11mo agoIt forces you to consider when, where and why a change occurs and can help reason later about changes. Thread safety is a big plus.
- ErroneousBosh 11mo agoOkay, so for example I might set something like "this bunch of parameters" immutable, but "this 16kB or so of floats" are just ordinary variables which change all the time? Or then would the block of floats be "immutable but not from this bit"? So the code that processes a block of samples can write to it, the code that fills the sample buffer can write to it, but nothing else should?
- stickfigure 11mo agoSounds like you have a data structure like `Array<Float>`. The immutable approach has methods on Array like: Array<Float> append(Float value); Array<Float> replace(int index, Float value); The methods don't mutate the array, they return a new array with the change. The trick is: How do you make this fast without copying a whole array? Clojure includes a variety of collection classes that "magically" make these operations fast, for a variety of data types (lists, sets, maps, queues, etc). Also on the JVM there's Vavr; if you dig around you might find equivalents for other platforms. No it won't be quite as fast as mutating a raw buffer, but it's usually plenty fast enough and you can always special-case performance sensitive spots. Even if you never write a line of production Clojure, it's worth experimenting with just to get into the mindset. I don't use it, but I apply the principles I learned from Clojure in all the other languages I do use.
- ErroneousBosh 11mo ago
- rafaelmn 11mo agoI would say it's more than immutability - it's the "feel" of working with values. I've worked with at least 6 languages professionally and likely more for personal projects over last 20 years. I can say that Clojure was the most impactful language I learned. I tried to learn Haskel before but I just got bogged down in the type system and formalization - that never sat with me (ironically in retrospect Monads are a trivial concept that they obfuscated in the community to oblivion, yet another Monad tutorial was a meme at the time). I used F# as well but it is too multi paradigm and pragmatic, I literally wrote C# in F# syntax when I hit a wall and I didn't learn as much about FP when I played with it. Clojure had the lisp weirdness to get over, but it's homoiconicty combined with the powerful semantics of core data structures - it was the first time where the concept of working with values vs objects 'clicked' for me. I would still never use it professionally, but I would recommend it to everyone who does not have a background in FP and/or lisp experience.
- MarkMarine 11mo agoI have dreams of being at a “Clojure shop” but I fear daily professional use might dull my love for the language. Having to realize that not everyone on my team wants to learn lisp (or FP) just to work with my code (something I find amazing and would love to be paid to do) was hard. On a positive note I have taken those lessons from clojure (using values, just use maps, Rich’s simplicity, functional programming without excessive type system abstraction, etc) and applied them to the rest of my programming when I can and I think it makes my code much better.
- StopDisinfo910 11mo agoI think the advantage is often oversold and people often miss how things actually exist on a continuum and just plainly opposing mutable and immutable is sidestepping a lot of complexity. For exemple, it's endlessly amusing to me to see all the efforts the Haskell community does to basically reinvent mutability in a way which is somehow palatable to their type system. Sometimes they even fail to even realise that it's what they are doing. In the end, the goal is always the same: better control and warranties about the impact of side effects with minimum fuss. Carmack approach here is sensible. You want practices which make things easy to debug and reason about while mainting flexibility where it makes sense like iterative calculations.
- eyelidlessness 11mo ago> Sometimes they even fail to even realise that it's what they are doing. Because that’s not what they’re doing. They’re isolating state in a systemic, predictable way.
- StopDisinfo910 11mo agoLenses is mutation by another name. You are basically recreating states on top of an immutable system. Sure, it's all immutable actually but conceptually it doesn't really change anything. That's what makes it hilarious. In the end, the world is stateful and even the purest abstractions have to hit the road at some point. But the authors of Haskell were fully aware of that. The monadic type system was conceived as a way to easily track side effects after all, not banish them.
- eyelidlessness 11mo agoBut there isn’t anything hilarious about that. It’s a clear-minded and deliberate approach to reconciling principle with pragmatic utility. We can debate whether it’s the best approach, but it isn’t like… logically inconsistent, surprising, or lacking in self awareness.
- 11mo ago
- dwwoelfel 11mo agoCarmack is talking about variable reassignment here, which Clojure will happily let you mutate. For example: (let [result {:a 1} result (assoc result :b 2)] ...) He mentions that C and C++ allow const variables, but Clojure doesn't support that. clj-kondo has a :shadowed-var rule, but it will only find cases where you shadow a top-level var (not the case in my example).
- didibus 11mo agoIt's more nuanced, because the shadowing is block-local, so when the lexical scope exits the prior bindings are restored. I think in practice this is the ideal middle ground of convenience (putting version numbers at the end of variables being annoying), but retaining mostly sane semantics and reuse of prior intermediate results.
- deleted 11mo ago[deleted]
- manoDev 11mo agoThat's not mutation though. The `assoc` on the second binding is returning a new object; you're just shadowing the previous binding name. This is different than mutation, because if you were to introduce an intermediate binding here, or break this into two `let`s, you could be holding references to both objects {:a 1} and {:a 1 :b 2} at any time in a consistent way - including in a future/promise dereferenced later.
- deleted 11mo ago[deleted]
- potetm 11mo agoregardless of the mechanism, you still run into the exact same problem John had.
- ratelimitsteve 11mo agothe flash of enlightment I had when I understood the incredible power the rules of functional programming give you as a coder is probably the biggest one I've had in my career so far. idempotence, immutability and statelessness on their own let you build a thing once in a disciplined way and then use it all willy nilly anywhere you want without having to think about anything other than "things go into process, other things come out" and it's so nice.
- m463 11mo agoI think a lot of this kind of stuff should have language support (like he mentions), even if it is not that functional and is just as a hint. That said, utopias are not always a great idea. Making all your code functional might be philosophically satisfying, but sometimes there are good reasons to break the rules.
- deleted 11mo ago[deleted]
- runeks 11mo agoOne big problem with mutation is that it makes it too easy to violate many good design principles, e.g. modularity, encapsulation and separation of concerns. Because any piece of code that holds a reference to a mutable variable is able to, at a distance, modify the behavior of a piece of code that uses this mutable variable. Conversely, a piece of code that only uses immutable variables, and takes as argument the values that may need to vary between executions, is isolated against having its behavior changed at a distance at any time.
- nvarsj 11mo ago> I think it may be one of those things you have to see in order to understand. Or the person doesn't understand, then declares the language to be too difficult to use. This probably happens more than the former, sadly. ex. I've heard people argue for rewriting perfectly working Erlang services in C++ or Java, because they find Erlang "too difficult". Despite it being a simpler language than either of those.
- bjoli 11mo agoI spent some time discussing in another thread discussing why the foreach loop is so bad in many languages. Most of the bugs I write come from me managing state, yet if I want to do much more than going start to end of a collection I have to either use methods that are slower than a proper loop or I have to manage all the state myself. In common lisp you have the loop macro (or better: iterate), in racket you have the for loops. I wrote a thing for guile scheme [0]. Other than that I dont know if many nice looping facilities. In many languages you can achieve all that with conbinatoes and what not, but always at the cost of performance. I think this is an opportunity for languages to become safer and easier to use without changing performance. 0:https://rikspucko.koketteriet.se/bjoli/goof-loop https://rikspucko.koketteriet.se/bjoli/goof-loop