I was taught, that data, by default, is immutable in F#.
When we reassign value to some variable, what really happens is that it rebinds the value of variable, but setting a new value is different thing.
Rebinding is called Shadowing whilst setting new value is impossible if we don't say explicitly, that value of the variable is mutable.
Can anyone explain to me this concept in a bit more details?
What is the difference between shadowing (rebinding):
let var = "new_value"
and setting a new value, as:
var <- "new_value"
Is this a moment, that during rebinding we create another object and we assign that object's address to the variable, whereas in the second example we change the value itself? I brought that from heap/stack concept.. but I may be wrong.
Thanks.
Shadowing is when you create a new binding that uses the same name as a previous binding. This "shadows" the original name, which hides it but doesn't change or replace it. Try this in FSI to see:
let foo = 42
let printFoo () =
printfn "%i" foo
printFoo() ;;
This will print:
42
val foo : int = 42
val printFoo : unit -> unit
val it : unit = ()
Then add:
// ... more code
let foo = 24
printfn "%i" foo // prints 24
printFoo ();;
This will print:
24
42
val foo : int = 24
val it : unit = ()
Note that it still prints 42 when you call printFoo()
- the function sees the original (unshadowed) binding, but the new print shows the new value.
Using <-
to mutate a value requires a mutable binding:
let mutable bar = 42
let printBar () =
printfn "%i" bar
printBar ();;
This, like above, prints 42. Note that you override the default immutable behavior here with the mutable keyword.
You then change the value within the mutable binding:
bar <- 24
printfn "%i" bar
printBar ();;
This will print 24 twice, since, unlike the shadowed version, the mutation changes the original binding. If you leave mutable
off in the original binding, you'll get an error when using <-
.
To add on Reed Copsey's excellent answer, if you're writing a loop where you change the value of an accumulator of some sort, you'd set the original value as mutable
. For example, you can do this.
let mutable acc = 0 // declaration
for i in 1..100 do
acc <- acc + i // assignment
This is more or less equivalent to the C# code :
var acc = 0;
for (int i = 1; i <= 100; i++)
{
acc = acc + i; // assignment
// Another way to write this:
// acc += i;
}
However, in Shadowing, as in this F# snippet:
let acc = 0 // declaration
for i in 1..100 do
let acc = acc + i // another declaration only within the loop
You're not actually doing anything useful!! The second declaration has scope only within the for
loop, and it doesn't change the value of the original acc
.
A rough C# equivalent would be this:
var acc = 0; // declaration
for (int i = 1; i <= 100; i++)
{
var acc2 = acc + i; // another declaration only within the loop
}
Note that using acc
(instead of acc2
) for the inside variable wouldn't compile in C#, as it doesn't have Shadowing in this context.
The use of shadowing is that, it prevents from using the original variant in a block of code where you don't want it. So there is one less variable to worry about.
Whenever I wonder what actually is happening I use tools like ILSpy
For example:
let f () =
let x = Dictionary<int, string> ()
let mutable x = ResizeArray<int> 16
x <- ResizeArray<int> 16
Using ILSpy to decompile it in C# code it becomes:
public static void f()
{
Dictionary<int, string> x = new Dictionary<int, string>();
List<int> x2 = new List<int>(16);
x2 = new List<int>(16);
}
Here it's more obvious what the difference between shadowing and setting.
Shadowing x
creates a new variable with name x2
.
Setting is normal assignment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With