Nim-过程(procedure)

Nim - procedure

要定义示例中的echo和readLine等新命令,需要过程的概念。您可能习惯了它们在其他语言中被称为方法或函数,但是Nim区分了这些概念。在Nim中,新过程是用proc关键字定义的:

其实函数叫做过程这个名字还是很贴切的,我们平时一直区分一个语言是面向过程的还是面向对象的,比如说C语言是一个面向对象的语言。在C语言中内存被分为四大类:全局区、静态区、堆区和栈区。而栈区主要依赖于函数,而有些翻译就会将栈区翻译为存储过程。从C语言的角度来看,每一个程序就是一个过程,这个过程在执行的途中会有很多其他的子过程也就是其他的函数。一个程序就是若干个过程嵌套凭借组合的结果。

所以在这里叫做过程没有丝毫的问题,而且很直观。

1
2
3
4
5
6
7
8
9
10
11
12
proc yes(question: string): bool =
echo question, " (y/n)"
while true:
case readLine(stdin)
of "y", "Y", "yes", "Yes": return true
of "n", "N", "no", "No": return false
else: echo "Please be clear: yes or no"

if yes("Should I delete all your important files?"):
echo "I'm sorry Dave, I'm afraid I can't do that."
else:
echo "I think you know what the problem is just as well as I do."

这个例子展示了一个叫做yes的过程,他接受一个叫做question类型为string的形式参数,返回一个布尔类型。

过程的第一句echo输出 question的内容以及 (y/n) 然后进入一个”死循环“中,然后使用case判断用户输入的内容,匹配到”y”, “Y”, “yes”, “Yes” 就返回 true, 匹配到”n”, “N”, “no”, “No” 将返回false。如果不是上述两种情况就进入else兜底,输出”Please be clear: yes or no” 之后进入下一次循环。

之后的if就不再是yes过程的内容了,则是调用yes后对其返回值进行一些处理。下面是原文的翻译

这个例子展示了一个名为yes的过程,它向用户询问一个问题,如果他们回答“是”(或类似的东西)就返回true,如果他们回答“否”(或类似的东西)就返回false。return语句立即离开过程(因此也离开while循环)。(question: string): bool语法描述了过程期望一个名为question的字符串类型的参数,并返回bool类型的值。bool类型是内置的:bool的唯一有效值是true和false。if或while语句中的条件必须是bool类型。

一些术语:在示例中,问题被称为(形式)参数,“我应该…”被称为传递给该参数的实参。

结果变量

返回值的过程声明了一个表示返回值的隐式结果变量。没有表达式的返回语句是返回结果的简写。如果在退出处没有返回语句,则总是在过程结束时自动返回结果值。

1
2
3
4
5
6
7
8
9
proc sumTillNegative(x: varargs[int]): int =
for i in x:
if i < 0:
return
result = result + i

echo sumTillNegative() # echoes 0
echo sumTillNegative(3, 4, 5) # echoes 12
echo sumTillNegative(3, 4 , -1 , 6) # echoes 7

结果变量已经在函数的开头隐式地声明了,因此再次用’var result’声明它,例如,将使用同名的普通变量来遮蔽它。结果变量也已经用类型的默认值初始化了。注意,在过程开始时,引用数据类型将为nil,因此可能需要手动初始化。

没有任何返回语句且不使用特殊结果变量的过程返回其最后一个表达式的值。例如,这个程序

1
2
proc helloWorld(): string =
"Hello, World!"

结果就是返回一个”Hello, World”。

参数

过程体中的参数是不可变的。默认情况下,它们的值不能更改,因为这允许编译器以最有效的方式实现参数传递。如果过程中需要一个可变变量,则必须在过程体中使用var声明它。隐藏参数名是可能的,这实际上是一种习惯用法:

1
2
3
4
proc printSeq(s: seq, nprinted: int = -1) =
var nprinted = if nprinted == -1: s.len else: min(nprinted, s.len)
for i in 0 ..< nprinted:
echo s[i]

如果过程需要修改调用者的参数,可以使用 var 参数(又是一个小细节):

1
2
3
4
5
6
7
8
9
proc divmod(a, b: int; res, remainder: var int) =
res = a div b # integer division
remainder = a mod b # integer modulo operation

var
x, y: int
divmod(8, 5, x, y) # modifies x and y
echo x
echo y

在示例中,resremaindervar 参数。过程可以修改 var 参数,并且调用者可以看到更改。请注意,上面的示例最好使用元组作为返回值,而不是使用 var 参数。

忽略语句

若要调用仅为其副作用返回值而忽略其返回值的过程,则必须使用discard语句。Nim不允许静默地丢弃返回值:

1
discard yes("May I ask a pointless question?")

如果调用的proc/迭代器已经被声明为可丢弃的pragma,则返回值可以被隐式忽略:

1
2
3
4
proc p(x, y: int): int {.discardable.} =
return x + y

p(3, 4) # now valid

命名参数

通常一个过程有许多参数,但并不清楚参数出现的顺序。对于构造复杂数据类型的过程尤其如此。因此,可以为过程的参数命名,以便清楚哪个参数属于哪个形参:

1
2
3
4
5
6
proc createWindow(x, y, width, height: int; title: string;
show: bool): Window =
# ...

var w = createWindow(show = true, title = "My Application",
x = 0, y = 0, height = 600, width = 800)

简单来说就是在调用过程传参数时候,可以加上参数名字来指定参数到底传给谁。很直接。

现在我们使用命名参数来调用createWindow,参数顺序不再重要了。将命名参数与有序参数混合也是可能的,但不是很好读:

1
2
var w = createWindow(0, 0, title = "My Application",
height = 600, width = 800, true)

编译器检查每个形参是否只接收一个实参。

默认参数值

为了使createWindow过程更容易使用,它应该提供默认值;如果调用者没有指定这些值,则将它们用作参数:

1
2
3
4
5
6
proc createWindow(x = 0, y = 0, width = 500, height = 700,
title = "unknown",
show = true): Window =


var w = createWindow(title = "My Application", height = 600, width = 800)

现在,对createWindow的调用只需要设置与默认值不同的值。

请注意,类型推断适用于具有默认值的参数;例如,不需要写title: string = “unknown”。

重载过程

Nim的过程重载和C++的函数重载很相似

1
2
3
4
5
6
7
8
9
10
11
12
13
proc toString(x: int): string =
result =
if x < 0: "negative"
elif x > 0: "positive"
else: "zero"

proc toString(x: bool): string =
result =
if x: "yep"
else: "nope"

assert toString(13) == "positive" # calls the toString(x: int) proc
assert toString(true) == "yep" # calls the toString(x: bool) proc

(注意,toString通常是Nim中的$操作符。)编译器为toString调用选择最合适的过程。这里不讨论重载解析算法是如何工作的——详细信息请参阅手册。歧义调用被报告为错误。

操作符

Nim标准库大量使用了重载——其中一个原因是像+这样的每个操作符都只是一个重载的过程。解析器允许您以中缀符号(a + b)或前缀符号(+ a)使用操作符。中缀操作符总是接收两个参数,前缀操作符总是接收一个参数。(后缀操作符是不可能的,因为这将是模棱两可的:a@ @b是表示(a) @ (@b)还是(a@) @ (b)?它总是表示(a) @ (@b),因为在Nim中没有后缀操作符。

除了一些内置的关键字操作符,如and, or, not,操作符总是由以下字符组成:+ - * \ / < > = @ $ ~ & % !? ^。|

允许使用用户定义的操作符。没有什么能阻止你定义自己的@!?+~操作符,但这样做可能会降低可读性。

操作符的优先级由它的第一个字符决定。详细信息可在手册中找到。

定义new操作符时,将操作符用反引号” ‘ “括起来:

1
2
3
proc `$` (x: myDataType): string = 
# now the $ operator also works with myDataType, overloading resolution
# ensures that $ works for built-in types just like before

前置申明

每个变量、过程等在使用之前都需要声明。(这样做的原因是,在像Nim这样广泛支持元编程的语言中避免这种需求是很重要的。)然而,对于相互递归的过程不能这样做:

1
2
# forward declaration:
proc even(n: int): bool
1
2
3
4
5
6
7
8
9
10
11
proc odd(n: int): bool =
assert(n >= 0) # makes sure we don't run into negative recursion
if n == 0: false
else:
n == 1 or even(n-1)

proc even(n: int): bool =
assert(n >= 0) # makes sure we don't run into negative recursion
if n == 1: false
else:
n == 0 or odd(n-1)

这里奇数取决于偶数,反之亦然。因此,甚至需要在编译器完全定义它之前引入它。这种前向声明的语法很简单:只需省略=和过程体即可。assert只是添加边界条件,稍后将在模块一节中介绍。

该语言的后续版本将削弱对前向声明的要求。

该示例还显示了进程的主体可以由单个表达式组成,然后隐式返回该表达式的值。

函数和方法

正如在介绍中提到的,Nim区分了过程、函数和方法,它们分别由proc、func和method关键字定义。在某些方面,Nim的定义比其他语言更迂腐。

函数更接近于纯数学函数的概念,如果你曾经做过函数式编程,你可能会对它很熟悉。本质上,它们是带有附加限制的过程:它们不能访问全局状态(除了const),也不能产生副作用。func关键字基本上是带有{. nosideeffects .}标记的proc的别名。然而,函数仍然可以更改其可变参数,即那些标记为var的参数,以及任何ref对象。

与过程不同,方法是动态分派的。这听起来有点复杂,但它是一个与继承和面向对象编程密切相关的概念。如果您重载一个过程(两个具有相同名称但不同类型或具有不同参数集的过程被称为重载),要使用的过程将在编译时确定。另一方面,方法依赖于从RootObj继承的对象。这将在本教程的第二部分进行更深入的讨论。


Nim-过程(procedure)
http://cvrain.cloudvl.cn/2023/11/25/Nim/nim-procedure/
作者
ClaudeRainer
发布于
2023年11月25日
许可协议