【问题标题】:scheme functions that "remember" values with let/set使用 let/set “记住”值的方案函数
【发布时间】:2012-06-05 23:27:10
【问题描述】:

我是 Scheme 的新手,并试图了解出现在函数中的某些值如何在多种用途中持续存在。采取以下柜台:

(define count
   (let ((next 0))
     (lambda ()
       (let ((v next))
         (set! next (+ next 1))
         v))))

我无法弄清楚(也没有在任何地方找到解释),这就是为什么每次使用 countnext 都不会重置为 0。

【问题讨论】:

标签: scheme closures mutators lexical-scope


【解决方案1】:

这称为闭包。整个程序中只有一个版本的next

为了更清楚地说明这一点,请考虑以下程序:

(define next 0)

(define count
  (lambda ()
    (let ((v next))
      (set! next (+ next 1))
      v))))

现在很明显只有一个next

您编写的版本不同,因为您使用了let 来确保只有lambda 表达式可以看到next。但是仍然只有一个next。如果您将其更改为此,请改为:

(define count
  (lambda ()
    (let ((next 0))
      (let ((v next))
       (set! next (+ next 1))
       v))))

那么你每次都会创建一个新版本的next,因为next的声明lambda,这意味着每次lambda是调用。

【讨论】:

  • 嗨,山姆。我已经对闭包以及让 lambda 的重要性有点熟悉了。但是知道那个这个语法创建了一个闭包并不等同于理解为什么。我想我一定对评估顺序感到困惑:似乎 (let ((next 0))) 的赋值部分仅在第一次执行 count 时才被评估。我该如何解释?
  • 基本上是因为函数是lambda创建的,而不是define创建的。所以next的绑定发生在函数之外,因为它在lambda之外。
【解决方案2】:

我要为 Sam 的出色回答补充一件事:您的问题表明这种行为可能与“让”有关。它不是。这是一个没有“let”的例子,它做了类似的事情:

#lang racket

(define (make-counter-from counter)
  (lambda ()
    (set! counter (+ counter 1))
    counter))

(define count (make-counter-from 9))

(count)
(count)

道德(如果有的话):是的!突变令人困惑!

编辑:根据您在下面的评论,听起来您确实在寻找一些洞察力,以了解您可以将哪种心理模型用于具有突变的语言。

在具有局部变量变异的语言中,您不能使用将参数替换为值的简单“替换”模型。相反,对函数的每次调用都会创建一个新的“绑定”,以后可以对其进行更新(也称为“变异”)。

因此,在我上面的代码中,使用 9 调用“make-counter-from”会创建一个新绑定,该绑定将“counter”变量与值 9 相关联。然后附加/替换此绑定 - 用于“ counter” 函数体中的变量,包括 lambda 内部的变量。该函数的结果是一个 lambda(一个函数),它“关闭”了对这个新创建的绑定的两个引用。如果您愿意,可以将它们视为对堆分配对象的两个引用。这意味着对结果函数的每次调用都会导致对该对象/堆事物的两次访问。

【讨论】:

  • 谢谢,这是一个很有帮助的例子,尽管出于同样的原因它难倒了我(参见对 Sam 帖子的回复)。
  • 据我判断,您修改后的帖子完美地回答了我的问题。我向 Valentin 点了点头,因为他直接解决了我的第二个帖子。
【解决方案3】:

我不完全同意你的解释。你是对的,函数的定义只被评估一次,但函数本身每次被调用时都会被评估。

我不同意的一点是“...重写定义...”,因为该函数只定义一次(并且没有显式覆盖)。

我是这样想象的:由于scheme中所谓的变量词法绑定,scheme解释器在函数定义的求值过程中注意到函数定义的环境中定义了一个变量——变量“下一个”。因此,它不仅记住函数定义,还记住变量“next”的值(这意味着它存储了两件事——函数定义和封闭环境)。当第一次调用函数时,它的定义由存储环境中的方案解释器评估(其中变量“next”的值为 0,并且该值是递增的)。第二次调用该函数时,会发生完全相同的事情 - 在其封闭环境中评估相同的函数定义。然而,这一次,环境为变量“next”提供了值 1,函数调用的结果为 1。

简而言之:功能(定义)保持不变,只是评估环境发生了变化。

【讨论】:

    【解决方案4】:

    要直接回答您的问题,“next 不会在每次使用 count 时重置为 0因为您的代码

    (define count (let ((next 0))
                     (lambda ()
                        (let ((v next))
                           (set! next (+ next 1))
                           v))))
    

    等价于

    (define count #f)
    
    (set! count ( (lambda (next)              ; close over `next`
                     (lambda ()                  ; and then return lambda which _will_
                        (let ((v next))          ;   read from `next`,
                           (set! next (+ v 1))   ;   write new value to `next`,
                           v)))                  ;   and then return the previous value;
                  0 ))                        ; `next` is initially 0
    

    (这是 “let-over-lambda” 模式;甚至还有一本同名的 Lisp 书)。

    分配count 的值仅“计算”一次。这个值是一个闭包,指的是next 的绑定,它(绑定)在它的外部(闭包)。然后每次count 被“使用”,即调用它引用的过程,它(过程)引用那个绑定:首先它从它读取,然后它改变它的内容。但它不会将其重新设置为初始值;绑定只启动一次,作为其创建的一部分,这发生在创建闭包时。

    此绑定仅在此过程中可见。闭包是这个过程和持有这个绑定的环境框架的捆绑。这个闭包是在next词法范围 内评估(lambda () ...) 表达式的结果,(lambda (next) ...) 表达式。

    【讨论】:

      【解决方案5】:

      好吧,我有一些顿悟。我相信我的困惑与定义和过程之间的区别有关 (lambda):定义发生一次,而过程在每次运行时都会进行评估。在原始函数中,let 定义了一个将next 设置为零的过程。该定义发生了一次,但在过程中使用 set! 会重写定义就像追溯性一样。因此,每次使用 count 都会生成一个新版本的函数,其中 next递增。

      如果这完全离谱,请纠正我。

      【讨论】:

      • 关于绑定的一点是它们可以在 lambda 表达式之间共享​​>:(let ((fg (let ((i 0)) (list (lambda()(set! i (+ i 1))) (lambda() (set! i (+ i 2))))))) (let ((once (car fg)) (twice (cadr fg))) .... ))。现在在.... 主体内调用oncetwice 会增加相同的 计数器。
      【解决方案6】:

      这是一个完全相同的程序。

      (define count
        (local ((define next 0)          
                (define (inc)
                  (local ((define v next))
                          (begin 
                            (set! next (+ next 1))
                            v))))
          inc))
      
       (count)   0
       (count)   1
       (count)   2 .........
      

      也许您的 let/set 序列遵循相同的机制。 您实际调用的过程是 inc,而不是 count。 inc 过程每次都会递增next。一个等价的定义可以是

       (define next 0)
      
       (define (inc)
        (begin
          (set! next (+ next 1))
          (- next 1)))
      
       (define count inc)
      
      
       (count)  0
       (count)  1
       (count)  2......
      

      所以,我猜想在第一个程序中调用 count 就像运行整个第二个程序一样。我不知道。我也是新来的计划。谢谢,有用的帖子。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-01-19
        • 2012-11-21
        • 2015-11-17
        • 1970-01-01
        • 2011-12-23
        • 2018-10-07
        • 2013-05-07
        • 2015-02-03
        相关资源
        最近更新 更多