Rebol 编程/Rebol 编程
Rebol 的强大功能很大程度上来自它既是一种函数式编程语言,也是一种符号式语言。
作为一种函数式编程语言,Rebol 使用一系列表达式(由函数构建),这些表达式被求值以产生结果流,这些结果流从一个表达式传递到另一个表达式。Rebol 没有关键字,单词根据上下文求值。一般来说,求值的顺序是从左到右,特殊运算符除外。
一种符号式语言是允许你像语言中的任何其他值一样表示和操作符号(单词)的语言。随着你对 Rebol 技能的提高,符号式编程的优势将更加明显。它是打开方言之门的关键,它允许你使用更少的代码创建更强大的程序,以及将 Rebol 表达式发送到互联网上,以便在其他计算机系统上求值,而不仅仅是在你自己的系统上。(称为“分布式计算”。)
空白字符,如空格、制表符、换行符、换页符,就像在英语中一样充当分隔符,将值彼此隔开。它们表示一个值的结束和另一个值的开始。
例如,这是一个包含三个值(整数1、运算符+和整数2)的序列,它们代表一个表达式
>> 1 + 2
请注意空格。如果我们省略空格,我们将获得
>> 1+2
这被认为是一个语法上不合法的值。
其他可以扮演分隔符角色的字符是:( ) " [ ] { } ;。
这一点很重要,因为在编写 Rebol 代码时,语言允许你自由选择,这样你的程序就可以完全在一行很长的代码中编写,或者在多行代码中进行分割和缩进。后一种方法是首选方法,建议读者遵循有关如何格式化源代码的已发布指南。 (Rebol/Core 用户指南 - 第 5 章 - 样式指南)
Rebol 使用算术运算符:+、-、*、/、//、**、布尔运算符:and、or、xor 和比较运算符:<、<=、<>、=、==、=?、>、>=。它们通常是二元的,在每侧使用一个参数。例如
>> 2 > 3 == false
每个运算符都有一个函数对应物
运算符 | 函数 |
---|---|
+ | add |
- | subtract |
* | multiply |
/ | divide |
// | remainder |
** | power |
and | and~ |
or | or~ |
xor | xor~ |
< | lesser? |
<= | lesser-or-equal? |
<> | not-equal? |
= | equal? |
== | strict-equal? |
=? | same? |
> | greater? |
>= | greater-or-equal? |
例如,上面的表达式可以改写如下
>> greater? 2 3 == false
例外
- 运算符可以用作前缀(虽然这通常不建议,因为它可能会损害代码的可读性,而且,它没有得到官方支持)。
- - 运算符可以用作一元运算符(函数对应物称为negate)。
示例
>> + 2 3 == 5 >> - 2 == -2
可以编写更复杂的运算符表达式。有两条规则需要牢记
- 所有运算符具有相同的优先级。
- 运算符表达式从左到右求值。
示例
>> 1 + 2 * 3 == 9
请注意,左侧的加法在乘法之前执行。如果我们想先执行乘法,我们可以重新排序表达式
>> 3 * 2 + 1 == 7
或者使用括号(对于长公式始终使用括号,可以避免错误)
>> 1 + (2 * 3) == 7
假设我们想比较两个表达式的结果:1 + 3 和 2 + 2。我们应该使用括号来达到预期的求值顺序
>> (1 + 3) = (2 + 2) == true
虽然第一对括号不是必需的(最左侧的加法会优先执行),但这对于最右侧的加法并不成立。如果我们省略第二对括号,我们将比较最左侧的加法结果与 2,这是合法的,但我们将尝试将 2 添加到false,这将导致非法加法
>> 1 + 3 = 2 + 2 ** Script Error: Expected one of: logic! - not: integer! ** Near: 1 + 3 = 2
错误报告看起来不太容易理解,它与我们在以下情况下获得的报告相同
>> false + 2 ** Script Error: Expected one of: logic! - not: integer! ** Near: false + 2
通常我们只求值一个表达式,只需要一个结果。但是,可以一个接一个地求值多个表达式
>> 4 + 6 7 + 8 == 15
它求值了 4 + 6,然后是 7 + 8,并且只显示了最后一个。可以使用括号编写类似的示例
>> (4 + 6) (7 + 8) == 15
Rebol 只显示最后一个表达式,但会跟踪所有表达式。
>> do [a: 4 + 6 7 + 8] == 15 >> a == 10
它与以下内容相同
>> do [ a: 4 + 6 7 + 8 ] == 15
要从两个或多个表达式中获取所有结果,可以使用reduce 函数,如下所示
>> reduce [4 + 6 7 + 8] == [10 15]
我们获得了一个包含所有收集结果的块。
另一个可能需要收集一些结果的重要情况可能是函数的求值,该函数需要一些参数。以add 函数为例
>> add 2 + 3 4 + 6 == 15
解释:在这种情况下,我们要求解释器求值add 函数。解释器需要为该函数收集两个参数,因此它对右侧的两个表达式进行求值,并获得两个结果,可以用作参数。它与以下内容相同
>> add (2 + 3) (4 + 6) == 15
上面的描述对于具有更多或更少参数的函数有效,即对于仅接受一个参数的函数也是如此
>> abs -4 + -5 == 9
这里解释器需要求值abs 函数。因此它对右侧的第一个表达式求值,并获得 -9,它用作abs 函数的参数。它与以下内容相同
>> abs (-4 + -5) == 9
因此,使用括号以避免错误...
由于解释器收集函数参数的方式,看起来右侧的运算符表达式优先于函数求值。
当函数使用未求值(或获取)的参数时,很容易解释为什么情况并非如此:在这种情况下,解释器不需要求值表达式来获得参数的值。
控制台是您编写小段代码以快速测试您的想法和函数的主要位置。无需编译,表达式会在您按下Enter键时立即计算。这允许在控制台中进行交互式编程,并有助于调试大型程序。没有编译阶段允许快速原型设计和测试。
我们之前看到了一个数学示例,但现在让我们尝试一些更复杂的内容
>> join "Hi" "There" == "HiThere"
join 函数将两个字符串合并并返回一个新字符串:"HiThere"。
现在让我们通过在join前面插入reverse来反转该字符串中的所有字符
>> reverse join "Hi" "There" == "erehTiH"
我们可以再次反转它
>> reverse reverse join "Hi" "There" == "HiThere"
现在您基本上观察到 Rebol 语言的工作方式
您可以直接操作返回的值,因为它们输出到函数左侧。值从程序的右侧流向左侧,您可以在途中操作它们。
这是一个非常重要的观察!可以无限地执行此类操作。这基本上是在 Rebol 中构建程序的方式。
>> reverse print join "Hi" "There" HiThere ** Script Error: reverse expected value argument of type: series tuple pair ** Near: reverse print join "Hi" "There"
发生的事情是print 函数没有返回reverse 函数可以使用的值。这就是为什么您会在控制台中看到print 的输出和reverse 的错误输出的原因。
此规则的另一个例外是二元运算符。它们通常在两侧消耗一个参数。
外框仅用于说明目的,但这就是您应该看到的操作方式。
本质上
- 函数返回值
- 您可以将其他运算符或函数用于它们的左侧。
- 运算符返回值
- 类似于函数,但消耗(通常)两侧的参数。您可以将函数用于运算符表达式的左侧或其他运算符(通常用于右侧)以消耗其输出。
混合函数和运算符的示例
等效代码是
>> print divide 2 + 2 8 0.5
运算符* 使用运算符+ 的输出的示例
>> 2 + 3 * 4 == 20
如上所述,Rebol 词汇可以作为符号使用。除此之外,我们可以让词汇“工作”作为变量,即引用其他 Rebol 值。
让我们尝试通过构建一个多行簿记程序来做到这一点。我们想要使用一个名为wallet的变量。
首先,让我们看看在控制台中输入它会发生什么
>> wallet ** Script Error: wallet has no value ** Near: wallet
发生这种情况是因为该词没有分配值。我们需要为它分配一个值
>> wallet: $25
我的钱包里有 25 美元。我可以简单地通过输入来返回此值
>> wallet == $25.00
现在我想加 5 美元
>> wallet: wallet + $5 == $30.00
另一种方法是写
>> wallet: add wallet 5 == $35.00
一直这样写会很乏味,所以我们想创建一个函数来处理向钱包中添加资金的任务。函数只是一段程序代码,每次您输入特定词语时都会执行该代码。一个简单的函数是这样创建的
>> earn: does [wallet: add wallet $5]
它与上面显示的代码相同,但包含在方括号中,称为块,提供给does 函数,这意味着每次评估它时创建一个执行此代码块的函数。它需要存储,并且存储方式与存储数字相同。我们将新函数分配给词语earn。
这是 Rebol 的一大优势之一,即存储值和函数的工作方式相同!我们仍在遵循我们的从右到左规则。
这开辟了一些非常巧妙的可能性,我们将在后面的章节中探讨。
我们的earn 函数已经创建。所以每次您输入
>> earn $40.00 >> earn $45.00
这样输入容易多了,对吧?但是,如果您每次想赚取不同的金额怎么办?就像我们之前在reverse 和join中看到的那样,这些函数接受一个或多个参数。因此,我们需要使用func 而不是does。
>> earn: func [amount] [wallet: add wallet amount]
请注意,我们的程序代码之前使用了另一个块。这是我们存储函数参数的地方,也称为参数列表。数字5 已在代码中被amount 替换,它是参数列表中的一个变量。您可以从参数列表中获取任意数量的参数,并在函数代码中使用任意次数。
我们可以同样创建一个spend 函数
>> spend: func [amount] [wallet: subtract wallet amount]
现在您可以像使用任何其他函数一样使用它们
>> earn $10 == $45.00 >> spend $25 == $20.00
您是否注意到我们在earn 和spend 的返回值中实际上使用了真正的 $ 符号?使用它,Rebol 将该数字识别为货币金额!
您现在可能想知道,为什么 Rebol 使用上述语法进行变量赋值?毕竟,大多数语言都使用这样的等号
number = 10
为什么 Rebol 那样写
number: 10
事实证明,Rebol并非仅仅为了不同而这样做,它是语言的重要组成部分。
正如我们之前暗示的那样,Rebol 的核心是一种高级语言。它超越了大多数其他语言,因为 Rebol 将代码、数据和元数据(描述其他数据的数据)的概念集成到一种语言中。(我们将在介绍“方言”时讨论这一点。)
但现在,这样想。当你写
number = 10
你正在声明
variable assignment-operator value
但是,当你写
number: 10
你正在声明
variable-defined-as value
这个事实使 Rebol 的变量定义脱颖而出,成为语言的特殊数据类型。定义是唯一的,不依赖于运算符(= 符号)的含义。这是一个强大的概念,在处理高级代码即数据和元数据时非常有用。
如果您仍然难以理解这种符号,那么这样想:在书面人类语言中,哪种更常见?事实上,如果您查看电子邮件标题或 http 标题,值字段是如何表达的?Rebol 方式。为什么呢?
控制台允许您在单个表达式中使用多行代码。如果您以[ 开始一个块并按下Enter 键,控制台将不会停止接受输入,直到您给出相应的]。
为了让您知道控制台现在正在接受多行输入,提示将从>> 更改为当前使用的分隔符,例如[,并且它将一直保持这种状态,直到] 出现。
>> todays-earnings: [ [ $25.00 [ $30.00 [ $14.00 [ $10.00 [ ]
多行字符串也是可能的,但请注意,多行字符串使用{ } 而不是" "。
>> very-long-string: { { This string { contains many { lines. { }
有时,您可能想在控制台中尝试代码示例以测试某个函数。只需从源代码中复制它并粘贴它(在 Windows 中使用Ctrl+C 和Ctrl+V)。
学习 Rebol 的所有人都会犯的最常见的错误是处理函数中的文字系列。
例如,这里有一个函数,它根据名称作为参数打印一个“Dear”。
dear: func [ name [string!] /local salute ][ salute: "Dear " print append salute name ]
当函数第一次被调用时,结果如预期
>> dear "John" Dear John
但是,当再次运行时,结果出乎意料
>> dear "Jane" Dear JohnJane
发生这种情况是因为salute 变量初始化为文字字符串“Dear”,该字符串保留在函数体中,并成为更改的对象。append 函数通过将实际参数字符串添加到该字符串来更改该字符串。
您可以通过检查dear 函数在首次计算之前的结果来验证这一点
probe :dear
这将导致
func [ name [string!] /local salute ] [ salute: "Dear " print append salute name ]
然后,当您在用参数“John”计算它后检查它时
func [ name [string!] /local salute ] [ salute: "Dear John" print append salute name ]
现在很明显,该字符串已更改为“Dear John”。
为了确保salute 变量每次都正确初始化,我们需要在函数体中保持字符串“Dear”不变。为了保护函数体中的字符串,我们可以将它的一个副本分配给salute 变量,如下所示
Dear: func [ name [string!] /local salute ] [ salute: copy "Dear " print append salute name ]
这保证了对salute 字符串的更改不会改变函数体中的原始字符串。
使用块时也会出现同样的陷阱
>> test: func [l [integer!]] [b: [] repeat x l [append b x]] >> test 2 == [1 2] >> test 3 == [1 2 1 2 3]
为了确保test 函数不会改变它包含的块,我们可以使用
b: copy []
在本例中,在test 函数的函数体中。
Rebol 从左到右计算,但中缀运算符优先于函数,破坏了正常的计算流程。
当比较值时,会发生一个常见的错误
if length? series < 10 [print "less then 10"] ** Script Error: length? expected series argument of type: series port tuple bitset struct ** Near: if length? series < 10
这里,中缀运算符< 优先于length? 函数。然后将结果(一个布尔值)传递给函数length?,而实际上length? 期望的是一个系列参数而不是一个布尔值。
为了解决这个问题,可以将表达式改写为
if (length? series) < 10 [print "less than 10"]
或者作为
if 10 > length? series [print "less than 10"]
在后一种版本中,中缀运算符优先,并计算10,然后计算length?。由于length? 需要一个参数,因此它消耗series 并将值返回给> 以给出正确的计算结果。
另一个常见的错误,无论初学者还是专家都可能犯,就是忘记为函数提供参数。
在下面的例子中,我们忘记为append函数提供最后一个参数
>> append "example" ** Script Error: append is missing its value argument ** Near: append "example"
错误信息中的“value”参数指的是什么?使用help命令来找出答案。
>> help append USAGE: APPEND series value /only DESCRIPTION: Appends a value to the tail of a series and returns the series head. APPEND is a function value. ARGUMENTS: series -- (Type: series port) value -- (Type: any) REFINEMENTS: /only -- Appends a block value as a block
在这里你可以看到,“value”是第二个参数。这就是缺少的部分。
额外的参数
[edit | edit source]另一个常见的参数错误是提供太多参数。这种错误比较微妙,因为你不会得到错误信息。
以下是一个例子。假设你有下面的表达式
if num > 10 [print "greater"]
但是,你决定在else情况下打印“not greater”。你可能会倾向于写
if num > 10 [print "greater"] [print "not greater"]
这是一个错误,但是当你尝试时,你不会得到错误信息。第二个代码块会被忽略。这是因为if函数只接受一个代码块。如果你想要两个代码块,你应该使用either函数,如下所示
either num > 10 [print "greater"] [print "not greater"]
注意代码中的这些错误类型。
注意: Rebol 允许这些“额外”值而不产生错误是有充分理由的。它实际上是 Rebol 的一项特殊功能。考虑上面提到的reduce函数。允许多个表达式依次出现,可以让你创建特殊的数据结果,这些结果非常有用。这是一个高级主题,但是看一下下面的例子,了解它在 Rebol 中的重要性。
>> reduce [if num > 10 [123] ["example" 456]] == [123 ["example" 456]]
错误的参数类型
[edit | edit source]如果为函数提供了不正确的参数类型,也会发生这种情况。在某些特定情况下,错误信息可能有点令人困惑。
修改上面的if示例,如果你写
if num > 10 print "greater"
你会得到这个错误
** Script Error: if expected then-block argument of type: block ** Near: if num > 10 print
之所以会发生这种情况,是因为你没有为函数提供正确的参数类型。你想要写的应该是
if num > 10 [print "greater"]
同样,如果你想知道 Rebol 中“then-block”的含义,请使用help函数
>> help if USAGE: IF condition then-block /else else-block DESCRIPTION: If condition is TRUE, evaluates the block. IF is a native value. ARGUMENTS: condition -- (Type: any) then-block -- (Type: block) REFINEMENTS: /else -- If not true, evaluate this block else-block -- (Type: block)
Copy vs copy/deep
[edit | edit source]给定以下序列
a: [a b c [d e f]]
执行copy
b: copy a
probe b
>> [a b c [d e f]]
现在b包含单词a, b, c,它的第四个元素与a的第四个元素相同。可以通过追加到a/4并再次检查b来查看这一点
append a/4 'g
probe b
>> [a b c [d e f g]]
在copy/deep上,c/4变为a/4的独立副本,因此子块a/4和c/4并不相同。
c: copy/deep a
append a/4 'h
probe c
>> [a b c [d e f g]]
c/4不包含h,但b/4包含。
probe b
>> [a b c [d e f g h]]
b和c都不会包含i,因为它在外部序列中。
append a 'i
probe c
>> [a b c [d e f g]]
probe b
>> [a b c [d e f g h]]
代码可读性
[edit | edit source]即使在控制台中,你也可以生成相当长的函数和变量序列。如果你把它们全部放在一行中,可能很难阅读,尤其是在 Rebol 使得使用括号进行评估变得不必要的情况下。
print multiply add 4 add 6 5 divide 1 square-root 3
训练有素的眼睛可能知道从哪里开始,但是这种混合使用函数作为参数的方式很快就会变得难以阅读。
一种方法是将代码拆分成多行。在 Rebol 中,你可以完全自由地这样做
print multiply add 4 add 6 5 divide 1 square-root 3
这有助于理解正在发生的事情。
print (4 + 6 + 5) * (1 / square-root 3)
是相同表达式的更简单的等效形式。