从REBOL /核心用户指南和什么是红色,我了解到Rebol和Red都使用了定义范围.
从指南中,我知道它是静态作用域的一种形式,"变量的作用域在定义其上下文时确定",也称为运行时词法作用域,并且是依赖于上下文定义的动态形式的静态作用域..
我知道在com-sci中,有两种形式的范围:词法范围(静态范围)和动态范围.这个定义范围使我困惑.
那么什么是定义范围?
Bri*_*anH 27
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]
]
Run Code Online (Sandbox Code Playgroud)
因此,加载该代码后,如果Rebol有词法范围,这就是您所看到的:
>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 1]
Run Code Online (Sandbox Code Playgroud)
那将是因为func-1使用a来自外部作用域的,a使用的func-2是来自内部作用域,以及func-3调用func-1,它仍然使用a来自定义它的外部作用域,而不管它是什么func-3.
如果Rebol有动态范围,这就是你所看到的:
>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 3]
Run Code Online (Sandbox Code Playgroud)
这将是因为func-3重新定义a,然后调用func-1,它只使用最新的活动定义a.
现在对于Rebol,你得到了第一个结果.但是Rebol没有词法范围.所以为什么?
Rebol假装它.这是它的工作原理.
在编译语言中,您有范围.当编译器遍历文件时,它会跟踪当前作用域,然后当它看到嵌套作用域成为当前作用域时.对于词法作用域,编译器保留对外部作用域的引用,然后通过跟随外部作用域的链接查找当前作用域中未定义的单词,直到找到该单词,或者不找到.动态范围的语言做类似的事情,但在运行时,调高堆栈.
Rebol没有做任何事情; 特别是它不是在运行时编译的.您认为代码实际上是数据,单词块,数字等.单词是在其中具有指针的数据结构,称为"绑定".
首次加载该脚本时,脚本中的所有单词都会添加到脚本的环境对象中(我们不恰当地称之为"上下文",尽管它不是).在收集单词时,脚本数据会发生变化.在脚本的"上下文"中找到的任何单词都链接到"上下文"或"绑定".这些绑定意味着您可以只跟随一个链接并到达存储该单词值的对象.这真的很快.
然后,一旦完成,我们就开始运行脚本了.然后我们谈谈这一点:func [] [a].这不是一个真正的声明,它是对一个名为的函数的调用,func它接受一个spec块和一个代码块并使用它们来构建一个函数.该函数也获得了自己的环境对象,但是在函数规范中声明了单词.在这种情况下,规范中没有单词,因此它是一个空对象.然后代码块绑定到该对象.但是在这种情况下,a在该对象中没有任何东西,所以没有对它做任何事情a,它保留了它之前已经绑定的绑定.
同样适用于context [...]调用 - 是的,这是对不恰当命名的函数的调用context,它通过调用来构建对象make object!.该context函数接受一个数据块,并搜索set-words(具有尾随冒号的那些东西a:,然后),然后使用其中的那些单词构建一个对象,然后它绑定该块中的所有单词和所有嵌套阻止对象中的单词,在这种情况下a,func-2和func-3.这意味着a代码块中的's'已经更改了绑定,而是指向该对象.
当func-2被定义,的结合a在其代码块不覆盖.当func-3被定义,它具有一个a在它的规范,所以a:有其结合覆盖.
所有这一切的有趣之处在于根本没有任何范围.第一个a:和ain func-1的代码体只绑定一次,所以它们保持第一个绑定.该a:在inner的码块和a在func-2的结合的两倍,因此他们保持第二结合.该a:在func-3的代码,势必三次,所以它也保持其最后的约束力.它不是范围,只是代码绑定,然后再次绑定较小的代码位,依此类推,直到完成为止.
每一轮绑定都是由一个"定义"某些东西(实际上是构建它)的函数执行,然后当该代码运行并调用定义其他东西的其他函数时,这些函数执行另一轮绑定到它的小代码子集.这就是我们称之为"定义范围"的原因; 虽然它确实没有确定范围,但它正是Rebol中确定范围的目的,它与词汇范围的行为非常接近,乍一看你无法区分它们.
当你意识到这些绑定是直接的,并且你可以改变它们时,它真的变得不同了(有点,你可以使用相同的名称和不同的绑定来创建新的单词).这些定义函数调用的功能相同,你可以自称:它的名字bind.通过这种方式,bind您可以打破范围界定的错觉,并创建与您可以访问的任何对象绑定的单词.你可以做一些精彩的技巧bind,甚至可以制作你自己的定义功能.这很有趣!
至于Red,Red是可编辑的,但它还包括一个类似Rebol的解释器,绑定和所有的好东西.当它用解释器定义事物时,它也会定义范围.
这有助于使事情更清楚吗?
Hos*_*ork 13
这是一个老问题,@ BrianH的答案在机制上非常彻底.但我认为我会给一个略有不同的焦点,作为一个"故事".
在Rebol中,有一类称为单词的类型.这些本质上是符号,因此扫描它们的字符串内容并将它们放入符号表中.因此,而"FOO"将是一个字符串,<FOO>则是另一种被称为代码字符串的"味道" ...... FOO,'FOO,FOO:并且:FOO具有相同符号ID的话都不同的"口味". (分别为"单词","lit-word","set-word"和"get-word".)
折叠为符号使得无法在加载后修改单词的名称.与每个都有自己的数据且可变的字符串相比,它们被卡住了:
>> append "foo" "bar"
== "foobar"
>> append 'foo 'bar
** Script error: append does not allow word! for its series argument
Run Code Online (Sandbox Code Playgroud)
不变性具有优势,因为作为符号,将一个词与另一个词进行比较是快速的.但是还有另外一个难题:一个单词的每个实例都可以选择在其中有一个不可见的属性,称为绑定.该绑定使其"指向"一个键/值实体,称为可以读取或写入值的上下文.
注意:与@BrianH不同,我认为将这类绑定目标称为"上下文"并不是那么糟糕 - 至少我今天不认为.稍后问我,如果有新证据出现,我可能会改变主意.可以说它是一个类似对象的东西,但并不总是一个对象......例如,它可能是对函数框架中的一个框架的引用.
无论谁将一个单词带入系统,都会在第一次出现时说出它所绑定的上下文.很多时候都是LOAD,所以如果你说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"
Run Code Online (Sandbox Code Playgroud)
注意:第二种情况不打印该字符串.它探测了函数规范,然后字符串只是评估中的最后一件事,所以它对它进行了评估.
但是,一旦你手中有一个包含文字的数据块,绑定就是任何人的游戏.只要上下文中包含单词的符号,您就可以将该单词重新定位到该上下文. (假设该块未被保护或锁定以防止修改...)
这种连锁的重新绑定机会是重要的一点.由于FUNC是一个"函数发生器",它采用了你给它的规格和体,它能够通过它的绑定来取得身体的"原始物质",并覆盖它决定的任何一个.也许是怪异的,但看看这个:
>> x: 10
>> foo: func [x] [
print x
x: 20
print x
]
>> foo 304
304
20
>> print x
10
Run Code Online (Sandbox Code Playgroud)
发生的事情是FUNC收到了两个块,一个代表一个参数列表,另一个代表一个正文.当它到达主体时,两者都print被绑定到原生打印功能(在这种情况下 - 重要的是要指出当你从控制台以外的地方获取材料时,它们每个都可以被绑定不同!). x被绑定到持有值10 的用户上下文(在这种情况下).如果FUNC没有做任何改变这种情况的事情,事情会保持这种状态.
但是它把图片放在一起并决定,因为参数列表中有一个x,它将通过正文查看并用x的符号ID 覆盖带有新绑定的单词...函数的本地.这是它没有覆盖全球的唯一原因x: 20.如果你省略了规范中的[x],FUNC就不会做任何事情,而且它会被覆盖.
定义链中的每个部分在传递之前都有机会.因此定义范围.
有趣的事实:因为如果你不为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]
]
Run Code Online (Sandbox Code Playgroud)
这种毫不起眼的前瞻性工具扩展在下面的样式语言(同样,任-C版本...在R3-α或Rebol2替代foreach的for-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]]
Run Code Online (Sandbox Code Playgroud)
这里定义范围界定的技巧最好通过我上面提到的来理解.FUNC只会覆盖参数列表中事物的绑定,并保持体内其他所有内容不变.所以会发生这样的情况,它会将您传递给COLLECT的主体用作新函数的主体,并覆盖KEEP的任何绑定.然后,它将KEEP设置为一个函数,该函数在调用时将数据添加到聚合器.
在这里,我们看到KEEP功能通过/ ONLY开关将块拼接到收集的输出中的多功能性(只有当我们看到长度为3的项目时,调用者选择不拼接).但这只是表面上的问题.它只是一个非常强大的语言功能 - 用户在事后添加 - 源于如此少的代码,这几乎是可怕的.当然还有更多的故事.
我在这里添加一个答案,因为已经填写了定义范围界定的关键缺失链接,这个问题被称为"定义范围的返回":
https://codereview.stackexchange.com/questions/109443/definitional-returns-solved-mostly
这就是为什么<with> return它与规范中的KEEP并列.它就在那里因为COLLECT试图告诉FUNC它想要"使用它的服务"作为代码的绑定和运行.但是身体已经由其他人在其他地方创作.因此,如果它有一个RETURN,那么RETURN已经知道返回的位置.FUNC只是为了"重新定义"保留,但只留下任何回报而不是添加自己的回报.因此:
>> foo: func [x] [
collect [
if x = 10 [return "didn't collect"]
keep x
keep 20
]
]
>> foo 304
== [304 20]
>> foo 10
== "didn't collect"
Run Code Online (Sandbox Code Playgroud)
这<with> return使得COLLECT能够足够聪明地知道在FOO的体内,它不希望返回反弹,所以它认为从参数只是[保持]的函数返回.
还有一点关于定义范围的"原因",而不仅仅是"什么".:-)