Неизменные значения в F # - PullRequest
4 голосов
/ 12 апреля 2010

Я только начинаю на F # и у меня есть основной вопрос.

Вот код:

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

Я не понимаю, как, когда вы определяете переменнуюэто ценность и неизменность.Здесь значение изменяется для того, чтобы выполнить цикл.Чем это отличается от переменной в C #?

Ответы [ 4 ]

5 голосов
/ 12 апреля 2010

Представленный код не будет представлен как цикл for в C #, он будет рекурсивным (примерно так):

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

Как видите, значение times не изменяется ни в одной точке.

5 голосов
/ 12 апреля 2010

это не меняется. Вы используете рекурсию. эта переменная остается неизменной, но она вычитается и передается функции. функция в этом случае одинакова.

стек будет выглядеть как

forLoop body 0
 |
 forLoop body 1
   |
   forLoop body 2
1 голос
/ 12 апреля 2010

Когда вы выполняете вызов (любой вызов), среда выполнения выделяет новый кадр стека и сохраняет параметры и локальные переменные вызываемой функции в новом кадре стека. Когда вы выполняете рекурсивный вызов, выделенные кадры содержат переменные с одинаковыми именами, но они хранятся в разных кадрах стека.

Чтобы продемонстрировать это, я буду использовать несколько упрощенную версию вашего примера:

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 # выполняет различные оптимизации, такие как tail-call , которые могут заменить вызов и выделение нового фрейма стека с использованием изменяемой переменной (которая более эффективная). Однако это всего лишь оптимизация, и вам не нужно об этом беспокоиться, если вы хотите понять ментальную модель рекурсии.

1 голос
/ 12 апреля 2010

Каждый экземпляр 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)

Различия возникают из-за того, что лямбда, переданная в QueueUserWorkItem в коде C #, захватывает переменную, а в версии F # - неизменяемое значение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...