Ruby 喜欢语法糖(syntactic sugar)。
在计算机科学中,语法糖是一类语法,这类语法对于语言的功能不产生任何影响。但是提高代码的可读性,从而更方便程序员使用。Ruby 提供了一些语法,使得其阅读起来更接近于自然语言。比如下面的代码:
stop = false
if not stop
puts 'Go!'
end
在自然语言中,我们很少会使用 if not 这样的语法,而是会使用 unless
。Ruby 中也提供了 unless
关键词,作为 if not 的替代。于是你可以把代码写成下面这样:
stop = false
unless stop
puts 'Go!'
end
在 Ruby 中,如果条件判断内只有一行,你可以把条件判断放在你要执行的代码后面,从而写成:
stop = false
puts 'Go!' unless stop
这样读起来确实变得更接近自然语言了。类似地,对于 while not
循环,你可以用 until
关键词来替代:
a = 5
until a == 0
a = a - 1
end
同样,你也可以简化成一行:
a = 5
a = a - 1 until a == 0
对于四则运算,一个常见的需求就是累加累减,也就是说,对于某一变量计算的结果会存回变量本身。对于这一点 Ruby 也提供了语法糖,即可以使用
a += b
a -= b
a *= b
a /= b
来替代
a = a + b
a = a - b
a = a * b
a = a / b
于是我们可以把上面的代码简化成:
a = 5
a -= 1 until a == 0
Warning
|
特别注意
如果你是 C 语言或者 C++ 语言的使用者,那么你必然还熟悉另一种累加累减运算符 |
对于死循环(infinite loop),你还可以使用 loop
来替代 while true
:
loop do
puts 'Hello World'
end
Caution
|
本章节涉及较为进阶内容,建议第一次接触编程或缺乏编程经验的开发者暂时跳过这一章节。 |
事实上,有了变量赋值和条件、循环(或者跳转),如果不考虑交互和通讯,就已经可以解决了所有计算机可以计算的问题。关于这一方面的研究被称为可计算性理论(Computability Theory)。20 世纪上半叶,对于可计算性的公式化表达主要有三:
-
阿隆佐·邱奇提出的 Lambda 演算。
-
阿兰·图灵提出的通用图灵机。
-
邱奇、克莱尼和罗斯塞尔提出的递归算法。
现在已经证明,这三种模型的可计算性上是等价的。在今天的高级语言中,语言通常会提供高于这三者的抽象。以 Ruby 为例,Ruby 同时提供偏向于图灵机设计理念的变量、分支系统,提供接近 Lambda 演算概念的函数式编程泛型,也支持递归函数定义。但即使如此,Ruby 编程语言的可计算性并不比只提供一种模型强。今天的计算机和 20 年前的计算机,也没有可计算性上的差异,只是存储更大、运行速度更快而已。这如同标准大气压下三杯 100℃ 的开水,把三杯水倒在一起,温度还是 100℃。但是不同语言确实会有不同的抽象,语言提供丰富的抽象的目的主要有两点:
-
更适合现代计算机系统的设计,从而提高性能。
-
更符合人类直觉,从而增加代码的可读性。
比如我们给定一个数组和范围,让程序计算其平方,在 Ruby 中可以写成:
def square(arr, a, b)
(a...b).each do |i|
arr[i] = arr[i] ** 2
end
end
这一程序可以同时对整数和小数作用。不同于 Ruby,像 C++ 之类的语言就要求必须要在编译器定义数据的类型。比如下面的程序只能对浮点数有效:
void square(float *A, int start, int end) {
for (int i = start; i < end; ++i) {
A[i] = A[i] * A[i];
}
}
但如果我们检查 C++ 编译器(此处使用 LLVM 9.0 (x86-64),编译参数为 -O2
)我们可以看到如下的交给 CPU 执行的汇编代码:
square(float*, int, int): # @square(float*, int, int)
cmp esi, edx
jge .LBB0_11
movsxd rax, esi
movsxd r11, edx
mov r10, r11
sub r10, rax
cmp r10, 7
jbe .LBB0_10
mov r8, r10
and r8, -8
lea rcx, [r8 - 8]
mov rdx, rcx
shr rdx, 3
add rdx, 1
mov r9d, edx
and r9d, 1
test rcx, rcx
je .LBB0_3
sub rdx, r9
lea rcx, [rdi + 4*rax]
add rcx, 48
xor esi, esi
.LBB0_5: # =>This Inner Loop Header: Depth=1
movups xmm0, xmmword ptr [rcx + 4*rsi - 48]
movups xmm1, xmmword ptr [rcx + 4*rsi - 32]
movups xmm2, xmmword ptr [rcx + 4*rsi - 16]
movups xmm3, xmmword ptr [rcx + 4*rsi]
mulps xmm0, xmm0
mulps xmm1, xmm1
movups xmmword ptr [rcx + 4*rsi - 48], xmm0
movups xmmword ptr [rcx + 4*rsi - 32], xmm1
mulps xmm2, xmm2
mulps xmm3, xmm3
movups xmmword ptr [rcx + 4*rsi - 16], xmm2
movups xmmword ptr [rcx + 4*rsi], xmm3
add rsi, 16
add rdx, -2
jne .LBB0_5
test r9, r9
je .LBB0_8
.LBB0_7:
add rsi, rax
movups xmm0, xmmword ptr [rdi + 4*rsi]
movups xmm1, xmmword ptr [rdi + 4*rsi + 16]
mulps xmm0, xmm0
mulps xmm1, xmm1
movups xmmword ptr [rdi + 4*rsi], xmm0
movups xmmword ptr [rdi + 4*rsi + 16], xmm1
.LBB0_8:
cmp r10, r8
je .LBB0_11
add rax, r8
.LBB0_10: # =>This Inner Loop Header: Depth=1
movss xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
mulss xmm0, xmm0
movss dword ptr [rdi + 4*rax], xmm0
add rax, 1
cmp r11, rax
jne .LBB0_10
.LBB0_11:
ret
.LBB0_3:
xor esi, esi
test r9, r9
jne .LBB0_7
jmp .LBB0_8
我们会发现,编译结果和我们的语义有非常大的差异。这是因为编译器识别出了这是一个循环,同时发现了循环内的数据没有依赖性。最后由于数据结构的类型已经被提前定义,数据的宽度可以精确确认。所以编译器就能精确使用 CPU 的 SIMD 指令集来进行运算。于是它就会把多个浮点数数据同时计算,并且会引入一些代码来处理边界情况,从而极大提高运行的性能。
这对于 Ruby 灵活的动态类型系统是无法做到的。这就是所谓为性能服务的语法设计。
在早年流行的高级语言 BASIC 中,一个非常常用的流程控制指令是 goto
,一个常见的无限循环写法如下(以 1978 年 Apple II Plus 上的 Applesoft BASIC 为例):
运行结果如下:
仔细思考会发现,goto
的语义不但可以替代所有循环(while
for
),还能替代循环内部的流程控制(break
continue
)。goto
在现代的 CPU 中都有直接的指令对应实现。在 x86 和 x86-64 系统上就是最直接的 JMP
指令,虽然还需要处理一些栈上内存相关的事物,但也可以简单由数个指令执行完毕,性能上是绝对没有问题的。
但我们现在很少使用 goto
语法的一大原因是因为,goto
的功能过于强大,虽然符合机器的执行原理,但是却不符合人的思考直觉。代码不仅仅需要被计算机执行,还需要被人类编写、讨论和维护。可读性的重要性随着工程的复杂度的提高会显得越来越重要。而 while
for
break
continue
等一系列限制更严格的条件控制方法的引入更符合了人类的逻辑直觉,从而提高的代码的可读性。