Nim教程翻译(一)

原文链接:http://nim-lang.org/docs/tut1.html

Nim是一种静态类型、命令式的系统级编程语言,其作者是Andreas Rumpf,最新版本是0.11.0。Nim的语法受到了Python和Pascal的启发,其主要特性包括编译到C的原生代码生成,不依赖于虚拟机;非跟踪垃圾收集器;跨模块内联;编译器和标准库都用Nim实现;强大的元编程能力,等等。


版本:0.11.2作者:Andreas Rumpf

介绍

"Der Mensch ist doch ein Augentier -- schöne Dinge wünsch ich mir."

 “人是视觉动物--我渴望美好事物。”   

这个文档是关于编程语言Nim的教程,这个教程假设你已经熟悉基本的编程语言概念,比如:基本变量,基本类型或者非常基本的语句。nim手册(nim:manual)中包含更多具有高级语言特性的例子。nim手册链接:http://nim-lang.org/docs/manual.html

第一个程序

我们从一个改进的“hello world”程序开始我们的旅程  

# This is a comment
echo("What's your name? ")
var name: string = readLine(stdin)
echo("Hi, ", name, "!")


把这个代码保存为“greetings.nim”文件,现在编译,运行它:

nim compile --run greetings.nim

带有--run开关选项,Nim会在编译之后自动执行源文件。你可以在文件名后面附加程序命令行参数:

nim compile --run greetings.nim arg1 arg2


常用的命令和开关有缩写,所以你也可以使用下面的缩写形式:


nim c -r greetings.nim


编译一个发行版本使用:

nim c -d:release greetings.nim


默认情况下,nim编译器针对你的调试喜好生成大量运行时检查。使用-d:release这些检查被关闭,优化会被打开。

虽然程序是干什么的应该是很明显的,我将解释这个语法:当程序启动的时候执行没有缩进的程序。缩进是nim分组声明的方法。缩进只能使用空格,制表符是不允许的。

字符串字面值使用双引号括起。var语句声明一个新变量,命名字符串类型的名称带有一个通过readLine过程返回的值。由于编译器知道readLine返回一个字符串,你可以省略类型声明(这叫做本地类型推理)

var name = readLine(stdin)


这基本上是nim中存在的类型推断的唯一形式:它在简洁和可读性之间是一个很好的妥协。

“hello world”程序中包含的几个标识符已经被编译器熟知:echo,readLine等。这些在系统模块中内置的声明暗含的被导入到其他的模块。

词法元素

让我们更详细的看看nim的词法元素:像其他编程语言一样,Nim包含(字符串)字面值,标识符,关键字,注释,运算符,和其他标点符号

字符串和字符字面值

字符串字面值用双引号,字符字面值用单引号。特殊转移字符用\:\n代表换行,\t制表符,跳格键等。这也有原始字符串字面值:

r"C:\program files\nim"


在原始字符串字面值中反斜杠符号不是一个转移字符

长字符串用三引号"""...""";它们可以跨越多行,并且\也不是转义字符。他们是非常有用的,例如嵌入HTML代码模板。

注释

注释可以在一个字符串或字符外的任何地方开始,以字符#开始。文档注释以##开始: 

# A comment.

var myVariable: int ## a documentation comment


文档注释是标记;文档注释只允许在输入文件的某些地方,因为它们属于语法树。这个特征可以用于简单文档生成。

你也可以用discard语句和长字符串来创建注释块:

discard """ You can have any Nim code text commented
out inside this with no indentation restrictions.
      yes("May I ask a pointless question?") """

数值

数值常量在很多语言中都存在;作为一个特殊的扭曲,强调允许更好的可读性,1_000_000 (一百万)。一个数带有点(或者e或E)的是一个float常数。十六进制常量以0x开始,二进制以0b开始,八进制以0o开始。一个单独的前导0不是八进制数。

var语句

var语句声明一个局部或全局变量

var x, y: int # declares x and y to have the type ``int``


缩进可以用在var关键字之后列出一整段变量:

var
  x, y: int
  # a comment can occur here too
  a, b, c: string

赋值语句

复制语句将一个新值赋值给一个变量或者一个更普遍的存储空间

var x = "abc" # introduces a new variable `x` and assigns a value to it
x = "xyz"     # assigns a new value to `x`


=是赋值操作符。赋值操作不能被重载,重写,或隐藏,但这可能会在nim的未来版本中发生变化。你可以用一个赋值语句来声明多个变量,所有的变量都将具有相同的值。

var x, y = 3  # assigns 3 to the variables `x` and `y`
echo "x ", x  # outputs "x 3"
echo "y ", y  # outputs "y 3"
x = 42        # changes `x` to 42 without changing `y`
echo "x ", x  # outputs "x 42"
echo "y ", y  # outputs "y 3"


注意:用一个赋值语句声明多个变量,调用一个过程会有出乎意料的结果:编译器将展开赋值语句,以调用这个程序多次结束。如果这个过程的结果取决于副作用,你的变量可能以不同的值结束。为了安全,仅仅使用常量。

常量

常量是绑定一个值的符号。这个常量值不能改变。编译器必须能够在编译时在一个常量的声明里评估表达式。

const x = "abc" # the constant x contains the string "abc"

缩进可以用在const关键字之后列出一整段常数:

const
  x = 1
  # a comment can occur here too
  y = 2
  z = y + 5 # computations are possible

let语句

let语句的作用与var语句相同,但是这个声明的符号只能单变量赋值:在初始化之后值不能改变:

let x = "abc" # introduces a new variable `x` and binds a value to it
x = "xyz"     # Illegal: assignment to `x`


let语句和const语句的不同在于:let声明一个变量不能重新分配值,const意味着执行编译时评估和把它变成一个数据段

const input = readLine(stdin) # Error: constant expression expected
let input = readLine(stdin)   # works

控制流语句

第一个greeting.nim程序包含3个语句,它们顺序执行。只有最原始的程序可以逃脱:分支和循环同样需要

if语句

if语句是控制流的一个分支:

let name = readLine(stdin)
if name == "":
  echo("Poor soul, you lost your name?")
elif name == "name":
  echo("Very funny, your name is name.")
else:
  echo("Hi, ", name, "!")


这可以有0个或多个elif部分,else部分也是可以选择的。elif关键字比else if短和有效避免过多的缩进

case语句

case语句提供另一种分支方式,一个case语句是一个多分支

let name = readLine(stdin)
case name
of "":
  echo("Poor soul, you lost your name?")
of "name":
  echo("Very funny, your name is name.")
of "Dave", "Frank":
  echo("Cool name!")
else:
  echo("Hi, ", name, "!")


正如上面所见,对于一个分支以逗号分割的值列表是允许的

case语句可以处理整数,字符串和其他序列类型。(什么是一个序数类型将稍后解释)对于整数或者其他序数类型值的范围也是可能的:

# this statement will be explained later:
from strutils import parseInt

echo("A number please: ")
let n = parseInt(readLine(stdin))
case n
of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}")
of 3, 8: echo("The number is 3 or 8")


然而,上面的代码不能编译:原因是你必须覆盖n可能包含的每一个值,但是这个代码仅处理了值0...8。因为它列出所有可能的值是很不实际的(尽管这个可能的由于范围符号),我们解决这个通过告诉编译器对于其他的每个值什么都不应该做:


...
case n
of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}")
of 3, 8: echo("The number is 3 or 8")
else: discard


这个空废弃语句是什么也不做的声明。编译器知道一个case语句带有一个else部分不会失败,因此错误消失了。注意,要涵盖所有可能的字符串值是不可能的,这就是为什么字符串case总是需要一个else分支

在一般情况下case语句用于子界类型或枚举是非常有用的,编译器将检查所有可能的值。

while语句

while语句是一个简单的循环构造:

echo("What's your name? ")
var name = readLine(stdin)
while name == "":
  echo("Please tell me your name: ")
  name = readLine(stdin)
  # no ``var``, because we do not declare a new variable here

这个例子用一个while循环一直询问用户的姓名,只要他什么都没有输入(只按RETURN)

for语句

for语句是一个结构去遍历一个构造器提供的所有值。该示例使用内置countup迭代器:

echo("Counting to ten: ")
for i in countup(1, 10):
  echo($i)
# --> Outputs 1 2 3 4 5 6 7 8 9 10 on different lines


内置的$操作符将一个整形(int)和许多其他的类型转化为一个字符串类型。变量i通过循环隐式的声明为int,因为那是迭代器countup返回的结果。i的值是1-10,每个值都被echo-ed上面的代码也可以这样写:

echo("Counting to 10: ")
var i = 1
while i <= 10:
  echo($i)
  inc(i) # increment i by 1
# --> Outputs 1 2 3 4 5 6 7 8 9 10 on different lines

countdown可以很容易的被实现(但是它很少使用)

echo("Counting down from 10 to 1: ")
for i in countdown(10, 1):
  echo($i)
# --> Outputs 10 9 8 7 6 5 4 3 2 1 on different lines

由于countup在程序中经常出现。nim也有一个迭代器功能相同:

for i in 1..10:
  ...


作用域和块语句

控制流语句有一个特性没有包括:他们打开一个新的范围。这意味着,在下面的例子中,x在循环外是不可访问的:

while false:
  var x = "hi"
echo(x) # does not work

一个while或者for语句声明了一个隐含的块。标识符只在声明它的块中可以访问。一个块语句可以用来打开一个新的块语句。

block myblock:
  var x = "hi"
echo(x) # does not work either

块标签(例子中的myblock)是可选择的。

break语句

break语句可以提前跳出一个块。break可以跳出while,for循环以及块语句。break是跳出最深处的结构,除非给出了一个块的标签。

block myblock:
  echo("entering block")
  while true:
    echo("looping")
    break # leaves the loop, but not the block  跳出循环,但不跳出块
  echo("still in block")

block myblock2:
  echo("entering block")
  while true:
    echo("looping")
    break myblock2 # leaves the block (and the loop)  跳出块(和循环)
  echo("still in block")

continue语句

就像在其他很多编程语言中,continue语句直接进入下一次迭代:

while true:
  let x = readLine(stdin)
  if x == "": continue
  echo(x)

when语句

例如:

when system.hostOS == "windows":
  echo("running on Windows!")
elif system.hostOS == "linux":
  echo("running on Linux!")
elif system.hostOS == "macosx":
  echo("running on Mac OS X!")
else:
  echo("unknown operating system")

when语句与if语句是大致相同的,以下是一些不同的地方:

  • 每一个条件都必须是一个常量表达式,因为它要通过编译器评估
  • 在一个分支语句中不打开一个新的范围
  • 只有第一条件评估为true时编译器才会检查语句的语义和生成代码。

when语句对于写特定平台的代码很有用,类似于c编程语言中的#ifdef构造。

注意:要注释一大块代码,通常使用when false:statement比用真正的注释要好。这种方式的嵌套是可能的。

语句和缩进

现在我们学习了基本的控制流语句,让我们重新回到nim的缩进规则

在nim中,简单语句与复杂语句是有区别的。简单语句不能包含其他语句,赋值,过程调用,return语句都属于简单语句。if,when,for,while都属于复杂语句,复杂语句可以包含其他语句。为了避免歧义,复杂语句必须有缩进,单一的简单语句没有缩进。

# no indentation needed for single assignment statement:  单个的赋值语句不需要缩进
if x: x = false

# indentation needed for nested if statement:             对于嵌套的if语句需要缩进
if x:
  if y:
    y = false
  else:
    y = true

# indentation needed, because two statements follow the condition:  需要缩进,因为条件后面有两个语句
if x:
  x = false
  y = false

表达式是一个语句的一部分,它通常返回一个值。例如if语句的条件就是表达式的一个例子。为了更好的可读性表达式可以在特定的地方包含缩进。

if thisIsaLongCondition() and
    thisIsAnotherLongCondition(1,
       2, 3, 4):
  x = true

作为一个经典法则。在表达式中操作符,开括号,逗号之后的缩进是允许的。

用圆括号和分号(;),你可以用只有一个表达式的语句是允许的。

# computes fac(4) at compile time:
const fac4 = (var x = 1; for i in 1..4: x *= i; x)

过程

为了在例子中定义新的命令如echo和readLine,需要过程的概念。(有些语言中叫做方法或者函数)。在nim中新过程用proc关键字定义:

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的过程,它问了用户一个问题,如果他回答“yes”(或者其他相似的答案)返回true,如果他回答“no”(或者其他相似的答案)返回false。一个return语句直接离开过程(和while循环)。这个(question: string): bool语法描述的是这个过程有一个名字为question的参数,参数类型为字符串类型,返回一个bool类型的值。bool类型是一个内置类型:bool类型唯一有效的值是true和false。if或者while语句中的条件语句就应该为bool类型。

一些专业术语:在例子中question叫做(正式)参数,"Should I..."叫做实参传递给这个形式参数

结果变量

一个过程返回一个值有一个隐式的结果变量声明,它代表返回值。一个没有表达式的返回语句是一个返回结果的简称。结果值总是自动返回在一个过程的末尾如果在出口没有返回语句。

proc sumTillNegative(x: varargs[int]): int =
  for i in x:
    if i < 0:
      return
    result = result + i

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

result变量已经隐式的声明在函数开始处,所以再次声明,例如:'var result',将会存在一个与result相同名字的正常变量。result变量也已经初始化为类型默认值。注意:引用数据类型将会是零在过程开始处,因此可能需要手动初始化。

参数

在过程中参数是恒定的。默认情况下,它们的值不能改变因为这允许编译器以一种更有效的方式实现参数传递。如果在过程中需要一个易变的变量,它必须在过程中用var声明。与参数相同的名字是可以的,事实上这是一个惯用语法。

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参数:(就像其他语言,值调用是不会改变实参的值,要想改变实参的值要通过引用调用,nim中是在过程中将参数用var声明就可以实现调用参数的改变)

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)

在例子中,res和remainder是var参数。var参数可以通过过程修改,这个改变对于调用者是可见的。注意:上面的例子使用元组作为返回值而不是使用var参数作为返回值将会更好。

discard语句

调用一个带有返回值的过程仅仅是为了它的其他的作用,忽视它的返回值,一个discard语句必须使用。nim不允许默默地丢掉一个返回值:

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

返回值会隐式的忽略如果调用的过程或者迭代器已经声明discardable编译指示:

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

p(3, 4) # now valid

discard语句也可以在注释部分创建作为块注释的描述。

参数命名

一个过程通常有多个参数,而且参数出现的顺序并不是明确的。为一个过程构造了一个复杂的数据类型是非常正确的。因此一个过程的参数可以是命名的,以至于哪个实参属于哪个形参是明确的。

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过程的顺序参数已经没有问题了。命名参数和顺序参数的混合也是可以的,但是没有可读性:

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

编译器检查每个形参确切地接收一个实参。

默认值

为了使createWindow过程易于使用它应该提供默认值,这些值将作为实参值如果调用者没有明确的指定它们:

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++相似的重载过程的功能:

proc toString(x: int): string = ...
proc toString(x: bool): string =
  if x: result = "true"
  else: result = "false"

echo(toString(13))   # calls the toString(x: int) proc    调用toString(x: int)过程
echo(toString(true)) # calls the toString(x: bool) proc   调用toString(x: bool)过程


(注意在nim中toString通常是$操作符)对于toString的调用编译器会选择最合适的过程。这个重载解析算法具体是怎样工作的不在这里讨论(它将在手册中详述)。然而,它不会引起糟糕的意外,它是基于一个非常简单的统一算法。模糊不清的调用将会报错。

操作符

nim库大量使用重载--对于这中情况一个原因是每一个操作符比如+只是一个重载的过程。解析器允许你以中缀表示法或者前缀表示法使用操作符。一个中缀操作符总是接收两个参数,一个前缀操作符只有一个参数。后缀操作符是不可能的,因为那是模棱两可的。例如:a @ @ b 意味着 (a) @ (@b) 还是 (a@) @ (b)?它总是意味着(a) @ (@b),因为在nim中没有后缀操作符。

除了一些内置的关键字操作符例如:and,or,not,操作符通常包含这些字符: + - * \ / < > = @ $ ~ & % ! ? ^ . |

用户定义的操作符是允许的。没什么阻止你定义你自己@!?+~操作符,但是可读性会遭到破坏。

操作符的优先级是由它的第一个字符决定的。相关细节可以在手册中找到。

为了定义一个新的操作符,用反单引号括起操作符:

proc `$` (x: myDataType): string = ...
# now the $ operator also works with myDataType, overloading resolution
# ensures that $ works for built-in types just like before 现在$操作符也作用于myDataType,重载决议使得$操作符为内置类型工作。


"``"标记可以用来调用一个操作符就像调用其他过程。

if `==`( `+`(3, 4), 7): echo("True")

提前声明

每个变量,过程等,在它可以使用之前需要声明。(这是编译效率的原因)。然而,对于相互递归过程不需要提前声明。

# forward declaration:
proc even(n: int): bool

proc odd(n: int): bool =
  n == 1 or even(n-1)

proc even(n: int): bool =
  n == 0 or odd(n-1)


这里odd依赖于even,反之亦然。因此even需要在它完全定义之前介绍给编译器。对于这样一个提前声明的语法是简单的,仅仅省略=和过程的主题

以后的语言版本可能会移除提前声明的需要。

这个例子也展示了一个过程的主体可以包含一个单一的表达式,它的值隐式的返回。



更多内容请参看Nim教程翻译(二)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值