27 comments

[ 3.4 ms ] story [ 52.2 ms ] thread
I feel the author isn't going far enough- make all variables immutable by default unless explicitly declared otherwise, and even then highly discouraged.

Where mutable variables are needed, I prefer to hide that logic inside a method that exists solely to do that dirty business on an abstract level, not specific to my business logic. That let's me test the 'dirty' mutable method in an isolated place and removes a whole class of mutability-related bugs from my business logic code.

Working on Java, this used to be nigh-impossible, but the Java 8 additions have truly made my life easier.

I agree up to a point.

Firstly, "Immutable by default" is recognised as a good design feature now for modern languages. But there is plenty of code in existing languages like Java, JavaScript, C, C# etc. which does not follow this rule.

Secondly, the "principle of least surprise" suggests that function arguments should work like local vars in that language. So if local vars are immutable by default, then arguments should be as well. Or if local vars can be optionally declared as immutable, then function arguments should be as well.

I can't really say what's the best for every/any language. It's not one size fit all. There are always design tradeoffs and differing schools of thought. But these days it looks a lot like "immutable by default, unless there's a good reason otherwise" is a good starting point.

I, the author, am tending to agree now. :)
Agreed. Erlang is another language with single assignment, i.e. every variable assignment target must have a new name. It's incredibly easy to see what calculations and conditions affect the final outcome.
How do you do a for loop in Erlang or something that sums up a value ?
(comment deleted)
You don't usually write for loops in Erlang, as a functional language loops are better expressed as recursive functions.

Here is an example of using a recursive function in Erlang to sum up a factorial.

  fac(1) ->
      1;
  fac(N) ->
      N * fac(N - 1).
Does Erlang handle the stack better? In most languages this would blow up if N got big enough.
Historical anecdote: Algol-68 disassociated variable assignment from mutability already 50 years ago. Whenever mutability was desired, a reference to a mutable value holder was used.

Example of immutable assignment: int n = 2

Example of reference to mutable value: ref int n = (heap int := 2)

However, in order to make Algol-68 code look more similar to Algol-60, they introduced backwards-compatible shorthands, so for example the declaration int n; was actually a shorthand for ref int n = (loc int);

This shorthand magic probably contributed to why Algol-68 syntax was perceived to be difficult.

Footnote: loc and heap were the two memory allocation operators available in Algol-68.

Rust takes a similar approach with let some_var and let mut some_var, respectively.
I think immutability of arguments should be optional by using a const keyword, like in c:

  int add(const int a, const int b);
The downside to this syntactic approach is that the implementation details are leaked in the signature.
I think it would better if they were immutable by default and instead of making them const, you would mark them mutable or reference.
> If we look to a language like Haskell we see that reassigning variables, in general, is frowned upon (is it even possible?)

This is perfectly valid Haskell:

    main = do
      let a = 7
      print a
      let a = 9
      print a
but you'll get a warning for it: tmp.hs:4:7: warning: This binding for ‘a’ shadows the existing binding bound at tmp.hs:2:7
The warning says it right away, the first a is shadowed, not reassigned, here.

The difference is that when a is reassigned the 7 is lost, while it should be still around when shadowed. I don‘t know of a way to access the 7 in this example however.

If there’s no way to access the 7 after it’s shadowed, why couldn’t an optimized garbage collector throw it away after it’s shadowed, thus making it equivalent to reassignment?
Why would they allow this? Is there any legitimate case where doing this makes sense? Seems very risky to me. In most languages I know you would expect that the "a" variables are the same.
In the past, I've used it in order to ensure the variable has a legitimate value. So, something like this (pseudo code):

    fn foo(a):
      a = a |> trim |> (default "chocolate")
      mix a "milk"

Well, in this contrived example, I wouldn't reassign a, just tack on `|> (mix "milk")` but in a complex example where you're reusing `a` in several places, it can make sense and is nicer than having to declare a bunch of variables just to hold the sanitized values of arguments.
Looks like you are using it as a reassignable function argument. Makes sense.
Members of the E family permit `var x` instead of `x` for reassignable arguments. Having immutable names by default with a keyword to make a mutable name has worked well.
I'm having a hard time understanding why they should be treated any differently than normal variables. I suppose the way that I view parameters is as regular variables who are initialized to values provided by the caller. Is there any argument against default mutability which still makes sense when viewing parameters from this perspective?
The difference between a local variable and a function argument is that altering the argument has an effect outside the function, and that the effect is invisible in that calling the function doesn't tell you that it will change the argument's value. It is a minor sin in the case of e.g. massaging data to turn NaNs into 0s or provide default values. It is also fine in the case of performance-critical code: c string manipulation functions might move a pointer along the string's length. However it is much easier to program against a function which has a single predictable result, ideally the generation of a return value based on input. If you mess with a function's input, you had better a) document that fact and b) have a reasonable expectation that your consuming dev is going to read the documentation. C programmers have this one tiny luxury; authors of an npm package do not.
Ok sorry, I didn't make explicit an important bit -- from my perspective these variables are initialized by the callee but are otherwise local to the function. From a C perspective imagine them as function local variables which are contructed by the callee's parameters.
You can't generalize across all languages that modifying an argument modifies something visible outside the function. It is true in languages that have pass by reference (C++ with &, Pascal, others) and it isn't true in languages which have pass by value (C, Java).

And yes, passing a reference or pointer by value enables you to have effects that are visible outside the function, but if you copied the reference or pointer value to a local variable, you still could have effects visible outside the function, so that isn't a question of how arguments should be treated; it's a general question of how functions should behave.