7 ms·
Anyone else dislike the immutability fad? Mutability has its place, and simply hiding it behind abstractions tacked on to the language (vars, refs, agents, and
by sillysaurus3 9y ago
Anyone else dislike the immutability fad?
Mutability has its place, and simply hiding it behind abstractions tacked on to the language (vars, refs, agents, and atoms in Clojure's case) isn't a productive way to deal with it. It has a lot of benefits, but the downsides are significant. Sometimes I just want to create a map and mutate its structure, and the language saying "no" is constraining me in an unpleasant way.
It's the old debate of whether the programmer should be constrained by the language, or the language should serve the programmer. Maybe it's a matter of opinion.
Also, typed function parameters are painful. Declaring a parameter as a `f: () -> int` instead of `f` requires that you think about what the signature needs to be. What if you don't know? What if you want to change it? The cost goes up significantly, and I'm not sure the purported ability to sidestep a few bugs is worth it. If safety is an issue, good test coverage can be a sufficient substitute for types.
(The article is excellent, by the way.)
- striking 9y agoRE: Typed functions: Kotlin lives on top of a statically typed architecture, so I think you do have to make that function typed. Some languages get around this requirement by inferring types, but type inference itself can then become Turing complete... So it's not like there's a "healthy option". It's more that you're choosing the poison you prefer.
- adrianN 9y agoIf you have sufficiently good test coverage to replace a type system you have to change tests whenever you'd have to change the type. No costs saved. Thinking more before coding is beneficial in my experience.
- norswap 9y agoAgreed that immutability seems overrated right now. The pendulum will swing back in full force, eventually. But of course, the art is in putting mutability and immutability in the right places. There are no fast rules, but in general mutability of local variables is harmless, and you probably need some mutability at the top level as well (as per: "functional core, imperative shell"). Mutability in other places can still be useful though. I don't agree or really follow your comment on function parameters. It seems an argument against typing in general.
- panic 9y agoI think type systems derived from linear/affine/uniqueness typing will help a lot here. Mutability is fine -- it's sharing that's the problem.
- sillysaurus3 9y agoRe: function parameters, here's a concrete example. Clojure's `reduce` takes a function `f` as an argument: https://clojuredocs.org/clojure.core/reduce https://clojuredocs.org/clojure.core/reduce When you pass an empty list, `f` is called with no arguments. That way, `(reduce + [])` can return 0, whereas `(reduce * [])` can return 1. It's very easy to write such a function in Clojure, thanks to a lack of types. What would that look like in Kotlin? A function that can either take zero arguments or takes two arguments of types specified by the input list?
- Nullabillity 9y agoThe usual implementation of fold/reduce takes a separate seed argument (as does clojure, optionally!), which IMO is far more sensible than having the same function do two completely separate things depending on the number of arguments.
- deleted 9y ago[deleted]
- sillysaurus3 9y agoSure, let's roll with that. How would you write a function parameter that takes two arguments, whose type is determined by the seed argument? Also, with reduce, the return value of the function can determine what the type of the input arguments should be. For example if you pass in a `(fn (x y) (list x y))`, you end up with: > (reduce (fn (x y) (list x y)) (list "a" "b" "c" "d")) ("a" ("b" ("c" "d"))) Let's throw in a print statement to print out the `y` parameter: > (reduce (fn (x y) (print (str y)) (list x y)) (list "a" "b" "c" "d")) "d" ("c" "d") ("b" ("c" "d")) ("a" ("b" ("c" "d"))) So `y` starts out as a string, then a list of strings, then a list whose first element is a string and the second element is a list of strings, and so on. I'm not sure what the type of that function would even look like. And if there's no way to express something as simple as `reduce` without resorting to `f: (x: Any, y: Any) -> Any`, are we certain it's good design?
- nicktelford 9y agoImmutability is useful as a default, but languages should absolutely provide mutable alternatives for situations the programmer deems appropriate. The reason immutability has become popular so recently, and the reason why I say it's the best default assumption is the rise in multi-threaded programming. There are a growing number of strategies for dealing with mutability in a multi-threaded environment in a way that is both safe, and intuitive to reason about, the two that springs to mind are the rediscovery of actors (e.g. Akka), and go's goroutines. Mutability certainly has its place, but so does immutability. Previously, immutability has been largely ignored in favour of the convenience of mutablity. I wouldn't say immutability is being heavily favoured now; more that it's back where it belongs, as part of a binary choice.
- tdb7893 9y agoEven in single threaded programs immutability can make things easier to reason about. I've had to look at some old Java projects and the easiest time I've had reasoning about them was a project that used mostly immutable objects. Ever since then I've used immutable as the default for my Java objects and it's been better for me. Your mileage may vary obviously
- lostcolony 9y agoBut even actors benefit from immutability. Imagine if the message you received and matched on was not the message you processed (because it was mutated in between). You either want immutability at least for message handling, or messages to be full copies, and there are tradeoffs to either choice.
- nicktelford 9y agoMessages, of course, should be immutable for exactly those reasons. Actors encapsulate mutable state; they essentially serialize access to that state such that access is single-threaded.
- lostcolony 9y agoActors don't necessarily encapsulate mutable state; they may in fact encapsulate immutable state. An actor can be treated (as in Erlang) as essentially just a(n optionally) recursive function with a mailbox, where mutation can only occur across the function boundary (i.e., when you recurse, you pass in the new value). At this point the only benefit to allowing mutability is to allow you convenience things like for loops, and for performance reasons. From the perspective outside of the actor, the state behaves the same way whether it's mutable or immutable; you do not gain new behaviors by making it mutable (i.e., for sharing data or similar), you just make a more complex coding model (in that you're mixing mutable and immutable and have to know which is which).
- zeptomu 9y agoIf your data is immutable or not is significant for program analysis and correctness, so I would say the distinction makes absolutely sense. > Sometimes I just want to create a map and mutate its structure, and the language saying "no" is constraining me in an unpleasant way. Languages that provide immutable data structures have often 2 variants, an immutable and mutable. Why can't you just take the mutable in that case? I think it's more about making immutability or mutability explicit.
- pmontra 9y agoYou can have both mutability and immutability in the same language. Example from Ruby: $ irb 2.4.1 :001 > a = [1, 2, 3] => [1, 2, 3] 2.4.1 :002 > a.map {|x| x + 1} => [2, 3, 4] 2.4.1 :003 > a => [1, 2, 3] 2.4.1 :004 > a.map! {|x| x + 1} => [2, 3, 4] 2.4.1 :005 > a => [2, 3, 4] 2.4.1 :006 > a.freeze # make a immutable => [2, 3, 4] 2.4.1 :007 > a.map! {|x| x + 1} RuntimeError: can't modify frozen Array Ruby is fundamentally a language based on mutability, but it shows that it should be possible to have both ways. Questions: 1) Is really there a language which is fully agnostic about mutability and immutability? 2) If not, it's maybe because designers have strong opinions about this matter? Actually IMHO designing a fully agnostic language would demonstrate strong opinions too.
- moosingin3space 9y agoI'd argue most languages are agnostic about (im)mutability. It's libraries that aren't.
- pmontra 9y agoMost but some don't. This fails in Erlang X = X + 1. because even variables are immutable. This is ok in Elixir even if it kind of fakes it [1] x = x + 1 and it's perfectly ok in Ruby and most other languages. [1] http://blog.plataformatec.com.br/2016/01/comparing-elixir-and-erlang-variables/ http://blog.plataformatec.com.br/2016/01/comparing-elixir-an...
- typon 9y ago> It's the old debate of whether the programmer should be constrained by the language, or the language should serve the programmer. Maybe it's a matter of opinion. I like to think of it as the language saving the programmer from herself
- sporkenfang 9y ago> Also, typed function parameters are painful. Declaring a parameter as a `f: () -> int` instead of `f` requires that you think about what the signature needs to be. What if you don't know? What if you want to change it? This is actually my favorite thing about languages with strong type systems where type inference is less emphasized -- if you're looking at a function definition, even if you're not terribly familiar with the language, you know what the thing is supposed to do. Sure, it's nice to let the compiler infer your types at compile or runtime, but it's really nice to just be able to read the code and be able to see what a function is at least meant to do, based on what it returns.
- mpweiher 9y ago...and in fact this documentation effect is (so far) the only positive effect of static typing that has had at least some scientific validation.
- reitanqild 9y ago? This sounds really fishy to me. Do you have more details? Source: have programmed professionally in major static and non-static languages and as the years pass I appreciate static languages more and more for each time I waste time debugging what I have learned to think of as stupid, completely avoidable errors.
- reitanqild 9y agoDeclaring a parameter as a `f: () -> int` instead of `f` requires that you think about what the signature needs to be. What if you don't know? (Java programmer here.) Then return a sufficiently "broad" type up to, if necessary, Object. That said I almost never fall that far back, I'd see that as a sign that I should stop and think. Oh, and the advantage of a typed languages like Java: you can have extremely good tool support. Netbeans (or IntelliJ or Eclipse) will happily help you to change types if it later turns out you were wrong.
- colanderman 9y ago> What if you want to change it? If you need to change the argument/return types of a function, you have a LOT more work to do than just changing a one-line signature.
- lomnakkus 9y ago> What if you want to change it? Then you change it and let the compiler/IDE help you find all the other bits of code you need to change. I don't see the problem.
- zintinio5 9y agoImmutability is absolutely fantastic as a default (especially for typical business logic). I'm not sure I want to go back to mutability except in constrained environments or time-sensitive software. Mutability should be harder to reach for because it forces you to think hard about the implications to other parts of your program. I can think of a number of times we've had to parallelize a routine or share resources between workers, and it has been almost trivial _because_ of immutability. In contrast to that, I inherited a hairball of C code using pthreads, mutexes, global variables, etc which had to be completely gutted and rewritten as it was impossible to understand the dependencies in the software (we were witnessing segfaults, deadlock, etc).
- kqr 9y ago> Anyone else dislike the immutability fad? [...] Sometimes I just want to create a map and mutate its structure, and the language saying "no" is constraining me in an unpleasant way. Immutability does not prevent you from incrementally creating new versions of a map! At this point, you are conflating no less than three different and orthogonal concepts: A) A convenient way to write code that incrementally creates new versions of a value. This is trivial in most languages using immutable values, but it often requires unfamiliar idioms, giving people the false impression it cannot be done. B) A performance optimisation that performs such local incremental updates in place. This is a real concern. Any immutable-by-default language should support a safe (!!) way for the programmer to ensure updates happen in place. C) A mechanism whereby new versions of values can be distributed to other parts of a program. This one is tricky as all hell to do safely, and most good solutions work just as well with immutable values. "But kqr, why did you take something so simple and made it so complicated?" It was complicated from the start. Traditional languages have just ignored this complexity and solved it in an "undefined behaviour" sort of way: let's just do wha the metal happens to do, however unsafe!" You'll note tha such languages have no way to do safe in place updates, and value propagation is never sychronised or otherwise guaranteed not to cause partial views. It's a low-level hack that's far outstayed its welcome in the age of huge systems with all sorts of interaction effects.
- mpweiher 9y ago> It was complicated from the start. No it wasn't. It's only "complicated" from a certain very specific and constrained POV, which you seem to have adopted wholesale and now assume as the only valid reality. Mutability is not just how "the metal" works, it's also how the rest of the world works. If I eat a banana, the contents of my stomach (and by extension I) change. You don't get a new me that's no longer hungry + the old me that hasn't changed. If I put gas in my car's tank, the car changes. I don't get a new car. Etc. You are also conflating immutability with atomic updates, as far as I can tell.
- inopinatus 9y agoYou are perceiving yourself as having mutated after eating the banana, merely because you are in temporal lockstep with your stomach. Most of what you've just stated as "fact" and "how the world works" is a matter of perception. Time, after all, is (we believe) simply another dimension of spacetime.
- emidln 9y ago> Mutability has its place, and simply hiding it behind abstractions tacked on to the language (vars, refs, agents, and atoms in Clojure's case) isn't a productive way to deal with it. In Clojure if you want mutability, you don't use an PersistentHashMap i.e. `{}` inside an atom `(atom {})`, you import and instantiate a mutable java class `java.util.concurrent.ConcurrentHashMap` or `java.util.HashMap` `(let [m (doto (java.util.HashMap.) (.put "foo" "bar") (.put "spam "eggs"))] .... )` and bang on it just like you would in Java. Clojure doesn't attempt to solve mutability because the host platform has already done a good job at that. Clojure provides semantics around state managment of persistent data if you need that sort of thing, but that's not necessarily a good replacement for mutability if you actually need a mutable thing.
- codecurve 9y agoFew points on immutability and Clojure. The abstraction that hides mutable data structures is transient[1], not vars, refs, agents and atoms. Those abstractions are all to do with mutable references, not mutable data. They're there to make sure that you have safe ways to coordinate state in concurrent environments. I agree that immutability can be annoying in languages that weren't designed with it in mind, but the languages that were almost always make it easier, safer and performant to create copies rather than mutate data. If you're writing Clojure or Haskell and you think you're going to save yourself time by "just creating a map and mutating its structure" then you're misunderstanding the purpose of the language. These constraints are what enable the guarantees and contracts those languages make. [1]: https://clojure.org/reference/transients https://clojure.org/reference/transients
- sillysaurus3 9y agoBut it does save time. JS is proof of that. There are no immutable data structures and the world rolls on. People will hate on JS, but it's effective. You can learn to get into the "immutability mindset" if you train yourself to, but are you certain it's worth the time investment? It seems like there's at least a chance that it's not.
- codecurve 9y agoSure, no arguments there. It does save time in JavaScript and a large part of that is because the language has been designed around mutability. Part of that trade-off is that JavaScript can't make the same guarantees about what happens when you pass an object into a function. It's harder to be confident that a given program is correct. Immutability is just a part of the "simple made easy"[1] ethos of Clojure and I think most Clojure programmers will argue that taking the time to understand that philosophy _is_ worth the investment. [1] https://www.youtube.com/watch?v=rI8tNMsozo0 https://www.youtube.com/watch?v=rI8tNMsozo0
- ruleabidinguser 9y agoAh, so you see why there are not many clojure programmers
- borplk 9y agoYou pay for not specifying those parameters and so on in other ways later on. By leaving those things out you are broadening the problem model and denying the compiler information that it needs from you as the programmer so that it can do a good job of solving the problem. You have to remember that the computer can't synthesise knowledge about the problem domain that you deny it in the first place. So sure tightening things down by specifying them may be tedious but it is the reality of the problem that you are solving. Convenience of leaving it out will translate to instability such as run-time exceptions not to mention a massively reduced ability in tooling support (like IDE auto-complete).
- dtech 9y ago> Also, typed function parameters are painful. Declaring a parameter as a `f: () -> int` instead of `f` requires that you think about what the signature needs to be. What if you don't know? Why are you writing the code if you don't know yet what your input will be? A function transforms input into output, how can it do that correctly when you don't know what its input is? If you don't know the input type yet, you should first be writing the things that will deliver the input, or leave the function as an unimplemented placeholder and define only the output type. > What if you want to change it This is one of the best reasons to use types. Directly after the change you'll be told by the compiler which other code needs changes. Compare this to un-typed languages where you'll need a lot of tests to ensure the same or risk runtime errors.