A colleague reviewed my code and asked why I was using the following pattern:
type MyType() =
member this.TestMethod() =
let a = 1
// do some work
let a = 2 // another value of a from the work result
()
I told him that that was a bad pattern and my intent was to use let mutable a = ...
. But then he asked why at all it is possible in F# class to have such sequential bindings, while it is not possible in a module or an .fsx
file? In effect, we are mutating an immutable value!
I answered that in a module or an .fsx file a let binding becomes a static method, therefore it is straightforward that the bindings with the same name will conflict in the same way as two class properties with the same name do. But I have no idea why this is possible inside a method!
In C#, I have found useful to scope variables, especially in unit tests when I want to make up some different values for test cases by just copy-pasting pieces of code:
{
var a = 1;
Assert.IsTrue(a < 2);
}
{
var a = 42;
Assert.AreEqual(42, a);
}
In F#, we could not only repeat the same let bindings, but change an immutable one to a mutable one and then mutate it later the usual way:
type MyType() =
member this.TestMethod() =
let a = 1
// do some work
let a = 2 // another value of a from the work result
let mutable a = 3
a <- 4
()
Why we are allowed to repeat let bindings in F# methods? How should I explain this to a person who is new to F# and asked "What is an immutable variable and why I am mutating it?"
Personally, I am interested in what design choices and trade offs were made to allow this? I am comfortable with the cases when the different scopes are easily detectable, e.g. when a variable is in constructor and then we redefine it in the body, or when we define a new binding inside a for
/while
loops. But two consecutive bindings at the same level are somewhat counter-intuitive. It feels like I should mentally add in
to the end of each line as in the verbose syntax to explain the scopes, so that those virtual in
s are similar to C#'s {}
I think it is important to explain that there is a different thing going on in F# than in C#. Variable shadowing is not replacing a symbol - it is simply defining a new symbol that happens to have the same name as an existing symbol, which makes it impossible to access the old one.
When I explain this to people, I usually use an example like this - let's say we have a piece of code that does some calculation using mutation in C#:
var message = "Hello";
message = message + " world";
message = message + "!";
This is nice because we can gradually build the message. Now, how can we do this without mutation? The trick is to define new variable at each step:
let message1 = "Hello";
let message2 = message1 + " world";
let message3 = message2 + "!";
This works - but we do not really need the temporary states that we defined during the construction process. So, in F# you can use variable shadowing to hide the states you no longer care about:
let message = "Hello";
let message = message + " world";
let message = message + "!";
Now, this means exactly the same thing - and you can nicely show this to people using Visual F# Power Tools, which highlight all occurrences of a symbol - so you'll see that the symbols are different (even though they have the same name).
The F# code:
let a = 1
let a = 2
let a = 3
a + 1
is just a condensed (aka "light") version of this:
let a = 1 in
let a = 2 in
let a = 3 in
a + 1
The (sort of) C# equivalent would be something like this:
var a = 1;
{
var a = 2;
{
var a = 3;
return a + 1;
}
}
In the context of having nested scopes, C# doesn't allow shadowing of names, but F# and almost all other languages do.
In fact, according to the font of all knowledge C# is unusual in being one of the few languages that explicitly disallow shadowing in this situation.
This might be because C# is a relatively new language. OTOH F# copies much of its design from OCaml, which in turn is based on older languages, so in some sense, the design of F# is "older" than C#.
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