简体   繁体   中英

Immutable values in F#

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.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM