Nim Manual Study Notes

Nim 是一种静态类型的、编译型、系统编程语言。它结合了其他成熟语言的成功概念。(如 Python、Ada 和 Modula)

词法元素

Nim 词法元素由(字符串)字面值、标识符、关键字、注释、操作符、和其它标点符号构成。

字符串和字符字面量

字符串字面值通过双引号括起来;字符字面值用单引号。特殊字符通过 \ 转义: \n 表示换行, \t 表示制表符等.

r"C:\program files\nim" # 原始字符串字面量,\ 不转义
'''hello\world''' # 长字符串字面量,\ 不转义

注释

  • 单行注释:#、文档注释 ##

  • 多行注释:#[....]#discard '''xxxx'''

数字

  • 下划线:1_000_000 一百万
  • 浮点字面值:1.0e9 十亿
  • 进制:0b 0o 0x

var 语句

var x, y: int # 声明 x 和 y 拥有类型 ``int``
var
  x, y: int
  # 可以有注释
  a, b, c: string

赋值语句

var x = "abc" # 引入一个新变量 `x` 并且赋值给它
x = "xyz"     # 赋新值给 `x`
var x, y = 3  # 给变量 `x` 和 `y` 赋值 3

常量

const x = "abc"
const
  x = 1
  # 这也可以有注释
  y = 2
  z = y + 5 # 计算是可能的

let 语句

let 语句像 var 语句一样但声明的符号是 单赋值 变量:初始化后它们的值将不能改变。

let x = "abc" # 引入一个新变量 `x` 并绑定一个值
x = "xyz"     # 非法: 给 `x` 赋值

let 和 const 的区别在于: let 引入一个变量不能重新赋值。 const 表示 强制编译期求值并放入数据段:

const input = readLine(stdin) # 错误: 需要常量表达式
let input = readLine(stdin)   # 可以

流程控制语句

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, "!"

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 语句可以处理整型、其它序数类型和字符串。对整型或序数类型值,也可以用范围:

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"
else: discard # 空 discard 语句 => 什么都不做

While 语句

echo "What's your name? "
var name = readLine(stdin)
while name == "":
  echo "Please tell me your name: "
  name = readLine(stdin)

For 语句

echo "Counting to ten: "
for i in countup(1, 10): # 倒数  countdown(10, 1)
  echo i
# --> Outputs 1 2 3 4 5 6 7 8 9 10 on different lines
for i in 1..10: # 1..10
for i in 0..<10: # 零索引计数 0..9 -> for i in 0..<s.len
for index, item in ["a", "b"].pairs:
  echo item, " at index ", index
# => a at index 0
# => b at index 1

作用域和块语句

while false:
  var x = "hi"
echo x # 不行
block myblock:
  var x = "hi"
echo x # 不行

Break 语句

block myblock:
  echo "entering block"
  while true:
    echo "looping"
    break # 跳出循环,但不跳出块
  echo "still in block"

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

Continue 语句

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

When 语句

when 语句几乎等价于 if 语句, 但有以下区别:

  • 每个条件必须是常量表达式,因为它被编译器求值。
  • 分支内的语句不打开新作用域。
  • 编译器检查语义并仅为属于第一个求值为 true 的条件生成代码。

when 语句在写平台特定代码时有用,类似于 C 语言中的 #ifdef 结构。

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"

语句和缩进

简单语句不必缩进(不能包含其它语句:属于简单语句的赋值, 过程调用或 return 语句),复杂语句必须缩进,避免歧义。

# 单个赋值语句不需要缩进:
if x: x = false

# 嵌套 if 语句需要缩进:
if x:
  if y:
    y = false
  else:
    y = true

# 需要缩进, 因为条件后有两个语句:
if x:
  x = false
  y = false

表达式为了更好的可读性可以在某些地方缩进:

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

用小括号和分号 (😉 可以在只允许表达式的地方使用语句:

const fac4 = (var x = 1; for i in 1..4: x *= i; x)

过程(procedure)

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."

Result 变量

一个返回值的过程有一个隐式 result 变量声明代表返回值。一个没有表达式的 return 语句是 return result 的简写。 result 总在过程的结尾自动返回如果退出时没有 return 语句.

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

形参

形参在过程体中不可改变,这允许编译器以最高效的方式实现参数传递。如果在一个过程内需要可以改变的变量,它必须在过程体中用 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 参数:

proc divmod(a, b: int; res, remainder: var int) =
  res = a div b        # 整除
  remainder = a mod b  # 整数取模操作

var
  x, y: int
divmod(8, 5, x, y) # 修改 x 和 y
echo x
echo y

Discard 语句

Nim 不允许静默地丢弃一个返回值,通过 discard 指定即可:

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

返回类型可以被隐式地忽略如果调用的方法、迭代器已经用 discardable pragma 声明过:

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

p(3, 4) # now valid

命名参数

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)

默认值

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)

重载过程

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
echo toString(true) # calls the toString(x: bool) proc

操作符

proc `$` (x: myDataType): string = ...
# 现在$操作符对myDataType生效,重载解析确保$对内置类型像之前一样工作。

前向声明

proc even(n: int): bool # 前向声明

proc odd(n: int): bool =
  assert(n >= 0) # 确保我们没有遇到负递归
  if n == 0: false
  else:
    n == 1 or even(n-1)

proc even(n: int): bool =
  assert(n >= 0) # 确保我们没有遇到负递归
  if n == 1: false
  else:
    n == 0 or odd(n-1)

迭代器

iterator countup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

迭代器看起来像过程,但有几点重要的差异:

  • 迭代器只能从循环中调用。
  • 迭代器不能包含 return 语句(过程不能包含 yield 语句)。
  • 迭代器没有隐式 result 变量。
  • 迭代器不支持递归。
  • 迭代器不能前向声明,因为编译器必须能够内联迭代器。(这个限制将在编译器的未来版本中消失。)

基本类型

布尔值

bool: true / flase,操作符 not, and, or, xor, <, <=, >, >=, !=, ==

字符

char: 1 byte, ord(char) -> int, chr(int) -> char, $char -> string.

字符串

字符串变量(string)可改变,Nim 中字符串有长度限制,以零结尾。获取字符串长度 .len,字符串赋值产生拷贝,拼接字符串可使用 &add 进行追加。

整型

Nim有以下内置整型:

int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64

默认整型是 int 。整型字面值可以用 类型前缀 来指定一个非默认整数类型:

let
  x = 0     # x 是 ``int``
  y = 0'i8  # y 是 ``int8``
  z = 0'i64 # z 是 ``int64``
  u = 0'u   # u 是 ``uint``

多数常用整数用来计数内存中的对象,所以 int 和指针具有相同的大小。

整数支持通用操作符 + - * div mod < <= == != > >= 。 也支持 and or xor not 操作符,并提供按位操作。 左移用 shl ,右移用 shr 。位移操作符实参总是被当作无符号整型。普通乘法或除法可以做算术位移。

浮点

Nim 有这些内置浮点类型:float float32 float64。默认浮点类型是float。在当前的实现, float 是 64 位。

var
  x = 0.0      # x 是 ``float``
  y = 0.0'f32  # y 是 ``float32``
  z = 0.0'f64  # z 是 ``float64``

自动类型转换在表达式中使用不同类型时执行:短类型转换为长类型。整数类型不会自动转换为浮点类型,反之亦然。使用 toInttoFloat 过程来转换。

var
  x: int32 = 1.int32   # 与调用 int32(1) 相同
  y: int8  = int8('a') # 'a' == 97'i8
  z: float = 2.5       # int(2.5) 向下取整为 2
  sum: int = int(x) + int(y) + int(z) # sum == 100

内部类型表示

var
  myBool = true
  myCharacter = 'n'
  myString = "nim"
  myInteger = 42
  myFloat = 3.14
echo myBool, ":", repr(myBool)
# --> true:true
echo myCharacter, ":", repr(myCharacter)
# --> n:'n'
echo myString, ":", repr(myString)
# --> nim:0x10fa8c050"nim"
echo myInteger, ":", repr(myInteger)
# --> 42:42
echo myFloat, ":", repr(myFloat)
# --> 3.1400000000000001e+00:3.1400000000000001e+00

高级类型

在 Nim 中新类型可以在 type 语句里定义:

type
  biggestInt = int64      # 可用的最大整数类型
  biggestFloat = float64  # 可用的最大浮点类型

枚举

枚举类型的变量只能赋值为枚举指定的值。这些值是有序符号的集合。每个符号映射到内部的一个整数类型。第一个符号用运行时的 0 表示,第二个用 1,以此类推。例如:

type
  Direction = enum
    north, east, south, west

var x = south     # `x`是`Direction`; 值是`south`
echo x            # 向标准输出写"south"

枚举的符号可以被限定以避免歧义:Direction.south$ 操作符可以将任何枚举值转换为它的名字, ord 过程可以转换为它底层的整数类型。

序数类型

枚举、整型、 char、 bool(和子范围)叫做序数类型。序数类型有一些特殊操作:

Operation Comment
ord(x) 返回表示 x 的整数值
inc(x) x 递增1
inc(x, n) x 递增 n; n 是整数
dec(x) x 递减1
dec(x, n) x 递减 n; n 是整数
succ(x) 返回 x 的下一个值
succ(x, n) 返回 x 后的第n个值
pred(x) 返回 x 的前一个值
pred(x, n) 返回 x 前的第n个值

inc, dec, succ 和 pred 操作通过抛出 EOutOfRange 或 EOverflow 异常而失败。

子范围

一个子范围是一个整型或枚举类型值(基本类型)的范围。例如:

type
  MySubrange = range[0..5]

MySubrange 是只包含 0 到 5 的 int 范围。赋任何其它值给 MySubrange 类型的变量是编译期或运行时错误。允许给子范围赋值它的基类型,反之亦然。

集合类型

集合的基类型只能是固定大小的序数类型,它们是:

  • int8-int16
  • uint8/byte-uint16
  • char
  • enum
type
  CharSet = set[char]
var
  x: CharSet
x = {'a'..'z', '0'..'9'} # 构造一个包含'a'到'z'和'0'到'9'的集合
操作符 含义
A + B 并集
A * B 交集
A - B 差集
A == B 相等
A <= B 子集
A < B 真子集
e in A 元素
e notin A A不包含元素e
contains(A, e) 包含元素e
card(A) A的基 (集合A中的元素数量)
incl(A, elem) 同 A = A + {elem}
excl(A, elem) 同 A = A - {elem}

位字段

type
  MyFlag* {.size: sizeof(cint).} = enum # 枚举
    A
    B
    C
    D
  MyFlags = set[MyFlag] # 集合

proc toNum(f: MyFlags): int = cast[cint](f) # 强转
proc toFlags(v: int): MyFlags = cast[MyFlags](v)

assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}

数组

type
  IntArray = array[0..5, int] # 一个索引为 0..5 的数​组
var
  x: IntArray
x = [1, 2, 3, 4, 5, 6]
for i in low(x)..high(x): # 最小/大索引
  echo x[i]
type
  Direction = enum
    north, east, south, west
  BlinkLights = enum
    off, on, slowBlink, mediumBlink, fastBlink
  LevelSetting = array[north..west, BlinkLights]
var
  level: LevelSetting
level[north] = on
level[south] = slowBlink
level[east] = fastBlink
echo repr(level)  # --> [on, fastBlink, slowBlink, off]
echo low(level)   # --> north
echo len(level)   # --> 4
echo high(level)  # --> west
type
  LightTower = array[1..10, array[north..west, BlinkLights]]
type
  IntArray = array[0..5, int] # 一个索引为0..5的数​组
  QuickArray = array[6, int]  # 一个索引为0..5的数​组
var
  x: IntArray
  y: QuickArray
x = [1, 2, 3, 4, 5, 6]
y = x
for i in low(x)..high(x):
  echo x[i], y[i]

序列

序列类似数组但是动态长度,可以在运行时改变。因为序列是大小可变的它们总是分配在堆上,被垃圾回收。

序列总是以从零开始的 int 类型索引。 len , low 和 high 操作符也可用于序列。 x[i] 标记可以用于访问 x 的第i个元素。

序列可以用数组构造器 [] 数组到序列操作符 @ 构成。另一个为序列分配空间的方法是调用内置 newSeq 过程。

var
  x: seq[int] # 整数序列引用
x = @[1, 2, 3, 4, 5, 6] # @ 把数组转成分配在堆上的序列
for value in @[3, 4, 5]:
  echo value
# --> 3
# --> 4
# --> 5

for i, value in @[3, 4, 5]:
  echo "index: ", $i, ", value:", $value
# --> index: 0, value:3
# --> index: 1, value:4
# --> index: 2, value:5

开放数组

注意: 开放数组只用于形参。

var
  fruits:   seq[string]       # 字符串序列用 '@[]' 初始化
  capitals: array[3, string]  # 固定大小的字符串数组

capitals = ["New York", "London", "Berlin"]   # 数组 'capitals' 允许只有三个元素的赋值
fruits.add("Banana")          # 序列 'fruits' 在运行时动态扩展
fruits.add("Mango")

proc openArraySize(oa: openArray[string]): int =
  oa.len

assert openArraySize(fruits) == 2     # 过程接受一个序列作为形参
assert openArraySize(capitals) == 3   # 也可以是一个数组

可变参数

varargs 参数像开放数组形参。 它也表示实现传递数量可变的实参给过程。编译器将实参列表自动转换为数组:

proc myWriteln(f: File, a: varargs[string]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, "abc", "def", "xyz")
# 编译器转为:
myWriteln(stdout, ["abc", "def", "xyz"])

转换只在可变形参是过程头部的最后一个形参时完成。它也可以在这个情景执行类型转换:

proc myWriteln(f: File, a: varargs[string, `$`]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, 123, "abc", 4.0)
# 编译器转为:
myWriteln(stdout, [$123, $"abc", $4.0])

在示例中 $ 适用于任何传递给形参 a 的实参。注意 $ 适用于空字符串指令。

切片

var
  a = "Nim is a progamming language"
  b = "Slices are useless."

echo a[7..12] # --> 'a prog'
b[11..^2] = "useful"
echo b # --> 'Slices are useful.'
"Slices are useless."
 |          |     |
 0         11    17   使用索引
^19        ^8    ^2   使用^

b[0..^1] == b[0..b.len-1] == b[0..<b.len] -> ^1 <=> b.len-1

对象

type
  Person = object
    name: string
    age: int

var person1 = Person(name: "Peter", age: 30)

echo person1.name # "Peter"
echo person1.age  # 30

var person2 = person1 # 复制person 1

person2.age += 14

echo person1.age # 30
echo person2.age # 44


# 顺序可以改变
let person3 = Person(age: 12, name: "Quentin")

# 不需要指定每个成员
let person4 = Person(age: 3)
# 未指定的成员将用默认值初始化。本例中它是一个空字符串。
doAssert person4.name == ""

在定义的模块外可见的对象字段需要加上 * :

type
  Person* = object # 其它模块可见
    name*: string  # 这个类型的字段在其它模块可见
    age*: int

元组

type
  # 类型表示一个人:
  # 一个人有名字和年龄。
  Person = tuple
    name: string
    age: int
  
  # 等价类型的语法。
  PersonX = tuple[name: string, age: int]
  
  # 匿名字段语法
  PersonY = (string, int)

var
  person: Person
  personX: PersonX
  personY: PersonY

person = (name: "Peter", age: 30)
# Person和PersonX等价
personX = person

# 用匿名字段创建一个元组:
personY = ("Peter", 30)

# 有匿名字段元组兼容有字段名元组。
person = personY
personY = person

# 通常用于短元组初始化语法
person = ("Peter", 30)

echo person.name # "Peter"
echo person.age  # 30

echo person[0] # "Peter"
echo person[1] # 30

# 你不需要在一个独立类型段中声明元组。
var building: tuple[street: string, number: int]
building = ("Rue del Percebe", 13)
echo building.street

# 下面的行不能编译,它们是不同的元组。
#person = building
# --> Error: type mismatch: got (tuple[street: string, number: int])
#     but expected 'Person'

元组只有在变量赋值期间可以解包:

import os

let
  path = "usr/local/nimc.html"
  (dir, name, ext) = splitFile(path)
  baddir, badname, badext = splitFile(path)
echo dir      # 输出 `usr/local`
echo name     # 输出 `nimc`
echo ext      # 输出 `.html`
# 下面输出同样的行:
# `(dir: usr/local, name: nimc, ext: .html)`
echo baddir
echo badname
echo badext

引用和指针类型

Nim 区分 被追踪未追踪 引用。未追踪引用也被称为 指针 。追踪的引用指向垃圾回收堆里的对象,未追踪引用指向手动分配对象或内存中其它地方的对象。因此未追踪引用是 不安全的。 为了某些低级的操作(例如,访问硬件),未追踪的引用是必须的。

追踪的引用用 ref 关键字声明;未追踪引用用 ptr 关键字声明。

[] 下标标记可以用来解引用一个引用,表示获取引用指向的内容。.(访问一个元组/对象字段操作符)和 [] (数组/字符串/序列索引操作符)操作符为引用类型执行隐式解引用操作:

type
  Node = ref object
    le, ri: Node
    data: int
var
  n: Node
new(n)
n.data = 9
# 不必写n[].data; 实际上n[].data是不提倡的!

为了分配一个新追踪的对象,必须使用内置过程 new 。 为了处理未追踪内存, 可以用 alloc, dealloc 和 realloc,如果一个引用指向 nothing, 它的值是 nil

过程类型

过程类型是指向过程的指针。 nil 是过程类型变量允许的值。Nim 使用过程类型达到 函数式编程技术。

proc echoItem(x: int) = echo x

proc forEach(action: proc (x: int)) =
  const
    data = [2, 3, 5, 7, 11]
  for d in items(data):
    action(d)

forEach(echoItem)

模块

Nim 支持用模块的概念把一个程序拆分成片段。每个模块在它自己的文件里。模块实现了 信息隐藏和编译隔离 。一个模块可以通过 import 语句访问另一个模块符号。 只有标记了星号(*)的顶级符号被导出:

# Module A
var
  x*, y: int

proc `*` *(a, b: seq[int]): seq[int] =
  # 分配新序列:
  newSeq(result, len(a))
  # 两个序列相乘:
  for i in 0..len(a)-1: result[i] = a[i] * b[i]

when isMainModule:
  # 测试序列乘 ``*`` :
  assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9])

模块限定:

# Module A
var x*: string

# Module B
var x*: int

# Module C
import A, B
write(stdout, x) # error: x 有歧义
write(stdout, A.x) # okay: 用了限定

var x = 4
write(stdout, x) # 没有歧义: 使用模块 C 的 x

排除符号

import mymodule except y

From 语句

from mymodule import x, y, z # --> x()
from mymodule as m import nil # --> m.x()

Include 模块

include 语句和导入一个模块做不同的基础工作:它只包含一个文件的内容。 include 语句在把一个大模块拆分为几个文件时有用:

include fileA, fileB, fileC