【问题标题】:The Order of Variable and Function Definitions变量和函数定义的顺序
【发布时间】:2013-10-29 10:01:37
【问题描述】:

为什么会这样:

  1. 函数定义可以使用它之后定义的定义
  2. 而变量定义不能。

例如,

a) 以下代码 sn-p 错误:

; Must define function `f` before variable `a`.
#lang racket
(define a (f)) 
(define (f) 10)

b) 虽然下面的 sn-p 是正确的:

; Function `g` could be defined after function `f`.
#lang racket
(define (f) (g)) ; `g` is not defined yet
(define (g) 10)

c)

; Variable `a` could be defined after function `f`
#lang racket
(define (f) a) ; `a` is not defined yet
(define a 10)

【问题讨论】:

    标签: scheme lisp racket definition operator-precedence


    【解决方案1】:

    关于球拍你需要知道几件事:

    1. 在 Racket 中,每个文件(以 #lang 开头)都是一个模块,这与许多没有模块的(传统,r5rs)方案不同。

    2. 模块的作用域规则类似于函数的规则,因此在某种意义上,这些定义类似于函数中的定义。

    3. Racket 从左到右评估定义。在方案行话中,您说 Racket 的定义具有 letrec* 语义;这与一些使用letrec 语义的方案不同,在这些方案中,相互递归定义永远不起作用。

    所以底线是定义都是在模块的环境中创建的(类似在函数中,对于函数局部定义),然后它们从左到右初始化。因此,反向引用总是有效的,所以你总是可以做类似的事情

    (define a 1)
    (define b (add1 a))
    

    它们是在单个范围内创建的——因此理论上,前向定义在它们在范围内的意义上是有效的。但实际上使用前向引用的值是行不通的,因为在评估实际值之前,您会得到一个特殊的 #<undefined> 值。要看到这一点,请尝试运行以下代码:

    #lang racket
    (define (foo)
      (define a a)
      a)
    (foo)
    

    模块的顶层受到进一步限制,因此此类引用实际上是错误,您可以通过以下方式看到:

    #lang racket
    (define a a)
    

    考虑到所有这些,对于函数内部的引用,事情会更加宽容。问题是函数的主体在函数被调用之前不会执行——因此,如果在函数内部发生前向引用,则在调用函数时它是有效的(= 不会出错或#<undefined>)在所有绑定都已初始化之后。这适用于普通函数定义

    (define foo (lambda () a))
    

    使用常用语法糖的定义

    (define (foo) a)
    

    甚至是最终扩展成函数的其他形式

    (define foo (delay a))
    

    通过所有这些,您不会在相同的规则下遇到任何错误——当函数体的所有使用都发生在定义初始化之后。

    但是,一个重要的注意事项是,您不应该将这种初始化与赋值混淆。这意味着像

    (define x (+ x 1))
    

    等同于主流语言中的x = x+1。它们更像是某种var x = x+1 的语言,它会因一些“对未初始化变量的引用”错误而失败。这是因为define 在当前范围内创建了一个 new 绑定,它不会“修改”现有的绑定。

    【讨论】:

    • 在通常的 Scheme 的 letrec 下,相互递归定义如何“从不工作”? “从不”是什么意思?
    • 对于“从不工作”——这些是价值用途,请参阅 Racket 的 R5RS 模式;是的,从左到右通常意味着从上到下。
    • 所以您的回答只是对 Racket 如何实现我笼统描述的内容的细节的过度技术性描述。
    • 问题是关于 Racket 的——我更准确地解释了范围和参考如何在 Racket 中工作。我不知道什么是“技术性过强”,但无论精确度如何,您都需要注意告诉人们define 就像:= 可能会让他们感到困惑。事实上,摆脱(define x (add1 x)) 是具有命令意识的学生在学习球拍(和其他良好的函数式语言)时需要克服的一个重要里程碑的错觉。
    • 我引入了伪代码:=,将值创建时间与值分配(绑定)时间分开。这就是:= 通常会唤起的形象。我特别告诉评估 RHS 需要在那时已经定义所有值,这样就可以完全排除 x := x+1 问题(或者它是相同的 x 并且它是未定义的,或者它是一个新的、阴影 @987654344 @)。总之,我的回答没有大问题。
    【解决方案2】:

    以下是大概的一般Scheme描述,类比。

    定义一个函数

    (define (f) (g))
    

    有点像

    f := (lambda () (g))
    

    所以 lambda 表达式被求值,结果函数对象(通常是一个闭包)被存储在被定义的新变量 f 中。 函数f将被调用时,必须定义函数g

    同样,(define (h) a)h := (lambda () a) 类似,因此只有在调用函数h 时,才会检查对变量a 的引用,以查找其值。

    但是

    (define a (f))
    

    就像

    a := (f)
    

    即函数f 必须在没有参数的情况下调用,并且该调用的结果存储在定义的新变量a 中。所以这个函数必须在那个时候已经定义好了。

    文件中的每个定义都按顺序执行,一个接一个。每个定义都可以引用文件中定义的任何变量,包括它上面和下面的(据说它们都属于同一个范围,但是只允许使用上面定义的那些变量的

    (这里有一个歧义:假设您使用的是 内置 函数,例如 (define a (+ 1 2)),但后来也在文件中定义它,例如 @987654336 @. 是定义还是重新定义?第一种情况,这是Racket的选择,禁止在定义前使用。第二种情况,使用“全局”值计算a,然后函数被重新定义。一些方案可能会走这条路。感谢 Eli Barzilay 向我展示这个,感谢 Chris Jester-Young 的帮助。

    【讨论】:

    • 这个答案也有一些问题,一些类似于 Valentin 的答案(闭包无关,这里的问题是范围和评估)。两个值得注意的问题:您应该避免将 Racket(和其他方案)中的 define 与分配进行比较——它们非常不同。另一个明显的问题是定义的“顺序”顺​​序是不相关的(例如,一些方案也禁止反向引用),并且并不总是正确的(例如,当有语法定义时)。此外,Racket 的模块是一种“封闭世界”范围,以避免 REPL 式顺序评估的一些问题。
    • 你的意思是 forward 引用可以在某些方案中被禁止吗?重新闭包:我可以说“功能对象”,这无关紧要;我从来没有说过它们被关闭有任何相关性;我不得不使用 some 名称。重新赋值,功能对象被定义,环境中的绑定将其作为值。这就是我所说的:=;再次,无关紧要的细节; 时间是相关的。显然我使用的评估模型 这里发生了什么;它可能是整个文件第一次处理,所以(define a (f)) 有效;那么 OP 就不必问他们的问题了。
    • @EliBarzilay 并且 Q 中没有提到语法定义。
    • 是的,可以禁止前向引用,但后向引用更有趣(尝试在球拍中查看#lang r5rs);闭包并不重要,因为您可以有其他评估实现——一个非常著名的“替代模型”是没有闭包但范围仍然相同的“替代模型”;时间在一种奇怪的意义上是相关的,因为 Scheme 通常试图避免它;最后的问题是关于 scope 和标识符引用——所有这些都是语法!
    • 第二次,我从未说过闭包很重要。 -- 移动球门柱警报:首先具体提及 语法定义,然后再谈一般语法 -- 时机就是一切:"and each id is set immediately after the corresponding val-expr is evaluated"。 -- 如果禁止前向引用,则不可能进行模块化编程。 (?我们从未谈论过 REPL,至少我没有;如果某些 REPL 禁止转发 refs,则不在此问题的范围内)
    猜你喜欢
    • 2021-07-15
    • 1970-01-01
    • 1970-01-01
    • 2011-06-29
    • 1970-01-01
    • 2014-12-11
    • 2012-04-18
    • 1970-01-01
    相关资源
    最近更新 更多