17

REBOL/Core Users GuideWhat is Red中,我了解到 Rebol 和 Red 都使用定义范围

从指南中,我知道它是静态作用域的一种形式,“变量的作用域是在定义其上下文时确定的”,也称为运行时词法作用域,是静态作用域的一种动态形式,取决于上下文定义.

我知道在 com-sci 中有两种形式的作用域:词法作用域(静态作用域)和动态作用域。这个定义范围让我感到困惑。

那么什么是定义范围呢?

4

3 回答 3

28

Rebol 实际上根本没有范围。

让我们看一下这段代码:

rebol []

a: 1

func-1: func [] [a]

inner: context [
    a: 2
    func-2: func [] [a]
    func-3: func [/local a] [a: 3 func-1]
]

因此,加载该代码后,如果 Rebol 具有词法作用域,您将看到以下内容:

>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 1]

那是因为func-1使用了a来自外部范围的 the,a使用的 byfunc-2来自内部范围,而func-3调用func-1仍然a从定义它的外部范围使用,而不管 in 是什么func-3

如果 Rebol 有动态范围,你会看到:

>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 3]

那是因为func-3重新定义a,然后调用func-1,它只使用最近的活动定义a

现在对于 Rebol,您会得到第一个结果。但是 Rebol 没有词法作用域。所以为什么?

Rebol 伪造它。这是它的工作原理。

在编译语言中,您有范围。当编译器遍历文件时,它会跟踪当前范围,然后当它看到成为当前范围的嵌套范围时。对于词法作用域,编译器保留对外部作用域的引用,然后通过指向外部作用域的链接查找当前作用域中未定义的单词,直到找到或没有找到该单词。动态范围的语言做类似的事情,但在运行时,向上调用堆栈。

Rebol 没有做任何事情。特别是它不是在运行时编译的,而是构建的。你认为的代码实际上是数据、字块、数字等。这些词是其中有一个称为“绑定”的指针的数据结构。

当第一次加载该脚本时,脚本中的所有单词都被添加到脚本的环境对象中(我们不恰当地将其称为“上下文”,尽管它不是)。在收集单词的同时,脚本数据发生了变化。在脚本的“上下文”中找到的任何单词都与“上下文”或“绑定”相关联。这些绑定意味着您只需点击该链接即可访问存储该单词值的对象。它真的很快。

然后,一旦完成,我们就开始运行脚本。然后我们到了这一点:func [] [a]. 这并不是真正的声明,而是对名为的函数的调用,该函数func接受一个规范块和一个代码块,并使用它们来构建一个函数。该函数也有自己的环境对象,但在函数的规范中声明了单词。在这种情况下,规范中没有单词,所以它是一个空对象。然后将代码块绑定到该对象。但是在这种情况下a,该对象中没有任何内容,因此对 没有做任何事情a它保留了它之前绑定时已经拥有的绑定

调用也是如此context [...]- 是的,这是对不恰当命名的函数的调用context,它通过调用make object!. 该context函数获取一个数据块,并搜索集合词(那些带有尾随冒号的东西,例如a:),然后构建一个包含这些词的对象,然后绑定该块中的所有词和所有嵌套块到对象中的单词,在这种情况下afunc-2func-3。这意味着该a代码块中的 ' 已更改其绑定,改为指向该对象。

func-2定义时,其代码块中的绑定不会a被覆盖。当func-3被定义时,它的a规范中有一个,所以a:它的绑定被覆盖了。

所有这一切的有趣之处在于根本没有任何作用域。那 firsta:ainfunc-1的代码体只绑定一次,所以它们保持它们的第一个绑定。a:in的inner代码块和ain的代码块func-2被绑定了两次,因此它们保留了第二次绑定。a:in的func-3代码被绑定了 3 次,所以它也保持最后一次绑定。这不是作用域,它只是绑定代码,然后再次绑定更小的代码,依此类推,直到完成。

每一轮绑定都由一个正在“定义”某些东西(实际上是构建它)的函数执行,然后当该代码运行并调用定义其他内容的其他函数时,这些函数对其一小部分代码执行另一轮绑定. 这就是我们称之为“定义范围”的原因;虽然它实际上不是作用域,但它是 Rebol 中作用域的目的,它与词汇作用域的行为非常接近,乍一看你无法区分。

当您意识到这些绑定是直接的并且您可以更改它们时,它真的变得不同了(有点,您可以创建具有相同名称和不同绑定的新单词)。那些定义函数调用的同一个函数,你可以自己调用:它被命名为bind. 有了bind你,你可以打破范围界定的错觉,让单词绑定到你可以访问的任何对象。你可以用 做绝妙的技巧bind,甚至可以制作自己的定义函数。这很有趣!

至于 Red,Red 是可编译的,但它还包括类似 Rebol 的解释器、绑定和所有好东西。当它使用解释器定义事物时,它也会定义范围。

这有助于使事情更清楚吗?

于 2014-02-23T06:06:53.590 回答
15

这是一个老问题,@BrianH 在这里的回答对力学非常透彻。但我想我会给一个稍微不同的焦点,作为一个“故事”。

在 Rebol 中,有一类称为words的类型。这些本质上是符号,因此它们的字符串内容被扫描并进入符号表。因此,while"FOO"将是一个字符串,并且将是字符串的另一种“风味” ,<FOO>称为标签... FOO,,,并且都是具有相同符号 ID 的单词的各种“风味”。 (分别是“word”、“lit-word”、“set-word”和“get-word”。)'FOOFOO::FOO

被折叠成一个符号使得一旦加载就无法修改单词的名称。与每个都有自己的数据并且是可变的字符串相比,它们被卡住了:

>> append "foo" "bar"
== "foobar"

>> append 'foo 'bar
** Script error: append does not allow word! for its series argument

不变性有一个优势,因为作为一个符号,可以快速将一个词与另一个词进行比较。但是还有另一个难题:单词的每个实例都可以有选择地在其中包含一个不可见的属性,称为绑定。该绑定让它“指向”一个键/值实体,称为可以读取或写入值 的上下文。

注意:与@BrianH 不同,我认为将此类绑定目标称为“上下文”并不是那么糟糕——至少我今天不这么认为。稍后再问我,如果有新的证据出现,我可能会改变主意。可以说它是一个类似对象的东西,但并不总是一个对象……例如,它可能是对堆栈上函数框架的引用。

谁将一个词带入系统,谁就能率先说出它与什么上下文相关联。很多时候都是加载,所以如果你说load "[foo: baz :bar]"并取回 3 字块[foo: baz :bar],它们将被绑定到“用户上下文”,并回退到“系统上下文”。

遵循绑定是一切工作的方式,每个单词的“风味”都有不同的作用。

>> print "word pointing to function runs it"
word pointing to function runs it

>> probe :print "get-word pointing to function gets it"
make native! [[
    "Outputs a value followed by a line break."
    value [any-type!] "The value to print"
]]
== "get-word pointing to function gets it"

注意:第二种情况没有打印该字符串。它探查了函数规范,然后字符串只是评估中的最后一件事,因此它对其进行了评估。

但是,一旦您掌握了包含单词的数据块,绑定就是任何人的游戏。只要上下文中包含一个单词的符号,您就可以将该单词重新定位到该上下文。 (还假设该块没有受到保护或锁定以防止修改......)

这个级联的重新绑定机会链是重点。由于 FUNC 是一个“函数生成器”,它接受一个规范和一个你给它的主体,它有能力获取主体的“原始物质”及其绑定并覆盖它决定的任何一个。也许很奇怪,但看看这个:

>> x: 10

>> foo: func [x] [
    print x
    x: 20
    print x
]

>> foo 304
304
20

>> print x
10

发生的事情是 FUNC 收到了两个块,一个代表参数列表,第二个代表正文。当它获得主体时,两个prints 都绑定到本机打印功能(在这种情况下 - 重要的是要指出,当您从控制台以外的地方获取材料时,它们可能各自绑定不同!)x绑定到持有值 10 的用户上下文(在这种情况下)。如果 FUNC 没有做任何事情来改变这种情况,事情将保持这种状态。

但是它将图片放在一起并决定,由于参数列表中有一个 x,它会查看主体并使用新的绑定覆盖带有 x 的符号 ID 的单词...本地到函数。这是它没有用x: 20. 如果您在规范 FUNC 中省略了 [x],则不会做任何事情,并且会被覆盖。

定义链中的每一部分在传递事物之前都有机会。因此定义范围

有趣的事实:因为如果你不为 FUNC 的规范提供参数,它不会重新绑定正文中的任何东西,这导致了“Rebol 中的一切都在全局范围内”的错误印象。但这根本不是真的,因为正如@BrianH 所说:“Rebol 实际上根本没有作用域(......)Rebol 伪造了它。” 事实上,这就是 FUNCTION(与 FUNC 相反)所做的——它在主体中寻找像x:这样的集合词,当它看到它们时将它们添加到本地框架并绑定到它们。效果看起来像本地范围,但同样,它不是!

如果想象这些带有不可见指针的符号被打乱,听起来有点像 Rube-Goldberg-esque,那是因为它是. 对我个人而言,了不起的事情是它完全有效……而且我看到人们用它来做特技,你不会凭直觉认为这样一个简单的技巧可以用来做。

举个例子:非常有用的 COLLECT 和 KEEP(Ren-C 版本):

collect: func [
    {Evaluates a block, storing values via KEEP function,
        and returns block of collected values.}
    body [block!] "Block to evaluate"
    /into {Insert into a buffer instead
             (returns position after insert)}
    output [any-series!] "The buffer series (modified)"
][
    unless output [output: make block! 16]
    eval func [keep <with> return] body func [
        value [<opt> any-value!] /only
    ][
        output: insert/:only output :value
        :value
    ]
    either into [output] [head output]
]

这个看起来不起眼的工具以以下风格扩展了语言(同样,Ren-C 版本......在 R3-Alpha 或 Rebol2 中替换foreachforfor-eachlength?length of

>> collect [
       keep 10
       for-each item [a [b c] [d e f]] [
           either all [
               block? item
               3 = length of item
           ][
               keep/only item
           ][
               keep item
           ]
       ] 
    ]
== [10 a b c [d e f]]

我上面提到的最能理解定义范围的技巧。FUNC 只会覆盖其参数列表中事物的绑定,而不会触及正文中的其他所有内容。所以发生的情况是,它将您传递给 COLLECT 的主体用作新函数的主体,并在其中覆盖 KEEP 的任何绑定。然后它将 KEEP 设置为一个函数,该函数在调用时将数据添加到聚合器。

在这里,我们通过 /ONLY 开关看到了 KEEP 函数在将块拼接到收集的输出中的多功能性(调用者只有在看到长度为 3 的项目时才选择不拼接)。但这只是表面问题。它只是一种非常强大的语言特性——用户事后添加的——源自如此少的代码,几乎令人恐惧。当然还有很多故事。

由于填写了定义范围的关键缺失链接,我在这里添加了一个答案,这个问题被称为“定义范围的返回”:

https://codereview.stackexchange.com/questions/109443/definitional-returns-solved-mostly

这就是为什么<with> return在规范中它与 KEEP 并列。它在那里是因为 COLLECT 试图告诉 FUNC 它想“使用它的服务”作为代码的绑定器和运行器。但身体已经由其他人在其他地方创作。因此,如果它有一个 RETURN,那么这个 RETURN 已经知道要返回到哪里。FUNC 只是为了“重新调整”keep 的范围,但保留任何回报而不是添加自己的回报。因此:

>> foo: func [x] [
     collect [
         if x = 10 [return "didn't collect"]
         keep x
         keep 20
     ]
]

>> foo 304
== [304 20]

>> foo 10
== "didn't collect"

正是<with> return这使得 COLLECT 能够足够聪明地知道在 FOO 的体内,它不希望返回反弹,因此它认为从参数只是 [keep] 的函数返回。

还有一点关于定义范围的“为什么”,而不是“什么”。:-)

于 2015-11-02T01:51:57.673 回答
0

我的理解是:

Rebol 是静态作用域的

但,

问题不是“Rebol 使用什么范围?”,而是“何时确定 Rebol 范围,何时编译 Rebol 程序?”。

Rebol 有静态作用域,但是动态编译。

我们习惯了有一个编译时间和一个运行时间。

Rebol 有多个编译时间。

Rebol 代码的编译取决于编译时存在的上下文。

Rebol 代码在不同的时间、不同的上下文中编译。这意味着 Rebol 函数可能在不同时间以不同方式编译。

于 2021-04-28T10:51:20.117 回答