←back to thread

498 points azhenley | 8 comments | | HN request time: 0.736s | source | bottom
Show context
EastLondonCoder ◴[] No.45770007[source]
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.

replies(17): >>45770035 #>>45770426 #>>45770485 #>>45770884 #>>45770924 #>>45771438 #>>45771558 #>>45771722 #>>45772048 #>>45772446 #>>45773479 #>>45775905 #>>45777189 #>>45779458 #>>45780612 #>>45780778 #>>45781186 #
rendaw ◴[] No.45770924[source]
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.

replies(10): >>45770972 #>>45771110 #>>45771163 #>>45771234 #>>45771937 #>>45772126 #>>45773250 #>>45776504 #>>45777296 #>>45778328 #
1. raincole ◴[] No.45773250[source]
Yet 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.

replies(3): >>45773392 #>>45773898 #>>45773931 #
2. suspended_state ◴[] No.45773392[source]
Or 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.
3. airstrike ◴[] No.45773898[source]
I 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.
4. wongarsu ◴[] No.45773931[source]
In 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

replies(4): >>45774362 #>>45775439 #>>45778129 #>>45778564 #
5. ymyms ◴[] No.45774362[source]
Another 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.

6. waffletower ◴[] No.45775439[source]
In a Clojure binding this is perfectly idiomatic, but symbolically shared bindings are not shadowed, they are immutably replaced. Mutability is certainly available, but is explicit. And the type dynamism of Clojure is a breath of fresh air for many applications despite the evangelism of junior developers steeped in laboratory Haskell projects at university. That being said, I have a Clojure project where dynamic typing is throughly exploited at a high level, allows for flexible use of Clojure's rational math mixed with floating point (or one or the other entirely), and for optimization deeper within the architecture a Rust implementation via JVM JNI is utilized for native performance, assuring homogenous unboxed types are computed to make the overall computation tractable. Have your cake and eat it too. Types have their virtues, but not without their excesses.
7. combyn8tor ◴[] No.45778129[source]
I did this accidentally the other day in Rust:

let x = some_function();

... A bunch of code

let x = some_function();

The values of x are the same. It was just an oversight on my part but wondered if I could set my linter to highlight multiple uses of the same variable name in the same function. Does anyone have any suggestions?

8. nayuki ◴[] No.45778564[source]
In Rust, one way I use shadowing is to gather a bunch of examples into one function, but you can copy and paste any single example and it would work.

    fn do_demo() {
        let qr = QrCode::encode_text("foobar");
        print_qr(qr);
        
        let qr = QrCode::encode_text("1234", Ecc::LOW);
        print_qr(qr);
        
        let qr = QrCode::encode_text("the quick brown fox");
        print_qr(qr);
    }
In other languages that don't allow shadowing (e.g. C, Java), the first example would declare the variable and be syntactically correct to copy out, but the subsequent examples would cause a syntax error when copied out.