为什么REBOL中的函数"有记忆"?

Car*_*orc 6 function rebol rebol2

在rebol中我写了这个非常简单的函数:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: ""
    loop Length [append password (pick chars random Length)]
    password
    ]
Run Code Online (Sandbox Code Playgroud)

当我连续多次运行时,事情变得非常混乱:

loop 5 [print make-password 5]
Run Code Online (Sandbox Code Playgroud)

给(例如)此输出:

  • TWTQW
  • TWTQWWEWRT
  • TWTQWWEWRTQWWTW
  • TWTQWWEWRTQWWTWQTTQQ
  • TWTQWWEWRTQWWTWQTTQQTRRTT

看起来这个函数记住了过去的执行并存储了结果而不是再次使用它!

我没问过这个!

我希望输出类似于以下内容:

  • IPS30
  • DQ6BE
  • E70IH
  • XGHBR
  • 7LMN5

我怎样才能达到这个效果?

Hos*_*ork 5

一个好问题.

Rebol代码实际上被认为是一种非常风格化的数据结构.该数据结构"碰巧是可执行的".但你需要了解它是如何工作的.

例如,来自@ WiseGenius的建议:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: copy ""
    loop Length [append password (pick chars random Length)]
    password
]
Run Code Online (Sandbox Code Playgroud)

看看包含的块append password....该块在那里"成像"; 引擎盖下的真实情况是:

chars: **pointer to string! 0xSSSSSSS1**
password: copy **pointer to string! 0xSSSSSSS2**
loop Length **pointer to block! 0xBBBBBBBB**
password
Run Code Online (Sandbox Code Playgroud)

当解释器加载它们时,所有系列都以这种方式工作.字符串,块,二进制文件,路径,parens等等.鉴于它是"乌龟一直向下",如果你遵循该指针,块0xBBBBBBBB在内部:

append password **pointer to paren! 0xPPPPPPPP**
Run Code Online (Sandbox Code Playgroud)

这样做的一个结果是可以在多个位置引用一系列(因此"成像"):

>> inner: [a]

>> outer: reduce [inner inner]
[[a] [a]]

>> append inner 'b

>> probe outer
[[a b] [a b]]
Run Code Online (Sandbox Code Playgroud)

这可能是新手混淆的原因,但一旦您了解了数据结构,您就会开始知道何时使用COPY.

所以你已经注意到了这个函数的一个有趣含义.考虑这个程序:

foo: func [] [
    data: []
    append data 'something
]

source foo

foo
foo

source foo
Run Code Online (Sandbox Code Playgroud)

这产生了一个可能令人惊讶的结果:

foo: func [][
    data: [] 
    append data 'something
]

foo: func [][
    data: [something something] 
    append data 'something
]
Run Code Online (Sandbox Code Playgroud)

我们打foo了几次电话,看来函数的源代码正在改变.从某种意义上说,它是自修改代码.

如果这困扰你,R3-Alpha中有工具可以攻击它.您可以使用PROTECT保护函数体不被修改,甚至可以创建自己的替代方法,如FUNC和FUNCTION,它们将为您完成.(PFUNC?PFUNCTION?)在Rebol版本3中,您可以编写:

pfunc: func [spec [block!] body [block!]] [
    make function! protect/deep copy/deep reduce [spec body]
]

foo: pfunc [] [
    data: []
    append data 'something
]

foo
Run Code Online (Sandbox Code Playgroud)

当你运行时,你得到:

*** ERROR
** Script error: protected value or series - cannot modify
** Where: append foo try do either either either -apply-
** Near: append data 'something
Run Code Online (Sandbox Code Playgroud)

所以这迫使你复制系列.它还指出FUNC只是一个功能!本身,功能也是如此.你可以制作自己的发电机.

这可能会打破你的大脑,你可能会尖叫说"这不是任何理智的软件编写方式".或许你会说"我的上帝,它充满了星星." 反应可能有所不同.但对于为系统提供动力并赋予其灵活性的"技巧"来说,这是相当基础的.

(注意:Rebol3Ren-C分支从根本上使得函数体 - 和一般的源序列 - 在默认情况下被锁定.如果想要一个函数中的静态变量,你可以说foo: func [x <static> accum (copy "")] [append accum x | return accum],函数将积累accum跨越电话状态.)

我还建议密切关注每次运行时实际发生的情况.在运行该foo函数之前,数据没有任何价值.每次执行函数时,会发生什么,评估者看到SET-WORD!后跟一个系列值,它执行对变量的赋值.

data: **pointer to block! 0xBBBBBBBB**
Run Code Online (Sandbox Code Playgroud)

在该赋值之后,您将有两个对该块存在的引用.一个是它存在于在LOAD时间建立的代码结构中,在函数运行之前.第二个引用是存储在数据变量中的引用.通过第二个参考,您正在修改此系列.

请注意,每次运行函数时都会重新分配数据.但是一遍又一遍地重新分配给相同的值......那个原始的块指针!这就是为什么你必须COPY,如果你想在每次运行中都有一个新的块.

掌握评估者规则中的基本简单性是令人头晕目眩的一部分.这就是简单化打扮成语言的方式(在某种程度上你可以根据自己的方式扭曲).例如,没有"多项任务":

a: b: c: 10
Run Code Online (Sandbox Code Playgroud)

那只是评估者打出一个:作为SET-WORD!符号并说"好吧,让我们将其绑定上下文中的变量a与下一个完整表达式产生的任何内容相关联." b:做同样的事. c:做同样但是因为整数值10 ...而命中一个终端然后计算为10.所以看起来像多次赋值.

所以请记住,系列文字的原始实例是挂在已加载源中的实例.如果评估者有办法做这种SET-WORD!或SET赋值,它将借用指向源中该文字的指针来戳入变量.这是一个可变的参考.您(或您设计的抽象)可以通过PROTECT或PROTECT/DEEP使其不可变,并且您可以使其不是COPY或COPY/DEEP的参考.


相关说明

有些人认为你永远不应该写副本[] ...因为(a)你可能养成忘记写COPY的习惯,(b)你每次做一个未使用的系列.那个"空白系列模板"被分配,必须由垃圾收集器扫描,没有人真正接触它.

如果你写make块!10(或者你想要预先分配块的任何大小)你可以避免这个问题,保存一个系列,并提供一个大小提示.

  • @Caridorc Rebol3重新定义了FUNCTION的含义,使其成为Rebol2所谓的FUNCT*(与[Redred]相符)(http://www.red-lang.org/p/contributions.html),*).我倾向于找到Rebol3函数,它自动收集SET-WORD!作为**/local**参数的正文中的元素,是新用户的更优雅的名称和最佳默认行为,因此我倾向于键入它与较低级别的原始FUNC.但是我在我的例子中将它改为FUNC,以防Rebol2和Rebol3中都考虑了这个例子. (2认同)