Immutable values in F#
开发者_StackOverflow中文版I'm just getting started in F# and have a basic question.
Here's the code:
let rec forLoop body times =
if times <= 0 then
()
else
body()
forLoop body (times - 1)
I don't get the concept of how when you define a variable it is a value and immutable. Here, the value is changing in order to loop. How is that different from a variable in C#?
it is not changing. you use recursion. that variable remains unchanged, but it is subtracted one and passed to function. function is same in this case.
stack will look like
forLoop body 0
|
forLoop body 1
|
forLoop body 2
The code presented would not be represented as a for loop in C#, it would be recursive (something like this):
void ForLoop(int times, Action body)
{
if (times <= 0)
{
return;
}
else
{
body();
ForLoop(times - 1, body);
}
}
As you can see, the value times
is not changed at any point.
When you perform a call (any call), the runtime allocates a new stack frame and stores parameters and local variables of the called function in the new stack frame. When you perform a recursive call, the allocated frames contain variables with the same names, but these are stored in different stack frames.
To demonstrate this, I'll use a slightly simplified version of your example:
let rec forLoop n =
if times > 0 then
printf "current %d" n
forLoop body (n - 1)
Now, let's say that we call forLoop 2
from some top-level function or module of a program. The runtime allocates the stack for the call and stores the value of the parameter in the frame representing the forLoop
call:
+----------------------+
| forLoop with n = 2 |
+----------------------+
| program |
+----------------------+
The forLoop
function prints 2
and continues running. It performs a recursive call to forLoop 1
, which allocates a new stack frame:
+----------------------+
| forLoop with n = 1 |
+----------------------+
| forLoop with n = 2 |
+----------------------+
| program |
+----------------------+
Since 1 > 0
the program enters the then
branch once again, prints 1
and makes one more recursive call to the forLoop
function:
+----------------------+
| forLoop with n = 0 |
+----------------------+
| forLoop with n = 1 |
+----------------------+
| forLoop with n = 2 |
+----------------------+
| program |
+----------------------+
At this point, the forLoop
function returns without making any other calls and stack frames are removed one by one as the programs returns from all the recursive calls. As you can see from the diagrams, we created three different variables that were stored on different stack frames (but all of them were named n
).
It is also worth noting that the F# compiler performs various optimizations such as tail-call, which can replace a call and an allocation of a new stack frame with a use of mutable variable (which is more efficient). However, this is just an optimization and you don't need to worry about that if you want to understand the mental model of recursion.
Each instance of times
in each recursive call is a different object in memory. If body()
uses times
in any way, it captures the immutable value from the current stack frame, which is different from values in subsequent recursive calls.
Below are a C# and F# program that show one way the difference may be important.
C# program - prints some random number:
using System;
using System.Threading;
class Program
{
static void ForLoop(int n)
{
while (n >= 0)
{
if (n == 100)
{
ThreadPool.QueueUserWorkItem((_) => { Console.WriteLine(n); });
}
n--;
}
}
static void Main(string[] args)
{
ForLoop(200);
Thread.Sleep(2000);
}
}
F# program - always prints 100:
open System
open System.Threading
let rec forLoop times =
if times <= 0 then
()
else
if times = 100 then
ThreadPool.QueueUserWorkItem(fun _ ->
Console.WriteLine(times)) |> ignore
forLoop (times - 1)
forLoop 200
Thread.Sleep(2000)
The differences arise because the lambda passed to QueueUserWorkItem
in the C# code captures a mutable variable, whereas in the F# version it captures an immutable value.
精彩评论