从REBOL/Core Users Guide和What is Red中,我了解到 Rebol 和 Red 都使用定义范围。
从指南中,我知道它是静态作用域的一种形式,“变量的作用域是在定义其上下文时确定的”,也称为运行时词法作用域,是静态作用域的一种动态形式,取决于上下文定义.
我知道在 com-sci 中有两种形式的作用域:词法作用域(静态作用域)和动态作用域。这个定义范围让我感到困惑。
那么什么是定义范围呢?
从REBOL/Core Users Guide和What is Red中,我了解到 Rebol 和 Red 都使用定义范围。
从指南中,我知道它是静态作用域的一种形式,“变量的作用域是在定义其上下文时确定的”,也称为运行时词法作用域,是静态作用域的一种动态形式,取决于上下文定义.
我知道在 com-sci 中有两种形式的作用域:词法作用域(静态作用域)和动态作用域。这个定义范围让我感到困惑。
那么什么是定义范围呢?
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:
),然后构建一个包含这些词的对象,然后绑定该块中的所有词和所有嵌套块到对象中的单词,在这种情况下a
,func-2
和func-3
。这意味着该a
代码块中的 ' 已更改其绑定,改为指向该对象。
func-2
定义时,其代码块中的绑定不会a
被覆盖。当func-3
被定义时,它的a
规范中有一个,所以a:
它的绑定被覆盖了。
所有这一切的有趣之处在于根本没有任何作用域。那 firsta:
和a
infunc-1
的代码体只绑定一次,所以它们保持它们的第一个绑定。a:
in的inner
代码块和a
in的代码块func-2
被绑定了两次,因此它们保留了第二次绑定。a:
in的func-3
代码被绑定了 3 次,所以它也保持最后一次绑定。这不是作用域,它只是绑定代码,然后再次绑定更小的代码,依此类推,直到完成。
每一轮绑定都由一个正在“定义”某些东西(实际上是构建它)的函数执行,然后当该代码运行并调用定义其他内容的其他函数时,这些函数对其一小部分代码执行另一轮绑定. 这就是我们称之为“定义范围”的原因;虽然它实际上不是作用域,但它是 Rebol 中作用域的目的,它与词汇作用域的行为非常接近,乍一看你无法区分。
当您意识到这些绑定是直接的并且您可以更改它们时,它真的变得不同了(有点,您可以创建具有相同名称和不同绑定的新单词)。那些定义函数调用的同一个函数,你可以自己调用:它被命名为bind
. 有了bind
你,你可以打破范围界定的错觉,让单词绑定到你可以访问的任何对象。你可以用 做绝妙的技巧bind
,甚至可以制作自己的定义函数。这很有趣!
至于 Red,Red 是可编译的,但它还包括类似 Rebol 的解释器、绑定和所有好东西。当它使用解释器定义事物时,它也会定义范围。
这有助于使事情更清楚吗?
这是一个老问题,@BrianH 在这里的回答对力学非常透彻。但我想我会给一个稍微不同的焦点,作为一个“故事”。
在 Rebol 中,有一类称为words的类型。这些本质上是符号,因此它们的字符串内容被扫描并进入符号表。因此,while"FOO"
将是一个字符串,并且将是字符串的另一种“风味” ,<FOO>
称为标签... FOO
,,,并且都是具有相同符号 ID 的单词的各种“风味”。 (分别是“word”、“lit-word”、“set-word”和“get-word”。)'FOO
FOO:
: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 收到了两个块,一个代表参数列表,第二个代表正文。当它获得主体时,两个print
s 都绑定到本机打印功能(在这种情况下 - 重要的是要指出,当您从控制台以外的地方获取材料时,它们可能各自绑定不同!)。 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 中替换和foreach
forfor-each
)length?
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] 的函数返回。
还有一点关于定义范围的“为什么”,而不是“什么”。:-)
我的理解是:
Rebol 是静态作用域的
但,
问题不是“Rebol 使用什么范围?”,而是“何时确定 Rebol 范围,何时编译 Rebol 程序?”。
Rebol 有静态作用域,但是动态编译。
我们习惯了有一个编译时间和一个运行时间。
Rebol 有多个编译时间。
Rebol 代码的编译取决于编译时存在的上下文。
Rebol 代码在不同的时间、不同的上下文中编译。这意味着 Rebol 函数可能在不同时间以不同方式编译。