F#中的不可变值

4

我刚开始学习 F#,有一个基本问题。

这是代码:

let rec forLoop body times =
    if times <= 0 then
        ()
    else
        body()
        forLoop body (times - 1)

我不理解当你定义一个变量时,它是一个值且不可变的概念。在这里,为了循环,该值正在改变。那和C#中的变量有什么不同呢?

4个回答

5

它没有改变。您使用了递归。那个变量保持不变,但是它减去了一并传递给函数。在这种情况下,函数是相同的。

堆栈看起来像:

forLoop body 0
 |
 forLoop body 1
   |
   forLoop body 2

5

在C#中,所展示的代码不会被表示为for循环,而是递归的(类似于这样):

void ForLoop(int times, Action body)
{
  if (times <= 0)
  {
     return;
  }
  else
  {
     body();
     ForLoop(times - 1, body);
  }
}

正如你所看到的,times 的值在任何时候都没有被改变。


1
每个递归调用中的times实例都是内存中的不同对象。如果body()以任何方式使用times,它会捕获当前堆栈帧中的不可变值,这与后续递归调用中的值不同。
以下是一个C#和F#程序,展示了差异可能很重要的一种方式。
C#程序 - 打印一些随机数:
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#程序 - 总是打印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)

这些差异是由于在 C# 代码中传递给 QueueUserWorkItem 的 lambda 捕获了一个可变变量,而在 F# 版本中它捕获了一个不可变值。


嗯...“内存中”的故事不是实现细节吗?我认为用绑定和值来描述会更清晰,这似乎是定义可观察行为的内容。另外,更加追求严谨的一点,当提到times时,我不会使用“对象”这个术语,因为据我所见它从未被装箱。 - kvb
@kvb,是的,我只是试图提供一个可能有用的心理模型(这可能不是实现中实际的方式)。 - Brian

1

当您执行调用(任何调用)时,运行时会分配一个新的堆栈帧,并将被调用函数的参数和局部变量存储在新的堆栈帧中。当您执行递归调用时,分配的帧包含具有相同名称的变量,但这些变量存储在不同的堆栈帧中。

为了证明这一点,我将使用您示例的略微简化版本:

let rec forLoop n = 
  if times > 0 then 
    printf "current %d" n
    forLoop body (n - 1) 

现在,假设我们从程序的某个顶层函数或模块中调用forLoop 2。运行时为调用分配堆栈,并将参数值存储在表示forLoop调用的帧中:
+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

forLoop 函数打印 2 并继续运行。它执行对 forLoop 1 的递归调用,该调用分配了一个新的堆栈帧:

+----------------------+
| forLoop with n = 1   |
+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

由于1 > 0,程序再次进入then分支,打印1并再次对forLoop函数进行递归调用:

+----------------------+
| forLoop with n = 0   |
+----------------------+
| forLoop with n = 1   |
+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

在这一点上,forLoop函数返回而不进行任何其他调用,并且当程序从所有递归调用中返回时,栈帧逐个被移除。从图表中可以看出,我们创建了三个不同的变量,它们存储在不同的栈帧中(但它们都被命名为n)。
值得注意的是,F#编译器执行各种优化,例如尾调用,它可以用可变变量的使用来替换调用和新栈帧的分配(这更高效)。然而,这只是一种优化,在理解递归的心智模型时,你不需要担心这个问题。

所以,如果我理解正确,每次递归对应于不同的堆栈帧,在其中temp的值是不变的? - rsteckly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接