Nim 实战

on

1. 为什么是 Nim

1.1. 什么是 Nim

1.1.1. 使用场景

1.1.2. 核心功能

1.1.3. Nim 工作原理

1.2. Nim 的优缺点

1.2.1. 优点

1.2.2. Nim 仍需要改进的地方

1.3. 总结

2. 入门

在本章中,你将了解 Nim 的语法,过程,for循环以及该语言的其他基本方面。 在本章中,我们将涵盖许多信息,以使你对语言有更广泛的了解。 在开始之前,请确保已安装 Nim 并在计算机上正常运行。 你还需要一个文本编辑器来编辑Nim代码。 请参阅附录B,以获取有关如何安装Nim和其他相关工具的说明。

2.1. Nim 语法

编程语言的语法是一组规则,它们控制用该语言编写程序的方式。 在上一章中,你已经对Nim的语法有所了解。

大多数语言在语法方面都有许多相似之处。 对于以下情况尤其如此 C语言家族,碰巧也是最受欢迎的-如此之多 四种最受欢迎的编程语言在语法上

受C. Nim启发,其目标是提高可读性,因此通常使用关键字代替 标点符号。 因此,Nim的语法与C longage系列大不相同。 相反,它的大部分灵感来自Python和Pascal。

在本节中,我将教你Nim语法的基础知识。 学习语法是非常重要的第一步,因为它教会了你编写Nim代码的特定方式。

2.1.1. 关键字

大多数编程语言都有关键字的概念,Nim也不例外。关键字是在特定上下文中使用时具有特殊含义的单词。因此,你不能在源代码中使用关键字作为标识符。

踩踏你可以通过踩踏来解决此限制。请参阅第1.2节以了解更多信息。

从0.12.0版开始,Nim有70个关键字。这听起来可能很多,但是你必须记住你不会使用其中的大多数。其中一些还没有含义,保留给该语言的将来版本;其他则有较小的用例。

最常用的关键字允许你执行以下操作:

  • 指定条件分支:if,case,of和何时

  • 定义变量,过程和类型:var,let,proc,类型和对象

  • 处理代码中的运行时错误:try,except 和 final

你将在本章的下一部分中确切地了解这些关键字的含义以及如何使用它们。有关关键字的完整列表,请参阅 Nim 手册,网址为 http://nim-lang.org/docs/manual.html#lexical-analysis-identifiers-keywords

2.1.2. 缩进

许多程序员缩进他们的代码,以使程序的结构更明显。 在大多数编程语言中,这不是必需的,而仅仅是对人类代码的帮助。 在这些语言中,关键字和标点符号通常用于分隔代码块。 在Nim中,就像在Python中一样,使用缩进本身。

让我们看一个简单的例子来说明差异。 以下三个用C,Ruby和Nim编写的代码示例都做同样的事情。 但是请注意分隔代码块的不同方法。

if (42 >= 0) {
  printf("42 is greater than 0");
}
if 42 >= 0
  puts "42 is greater than 0"
end
if 42 >= 0:
  echo "42 is greater than 0"

如你所见,C使用大括号分隔代码块,Ruby使用关键字end,Nim使用缩进。 Nim还在缩进开始之前的行上使用冒号字符。 这对于if语句和其他许多语句都是必需的。 但是,当你继续学习Nim时,你会发现并不是所有以缩进代码块开头的语句都必须使用冒号。

还要注意清单2.1中分号的使用。 在某些编程语言(大多数是C系列)的每一行结尾处都需要这样做。 它告诉编译器一行代码在何处结束。 这意味着单个语句可以跨越多行,或者多个语句可以在同一行上。 在C语言中,你将同时实现以下两个目的:

printf("The output is: %d",
  0);
printf("Hello"); printf("World");

在Nim中,分号是可选的,可用于在一行上写两个语句。 在多行中跨越一条语句会有些复杂-你只能在标点符号后拆分一条语句,并且必须缩进下一行。 这是一个例子:

echo("Output: ", # Both of these statements are correct because they’ve been split
  5)
echo(5 +         # after the punctuation and the next line has been indented.
  5)

echo(5           # This statement has been incorrectly split before the punctuation.
  + 5)
echo(5 +         # This statement has not been correctly indented after the split.
5)

由于缩进在Nim中很重要,因此你需要保持其样式一致。 约定指出,所有Nim代码均应缩进两个空格。 Nim编译器当前不允许使用制表符,因为空格和制表符的不可避免的混合会产生有害的影响,尤其是在对空格敏感的编程语言中。

2.1.3. 注释

代码注释很重要,因为它们使你可以在代码段中添加其他含义。 Nim中的注释使用井号(#)编写。 紧随其后的所有内容将在新行开始之前进行注释。 可以使用#[和]#创建多行注释,也可以使用when::禁用代码。 这是一个例子:

# Single-line comment
#[
Multiline comment
]#
when false:
  echo("Commented-out code")

两种类型的多行注释中的第一种可以用于注释掉文本和代码,而后者只能用于注释掉代码。 编译器仍会解析代码并确保其在语法上有效,但不会包含在结果程序中。 这是因为编译器会在编译堆时检查when语句。

2.2. Nim 基础

现在,你已经对Nim的语法有了基本的了解,为学习Nim的某些语义奠定了良好的基础。 在本节中,你将学习每个Nim程序员每天使用的一些基本知识。 你将了解最常用的静态类型,可变和不可变变量的详细信息,以及如何通过定义过程将常用代码分为独立的单元。

2.2.1. 基本类型

Nim是一种静态类型的编程语言。这意味着Nim中的每个标识符在编译时都具有与其关联的类型。当你编译Nim程序时,编译器将确保你的代码是类型安全的。如果不是,则编译术语会编译器输出错误。这与诸如Ruby之类的动态类型编程语言相反,后者只能确保你的代码在运行时是类型安全的。

按照约定,类型名称以大写字母开头。内置类型不遵循此约定,因此你可以通过检查名称的首字母来轻松区分内置类型和用户定义类型。 Nim支持许多内置类型,包括用于处理C外部函数接口(FFI)的类型。我不会在这里介绍所有内容,但本书稍后将介绍它们。

外部函数接口外部函数接口(FFI)使你可以使用以其他编程语言编写的库。 Nim包含C和C ++固有的类型,从而允许使用以这些语言编写的库。

大多数内置类型是在系统模块中定义的,该模块会自动导入你的源代码中。 在代码中引用这些类型时,你可以使用模块名称(例如system.int)来限定它们的类型,但这不是必需的。 有关系统模块中定义的基本类型的列表,请参见表2.1。

模块使用import关键字导入模块。 在本书的后面,你将学到更多关于模块的知识。

Table 1. 表 2.1 基本类型

类型

描述和使用

int

整数类型是用于整数的类型。 例如 52。

float

字符串类型用于存储多个字符。 通过将多个字符放在双引号中来创建字符串文字:"Nim is awesome"。

string

布尔类型存储两个值之一,即 true 或 false。

bool

字符类型存储单个 ASCII 字符。 字符文字被创建

char

通过在单引号内放置一个字符; 例如 "A"。

整型

整数类型表示不带小数部分的数值数据。 即是整数。 这种类型可以存储的数据量是有限的,因此在Nim中有多种版本,每种版本都适合不同的大小要求。 Nim中主要的整数类型是int,这是你在Nim程序中应该使用最多的整数类型。 有关整数类型的列表,请参见表2.2。

Table 2. 表 2.2 整型

类型

大小

范围

描述

int

Architecture-dependent. 32-bit on 32-bit systems, 64-bit on 64-bit systems

32-bit: -2,147,483,648 to 2,147,483,647 64-bit: -9,223,372,036,854, 775,808 to 9,223,372,036, 854,775,807

通用有符号的两个补码整数。 通常,你应该在大多数程序中使用此整数类型。

int8

8-bit

int16

16-bit

int32

32-bit

int64

64-bit

2.2.2. 定义变量和其它存储

Nim 中的存储使用三个不同的关键字来定义。除了上一节提到的 let 关键字,你还可以使用 constvar 来定义存储。

let number = 10

通过使用 let 关键字,你将创建一个被称为不可变的变量-一个只能被分配一次的变量。在本例中,我们创建了一个名为 number 的新的不可改变的变量,并且标识符 number 被绑定为值 10。如果你试图给这个变量分配一个不同的值,你的程序将无法编译,就像下面的 numbers.nim 例子一样:

let number = 10
number = 4000

前面的代码在编译时将产生以下输出:

numbers.nim(2, 1) Error: 'number' cannot be assigned to

Nim 还支持使用关键字 var 的可突变变量,如果你打算改变一个变量的值,可以使用这些关键字。前面的例子可以通过将 let 关键字替换为 var 关键字来解决:

var number = 10
number = 4000

在这两个例子中,编译器将根据分配给它的值来推断数字变量的类型。在这个例子中,number 将是一个 int。你可以在变量名后面写上类型,并用冒号字符(:)隔开,从而明确地指定类型。通过这样做,你可以省略赋值,当你在定义变量时不想给它赋值时,这很有用。

var number: int <-- This will be initialized to 0.

不可变变量 不可变变量在定义时必须分配一个值,因为它们的值不能改变。这包括 constlet 定义的存储。

变量的初始值将永远是二进制零。根据类型的不同,这将以不同的方式表现出来。例如,默认情况下,整数将为 0,字符串将为 nilnil 是一个特殊的值,表示任何引用类型都没有值。你将在后面学习更多关于这方面的知识。

变量的类型是不能改变的。例如,将一个字符串分配给一个 int 变量会导致一个编译时错误,就像这个 typeMismatch.nim 的例子一样。

下面是错误的输出:

typeMismatch.nim(2, 10) Error: type mismatch: got (string) but expected 'int'

Nim 也支持常量。因为常量的值也是不可改变的,所以常量类似于使用 let 定义的不可改变的变量,但 Nim 常量有一个重要的区别:它的值必须在编译时可以计算。

proc fillString(): string =
  result = ""
  echo("Generating string")  | The $ is a commonly used
  for i in 0 .. 4:           | operator in Nim that converts
    result.add($i)         --  its input to a string.

const count = fillString()

PROCEDURES 别担心你还不了解 Nim 的程序细节。你将很快了解它们。

在清单2.8中的 fillString 存储过程将生成一个新的字符串,等于 "01234"。然后,常数将被分配给这个字符串。

我在 fillString 的顶部添加了 echo,以便向你展示它是在编译时执行的。试着用 Aporia 或在终端执行 nim c file.nim 来编译这个例子。你会在输出中看到 "Generating string"。运行二进制文件永远不会显示该消息,因为 fillString 存储过程的结果已经嵌入其中。

为了生成常量的值, fillString 存储过程必须在编译时由 Nim 编译器执行。但你必须注意,并不是所有的代码都能在编译时执行。例如,如果一个编译时过程使用 FFI,你会发现编译器会输出一个类似于 "Error: cannot 'importc' variable at compile time" 的错误。

使用常量的主要好处是效率高。编译器可以在编译时为你计算一个值,节省了本来在运行时花费的时间。明显的缺点是编译时间较长,但也可能产生较大的可执行大小。就像许多事情一样,你必须为你的用例找到合适的平衡点。Nim 为你提供了工具,但你必须负责任地使用它们。

你也可以在同一个 varletconst 关键字下指定多个变量定义。要做到这一点,请在关键字后添加新的一行,并在下一行缩进标识符:

var
  text = "hello"
  number: int = 10
  isTrue = false

变量的标识符是它的名称。它可以包含任何字符,只要名字不以数字开头,也不包含两个连续的下划线。这适用于所有的标识符,包括过程名和类型名。标识符甚至可以使用 Unicode 字符。

var 火 = "Fire"
let ogien = true

与其他许多编程语言不同,Nim中的标识符不区分大小写,但标识符的第一个字母除外。这是为了帮助区分必须以小写字母开头的变量名和必须以大写字母开头的类型名。

Nim 中的标识符也是对样式不敏感的。这使得用 camelCase 写的标识符与用 snake_case 写的标识符具有同等效力。实现这一点的方法是忽略标识符中的下划线字符,所以 fooBar 相当于 foo_bar。你可以自由地用任何你喜欢的风格来写标识符。

Stropping

你可能还记得在2.1节中,Nim 中有些标识符是保留的。这种标识符被称为关键字,由于它们具有特殊的含义,所以不能用作变量、类型或过程的名称。

为了绕过这个限制,你可以选择一个不同的名字,或者使用反引号(`)明确地标记标识符。后一种方法叫做笔划,下面是它的使用方法。

var `var` = "Hello"
echo(`var`)

var 关键字用反标号括起来,允许定义该名称的变量。

即使它们是用不同的样式定义的。但我们鼓励你遵循 Nim 的风格约定,它规定变量应该使用 camelCase,类型应该使用 PascalCase。关于 Nim 惯例的更多信息,可以看看 GitHub 上的 "Nim 代码风格指南":https://github.com/nim-lang/Nim/wiki/Style-Guide-for-Nim-Code

2.2.3. 过程定义

程序允许你把你的程序分成不同的代码单元。这些单元通常在被赋予一些输入数据后,执行一个单一的任务,通常是以一个或多个参数的形式。

在本节中,我们将探讨 Nim 中的程序。在其他编程语言中,过程可能被称为函数、方法或子程序。每种编程语言对这些术语都有不同的含义,Nim 也不例外。在 Nim 中,可以使用 proc 关键字来定义一个过程,后面是过程的名称、参数、可选的返回类型、= 和过程主体。图2.1显示了一个 Nim 过程定义的语法。

s4 271667.jpg@596w 1l

图2.1中的存储过程被命名为 myProc,它接受一个类型为 string的参数(name),并返回一个类型为 string 的值。存储过程的主体隐式地返回一个字符串 "Hello"和参数名的连词。

你可以通过在存储过程名称后面写上参数名来调用一个存储过程:myProc("Dominik")。可以在括号内指定任何参数。在前面的例子中,调用带有 "Dominik" 参数的存储过程 myProc,将会返回字符串 "Hello Dominik"。

每当调用带有返回值的存储过程时,其结果必须以某种方式被使用。

proc myProc(name: string): string = "Hello " & name
myProc("Dominik")

编译这个例子将导致一个错误。"file.nim(2, 7) Error: value of type 'string' has to be discarded." "file.nim(2, 7) Error: value of type 'string' has to be discarded." 这个错误的发生是由于 myProc 存储过程返回的值被隐式丢弃的结果。在大多数情况下,忽略存储过程的结果是你的代码中的一个错误,因为结果可能描述了一个发生的错误或给你一个重要的信息。你很可能会想对这个结果做一些事情,比如把它存储在一个变量中,或者通过调用把它传递给另一个过程。如果你真的不想对存储过程的结果做任何事情,你可以使用丢弃关键字来告诉编译器要保持安静。

proc myProc(name: string): string = "Hello " & name
discard myProc("Dominik")

discard 关键字只是让编译器知道,你很乐意忽略存储过程返回的值。

程序顺序

程序必须定义在调用站点之上。例如,以下代码将无法编译。

myProc()
proc myProc() = echo("Hello World")

对于有循环依赖关系的程序,必须使用正向声明:

proc bar(): int
proc foo(): float = bar().float
proc bar(): int = foo().int

Nim 的未来版本可能会取消对前向声明的需求,并允许以任何顺序定义程序。

当一个存储过程不返回任何值时,可以省略返回类型。在这种情况下,过程被称为返回 void。下面的两个例子没有返回值。

proc noReturn() = echo("Hello")
proc noReturn2(): void = echo("Hello")

在存储过程定义中避免使用多余的 void 是一种习惯。空格类型在其他情况下也很有用,比如在第九章中我们将学习的泛型。

Nim 允许你进一步减少不必要的语法。如果你的存储过程没有参数,你可以省略括号。

proc noReturn = echo("Hello")

从程序中返回值

一个存储过程主体可以包含多条语句,用分号或换行符分隔。如果一个存储过程的最后一个表达式有一个与之相关的非空值,那么这个表达式将被隐式地从存储过程中返回。如果你愿意的话,你总是可以使用 return 关键字作为过程的最后一条语句,但是这样做既不习惯也没有必要。对于过程的早期返回,return 关键字仍然是必要的。

下面的代码块显示了从过程中返回值的不同例子。

proc implicit: string =
  "I will be returned"

proc discarded: string =
  discard "I will not be returned"

proc explicit: string =
  return "I will be returned"

proc resultVar: string =
  result = "I will be returned"

proc resultVar2: string =
  result = ""
  result.add("I will be ")
  result.add("returned")

proc resultVar3: string =
  result = "I am the result"
  "I will cause an error"

assert implicit() == "I will be returned"
assert discarded() == nil
assert explicit() == "I will be returned"
assert resultVar() == "I will be returned"
assert resultVar2() == "I will be returned"
# resultVar3 does not compile!

断言

显示从过程中返回值的例子的代码块使用断言来显示当调用每个定义的过程时你应该期望的输出。在第三章中,当你需要测试你的代码时,你会学到更多关于断言的知识。

就像一个变量的默认值一样,一个过程的返回值默认为二进制零。Nim支持很多不同的设置返回值的方法,你可以自由组合它们。

每个具有返回类型的过程都在其主体内部隐式声明了一个结果变量。这个结果变量是可变的,并且与存储过程的返回类型相同。它可以像其他变量一样被使用;resultVarresultVar2 过程就是两个例子。你应该尽可能地使用它,而不是定义自己的变量并显式地返回它。

当结果变量与隐式返回相结合时,它带有一些限制。这些限制可以防止出现歧义。例如,在 resultVar3 pro-cedure 中,你认为应该返回什么:最后的表达式,还是 result 被分配的值?编译器不会为你选择;它只是显示一个错误,这样你就可以纠正歧义。

到目前为止,我一直在明确地指定过程的返回类型。你可能还记得,这对于变量定义来说并不是必须的。也可以要求编译器为你推断过程的返回类型。为了做到这一点,你需要使用自动类型。

proc message(recipient: string): auto =
  "Hello " & recipient

assert message("Dom") == "Hello Dom"

虽然这很方便,但只要有可能,你就应该明确地指定类型。这样做可以让你和其他人更容易确定过程的返回类型,而不需要理解过程的主体。

警告:类型推断 存储过程的类型推断在 Nim 中还是有点经验性的。你可能会发现它在某些情况下是有限的,特别是如果你习惯于更高级的类型推理形式,比如那些在 Haskell 或 OCaml 中发现的类型推理。

程序参数

可以通过列出参数并以逗号分隔来定义一个有多个参数的过程。

proc max(a: int, b: int): int =
  if a > b: a else: b

assert max(5, 10) == 10

如果连续指定参数的类型,你就不需要重复。

proc max(a, b: int): int =
  if a > b: a else: b

缺省参数可以用来请求参数,这些参数可以在调用站点上选择性地指定。你可以通过使用等号给参数赋值来引入缺省参数;在这种情况下也可以省略类型。

proc genHello(name: string, surname = "Doe"): string =
  "Hello " & name & " " & surname

assert genHello("Peter") == "Hello Peter Doe"
assert genHello("Peter", "Smith") == "Hello Peter Smith"

可以使用 varargs 类型来指定一个过程的可变参数数。

proc genHello(names: varargs[string]): string =
  result = ""
  for name in names:
    result.add("Hello " & name & "\n")

assert genHello("John", "Bob") == "Hello John\nHello Bob\n"

程序重载

过程重载是一个你可能还没有接触过的功能,但它是Nim中常用的一个功能。存储过程重载是指可以定义不同的、具有相同名称的存储过程的实现。每一个过程都有相同的名称,但接受不同的参数。根据传递给过程的参数,编译器会选择合适的实现。

举个例子,考虑一个过程 getUserCity。它可能会接受两个参数:firstName 和 lastName。firstName 和 lastName。

proc getUserCity(firstName, lastName: string): string =
  case firstName
  of "Damien": return "Tokyo"
  of "Alex": return "New York"
  else: return "Unknown"

case 语句 案例说明对你来说可能还很陌生。它们将在后面的2.4节中解释。

这种存储过程可以用来根据指定的名字从数据库中检索一个人的居住城市。你也可能希望提供其他的搜索条件—​更独特的东西,比如一个ID号。要做到这一点,你可以像这样重载 getUserCity 存储过程。

proc getUserCity(userID: int): string =
  case userID
  of 1: return "Tokyo"
  of 2: return "New York"
  else: return "Unknown"

这样,你可以重复使用这个名字,但你仍然可以使用不同的实现方式,如这里所示。

doAssert getUserCity("Damien", "Lundi") == "Tokyo"
doAssert getUserCity(2) == "New York

匿名程序

有时你可能希望将存储过程作为参数传递给其他存储过程。下面的列表显示了一个新存储过程的定义,以及如何将对它的引用传递给过滤器存储过程。

import sequtils
let numbers = @[1, 2, 3, 4, 5, 6]
let odd = filter(numbers, proc (x: int): bool = x mod 2 != 0)
assert odd == @[1, 3, 5]

这些程序被称为匿名程序,因为没有与之相关的名称。在列表2.9中,匿名存储过程用粗体标出。 符号 @ 符号创建了一个新的序列。我们将在下一节中学习更多关于它的知识。

匿名存储过程得到一个单一的参数,x,类型为int。这个参数是数字序列中的一个项目。这个匿名存储过程的工作是决定是否应该过滤掉这个项目,或者是否应该保留这个项目。当存储过程返回true时,这个项目就不会被过滤掉。

过滤过程是进行实际过滤的过程。它需要两个参数:一个序列和一个匿名过程。然后它遍历每一个项目,并使用得到的匿名存储过程来判断是否应该过滤掉这个项目或者保留它。然后,过滤过程返回一个新的序列,其中只包括匿名过程确定应该保留而不是过滤掉的项目。在列表2.9中,得到的序列将只包含奇数。这反映在匿名过程中,它检查每个项目除以2是否会产生余数。如果产生了余数,则返回true,因为这个 表示这个数字是奇数。

匿名过程的语法有点繁琐。值得庆幸的是,Nim为定义匿名过程和过程类型提供了一些语法糖。语法糖不是语言的一部分,而是在标准库中定义的,所以要使用它必须导入未来模块。(语法糖是通过宏来定义的,我们将在第9章中学习。)

2.3. 集合类型

列表、数组、集合等集合都非常有用。在本节中,我将讲述 Nim 中最常用的三种集合类型:数组、序列和集合类型。

2.3.1. 数组

数组类型表示一个静态项数的列表。这种类型类似于 C 数组,但提供了更多的内存安全性,如下例所示。

var list: array[3, int]
list[0] = 1
list[1] = 42
assert list[0] == 1
assert list[1] == 42
assert list[2] == 0  # 该数组包含三个元素。任何未设置的元素都会被赋予一个默认值。
echo list.repr       # 这将输出[1,42,0]。存储过程repr将任何变量转换为字符串,但产生的字符串有时包含调试信息,如变量的内存地址。
echo list[500]       # 编译会出现 "Error: index out of bounds"的失败。

数组是值类型,就像 int、float 和许多其他类型一样,这意味着它们是在堆上分配的。这与 C 语言的数组类似,但它与 Java 的数组完全不同,后者是引用类型,存储在堆上。

数组的大小是静态的,所以一个数组一旦声明,就不能改变它的大小。这就是为什么当你试图访问一个超出其边界的索引时,编译器会给你一个错误。在 C 语言中,没有对索引边界进行检查,所以有可能访问数组边界之外的内存。

Nim 在编译时和运行时都会执行这些检查。只要不关闭 --boundsChecks 选项,就会进行运行时检查。

2.3.2. 序列

2.3.3. 集合

2.4. 控制流

2.5. 异常处理

2.6. 用户定义类型

2.6.1. 对象

2.6.2. 元组

2.6.3. 枚举

2.7. 总结

3. 编写聊天程序

3.1. 聊天程序的架构

3.1.1. 程序最终长什么样?

3.2. 开始工程

3.3. 在客户端组件中检索输入

3.3.1. 检索用户提供的命令行参数

3.3.2. 从标准输入流中读取数据

3.3.3. 使用 spawn 避免阻塞输入/输出

3.4. 实现协议

3.4.1. 模块

3.4.2. 解析 JSON

3.4.3. 生成 JSON

3.5. 使用 sockets 传送数据

3.5.1. 什么是 socket

3.5.2. 异步输入/输出

3.5.3. 异步传输数据

3.6. 总结

4. 标准库概览

4.1. 仔细查看模块

4.1.1. 命名空间

4.2. 标准库概览

4.2.1. 纯模块

4.2.2. 非纯模块

4.2.3. 包装器

4.2.4. 在线文档

4.3. 核心模块

4.4. 数据结构和算法

4.4.1. tables 模块

4.4.2. sets 模块

4.4.3. 算法

4.4.4. 其它模块

4.5. 操作系统接口

4.5.1. 文件系统

4.5.2. 执行外部程序

4.5.3. 其它操作系统服务

4.6. 理解并操作数据

4.6.1. 解析命令行参数

4.7. 网络和英特网

4.8. 总结

5. 包管理

5.1. Nim 包管理器

5.2. 安装 Nimble

5.3. nimble 命令行工具

5.4. 什么是 Nimble 包

5.5. 安装 Nimble 包

5.5.1. 使用 install 命令

5.5.2. install 命令工作原理

5.6. 创建 Nimble 包

5.6.1. 选择一个名字

5.6.2. Nimble 包的目录布局

5.6.3. 编写.nimble文件并整理依赖性

5.7. 发布 Nimble 包

5.8. 开发 Nimble 包

5.8.1. 提供版本号含义

5.8.2. 存储单个软件包的不同版本

5.9. 总结

6. 并行

6.1. 并发与并行

6.2. 在 Nim 中使用线程

6.2.1. threads 模块和 GC 安全

6.2.2. 使用线程池

6.2.3. 线程中的异常

6.3. 解析数据

6.3.1. 掌握维基百科 page-counts 的格式

6.3.2. 解析维基百科 page-counts 的格式

6.3.3. 高效地处理文件的每一行

6.4. 并行解析器

6.4.1. 计量 sequential_counts 的执行时间

6.4.2. 并行化 sequential_counts

6.4.3. 类型定义和解析过程

6.4.4. parseChunk 过程

6.4.5. 并行化的 readPageCounts 过程

6.4.6. parallel_counts 的执行时间

6.5. 处理竞争条件

6.5.1. 使用守卫和锁来阻止竞争条件

6.5.2. 使用通道,以便线程可以发送和接收消息

6.6. 总结

7. 创建推特克隆

7.1. web 应用的架构

7.1.1. 微框架中的路由

7.1.2. 推特的架构

7.2. 开始工程

7.3. 在数据库中存储数据

7.3.1. 设置类型

7.3.2. 设置数据库

7.3.3. 存储并检索数据

7.3.4. 测试数据库

7.4. 开发 web 应用的视图

7.4.1. 开发用户视图

7.4.2. 开发通用视图

7.5. 开发控制器

7.5.1. 实现 /login 路由

7.5.2. 扩展 / 路由

7.5.3. 实现 /createMessage 路由

7.5.4. 实现用户路由

7.5.5. 添加 Follow 按钮

7.5.6. 实现 /fellow 路由

7.6. 部署 web 应用程序

7.6.1. 配置 Jester

7.6.2. 设置反向代理

7.7. 总结

8. 与其它语言交互

8.1. Nim 的外部函数接口

8.1.1. 静态与动态链接

8.1.2. 包装 C 过程

8.1.3. 类型兼容性

8.1.4. 包装 C 类型

8.2. 包装一个外部 C 库

8.2.1. 下载库

8.2.2. 创建 SDL 库包装器

8.2.3. 动态链接

8.2.4. 包装类型

8.2.5. 包装过程

8.2.6. 使用 SDL 包装器

8.3. Javascript 后端

8.3.1. 包装 canvas 元素

8.3.2. 使用 Canvas 包装器

8.4. 总结

9. 元编程

9.1. 泛型

9.1.1. 泛型过程

9.1.2. 类型定义中的泛型

9.1.3. 约束泛型

9.1.4. 概念

9.2. 模板

9.2.1. 传递代码块到模板

9.2.2. 模板中的参数替换

9.2.3. 卫生模板

9.3. 宏

9.3.1. 编译时函数 执行

9.3.2. 抽象语法树

9.3.3. 宏定义

9.3.4. 宏中的参数

9.4. 创建一个配置 DSL

9.4.1. 开始一个配置工程

9.4.2. 生成对象类型

9.4.3. 生成构造器过程

9.4.4. 生成 load 过程

9.4.5. 测试配置器

9.5. 总结