开始使用 Nim(翻译)

原文: http://howistart.org/posts/nim/1html


Nim 是一门年轻的, 让人兴奋的命令式编程语言, 即将发布 1.0 办法.
我对与 Nim 最主要的兴趣在于性能/生成力的比值, 以及使用 Nim 写程序带来的乐趣.
这份教程里我会展现一下我是怎么展开一个 Nim 项目的.react

如今咱们的目标是写一个 Brainfuck 语言的简单的解释器.
Nim 是一个使用的编程语言, 有着各类有趣的功能, Brainfuck 正好相反:
它很不实用, 它的全局功能就 8 个简单字符代码的指令.
不过 Brainfuck 对咱们来讲仍是不错的, 由于它够简单, 写解释器也就很简单.
后面咱们还会写一个高性能的编译器, 把 Brainfuck 在编译时转换为 Nim.
全部代码会被包装成 [nimble 模块]而后在网上发布git

安装

安装 Nim 步骤不错, 你能够看官方的说明. Windows 的二进制包是现成的.
其余操做系统你能够用 build.sh 教程编译生成的 C 代码, 通常的操做系统一分钟内能完成.github

这向咱们透露了 Nim 第一个有意思的事实: 它主要是编译为 C (也能够 C++, ObjectiveC, 甚至 JavaScript)
而后用高度优化的 C 编译器把代码编译成为实际的程序.
你直接就能从 C 的生态系统当中获益.web

若是你选择从 Nim 的编译器 自举, 也就是 Nim 语言自身实现的版本, 那么,
你能够看一看编译器是怎么一步一步把本身编译起来的(两分钟之内能完成):编程

bash$ git clone https://github.com/Araq/Nim
$ cd Nim
$ git clone --depth 1 https://github.com/nim-lang/csources
$ cd csources && sh build.sh
$ cd ..
$ bin/nim c koch
$ ./koch boot -d:release

这样你获得的是开发版本的 Nim. 要追上最新版本, 按下边两步应该就能够了:json

bash$ git pull
$ ./koch boot -d:release

若是能历来没作过, 那么这个时候安装一下 git 也是很不错的.
大部分的 nimble 模块托管在 GitHub 上, 咱们须要用 git 来获取.
在基于 Debian 的发行版当中(好比 Ubuntu), 这样就能安装:小程序

bash$ sudo apt-get install git

安装好之后, 把 nim 二进制文件加入到你的 PATH 环境变量当中去. 用 Bash 的话是这样作:vim

bash$ echo 'export PATH=$PATH:$your_install_dir/bin' >> ~/.profile
$ source ~/.profile
$ nim
Nim Compiler Version 0.10.2 (2014-12-29) [Linux: amd64]
Copyright (c) 2006-2014 by Andreas Rumpf
::

    nim command [options] [projectfile] [arguments]

Command:
  compile, c                compile project with default code generator (C)
  doc                       generate the documentation for inputfile
  doc2                      generate the documentation for the whole project
  i                         start Nim in interactive mode (limited)
...

nim 命令返回起版本跟用法, 就能够继续后面的步骤了.后端

如今 [Nim 的标准模块]只要 import 一下就行了.
其余的模块均可以用 nimble 来获取, 也就是 Nim 的包管理工具.
咱们要看一下基础的安装说明.
一样, Windows 平台有编译好的包, 不过从源码编译也挺轻松的:

bash$ git clone https://github.com/nim-lang/nimble
$ cd nimble
$ nim c -r src/nimble install

Nimble 的二进制目录也要加到 PATH 环境变量当中去:

bash$ echo 'export PATH=$PATH:$HOME/.nimble/bin' >> ~/.profile
$ source ~/.profile
$ nimble update
Downloading package list from https://github.com/nim-lang/packages/raw/master/packages.json
Done.

如今咱们来浏览可用的 nimble 模块或者从命令行当中进行搜索:

bash$ nimble search docopt
docopt:
  url:         git://github.com/docopt/docopt.nim (git)
  tags:        commandline, arguments, parsing, library
  description: Command-line args parser based on Usage message
  license:     MIT
  website:     https://github.com/docopt/docopt.nim

咱们来安装刚才找到的 docopt 模块, 待会可能会用到:

bash$ nimble install docopt
...
docopt installed successfully.

看看安装模块多块(我这里小于 1 秒). 这是 Nim 另外一个好处.
基本上模块的源代码只是被下载, 共享的模块当中没有什么要被编译的.
而是在咱们使用到模块的时候, 模块才会被静态编译到程序当中.

能够找到关于 Nim 的编辑器支持 的一个列表,
好比 Emacs(nim-mode), Vim(nimrod.vim[nimrod-vim], 个人用的), 还有 Sublime(Nimlime).
对于这篇教程范围来讲, 什么编辑器都是能够的.

项目设置

如今咱们开始建项目:

bash$ mkdir brainfuck
$ cd brainfuck

第一步: 要在终端打印 Hello World, 咱们先创建一个 hello.nim 包含如下内容:

nimecho "Hello World"

编译代码, 而后运行, 先用两个独立的步骤:

bash$ nim c hello
$ ./hello
Hello World

而后能够用一个步骤, 指明 Nim 编译器在生成二进制文件之后顺便运行一下:

bash$ nim c -r hello
Hello World

把代码改得稍微复杂一点, 那么运行起来就能久一点:

nimvar x = 0
for i in 1 .. 100_000_000:
  inc x # increase x, 增长 x, 顺便说下这是注释

echo "Hello World ", x

如今咱们是初始化变量 x0, 每次增长 1 一共一亿次. 继续编译, 运行.
注意这一次运行了多久. Nim 的性能很不堪么? 固然不是, 事实上正好相反.
上边咱们是在调试模式下生成的二进制文件, 添加了整数溢出的检测, 数组超出范围, 以及不少, 并且咱们一点没作优化.
使用 -d:release 选项能够帮助咱们切换到 release 模式, 提供全速:

bash$ nim c hello
$ time ./hello
Hello World 100000000
./hello  2.01s user 0.00s system 99% cpu 2.013 total
$ nim -d:release c hello
$ time ./hello
Hello World 100000000
./hello  0.00s user 0.00s system 74% cpu 0.002 total

实际上者也太快了. C 编译器直接把整个 for 循环给优化没了. Oops.

要建立一个新项目用 nimble init 能够成成基本的模块配置文件:

bash$ nimble init brainfuck

新生成的 brainfuck.nimble 应该是这样的:

ini[Package]
name          = "brainfuck"
version       = "0.1.0"
author        = "Anonymous"
description   = "New Nimble project for Nim"
license       = "BSD"

[Deps]
Requires: "nim >= 0.10.0"

咱们加上实际做者, 描述, 还有 docopt 这个依赖, 按照 [nimble 开发者信息]中描述的.
最重要的, 咱们要定义好想要建立的二进制文件:

ini[Package]
name          = "brainfuck"
version       = "0.1.0"
author        = "The 'How I Start Nim' Team"
description   = "A brainfuck interpreter"
license       = "MIT"

bin           = "brainfuck"

[Deps]
Requires: "nim >= 0.10.0, docopt >= 0.1.0"

由于咱们已经安装了 git, 咱们要记录源码全局的版本, 还有发到线上, 那么初始化一下 Git 仓库:

bash$ git init
$ git add hello.nim brainfuck.nimble .gitignore

其中个人 .gitignore 是这样的:

bashnimcache/
*.swp

Git 须要 ignore 掉 Vim 的 swap 文件, 还有 nimcache 文件中包含的生成的当前项目的 C 代码.
若是你对 Nim 怎么生成 C 代码感兴趣, 能够看一下.

要展现 nimble 的能力, 咱们来初始化 brainfuck.nim, 写上 main 程序:

nimecho "Welcome to brainfuck"

咱们能够像以前编译 hello.nim 同样进行编程, 不过考虑咱们已经在模块里定义好 brainfuck 的二进制文件,
咱们用 nimble 来作这个工做吧:

bash$ nimble build
Looking for docopt (>= 0.1.0)...
Dependency already satisfied.
Building brainfuck/brainfuck using c backend...
...
$ ./brainfuck
Welcome to brainfuck

nimble install 能够用来在咱们的系统当中安装二进制文件, 而后咱们能够随处运行:

bash$ nimble install
...
brainfuck installed successfully.
$ brainfuck
Welcome to brainfuck

程序能运行了是很棒的事情, 可是 nimble build 实际上作的是 release build.
这会比调试中的 builg 过程更漫长, 并且去掉开发过程当中很重要的检查,
因此这个时候 nim c -r brainfuck 仍是比较适合这种状况的.
开发过程中多执行几回程序, 感觉一下每一个地方是怎么运行的.

编码

Nim 有文档能够参考, 不过你不知道怎么找到某些东西的话, 还有个索引你能够搜索.

咱们开始修改 brainfuck.nim 开发咱们的解释器吧:

nimimport os

首先咱们引入 os 模块, 那么咱们能够读取命令行的参数:

nimlet code = if paramCount() > 0: readFile paramStr(1)
           else: readAll stdin

paramCount() 能够告诉咱们传给应用的命令行参数的个数.
咱们拿到命令行参数的话, 咱们设想会是文件名, 那么直接经过 readFile paramStr(1) 读取文件.
不然咱们直接从标准输入读取所在的东西. 两种状况下, 结果都是存储在 code 变量,
这个变量被 let 关键字声明为不可修改的.

要看是否正常运行, 咱们能够 echo 一下 code:

nimecho code

而后试一试:

nim$ nim c -r brainfuck
...
Welcome to brainfuck
I'm entering something here and it is printed back later!
I'm entering something here and it is printed back later!

你输入完"代码"之后要用 ctrl-d 来结束.
或者你能够传入一个文件名, nim c -r brainfuck 命令后面全部的都做为命令行参数传给生成的可执行文件:

nim$ nim c -r brainfuck .gitignore
...
Welcome to brainfuck
nimcache/
*.swp

而后咱们写:

nimvar
  tape = newSeq[char]()
  codePos = 0
  tapePos = 0

咱们定义一些会用到的变量. 须要保存 code 字符串当中的当前位置(codePos)延迟 tape 上的位置(tapePos).
Brainfuck 运行在一卷无限长延伸的 tape 上, 表示为一个 seqchar(字符的序列).
序列是 Nim 当中动态长度的 array, 除了协程 newSeq 你也能够用 var x = @[1, 2, 3] 初始化.

咱们花一点时间来回味一下不用为变量申明类型带来的方便, 它们都是自动推断的.
若是非要写得更明确一点, 咱们能够写:

nimvar
  tape: seq[char] = newSeq[char]()
  codePos: int = 0
  tapePos: int = 0

而后咱们写一个小的 procedure, 而后在后边立刻调用:

nimproc run(skip = false): bool =
  echo "codePos: ", codePos, " tapePos: ", tapePos

discard run()

有些事情能够注意的:

  • 咱们传入了 skip 参数, 初始化为 false
  • 很明显这个参数的类型是 bool
  • 返回值也是 bool 类型的, 可是咱们什么都没返回么? 每一个返回结果都是默认二进制 0, 咱们是返回的 `false
  • 咱们能够明确用 result 变量在每一个 proc 表示返回值, 设置为 result = true
  • 控制流能够被改变, 使用 return true 能够当即返回结果
  • 咱们须要明确 discard 掉调用 run() 返回的 bool 数值.
    不然编译器会警告 brainfuck.nim(16, 3) Error: value of type 'bool' has to be discarded.
    这是用来防止咱们忘记处理返回结果的.

继续以前, 咱们来想一下 Brainfuck 是怎样运行的.
若是以前你接触过图灵机, 那么其中一些地方你会感到很熟悉.
咱们会输入一个字符串 code, 还有一个包含 chartape 会在一个方向无线延伸.
输入的字符串当中会出现 8 中命令, 其余的字符都会被忽略掉:

操做符   含义                           Nim 对应代码
>       在 tape 上向右移动              inc tapePos
<       在 tape 上向左移动              dec tapePos
+       增长 tape 上的数值              inc tape[tapePos]
-       减少 tape 上的数值              dec tape[tapePos]
.       输出 tape 上的数值              stdout.write tape[tapePos]
,       输入值到 tape 上                tape[tapePos] = stdin.readChar
[       若是 tape 上的值是 \0, 向前移动到匹配了 ] 以后的命令
]       若是 tape 上不是 \0, 向后移动到匹配 [ 以后的命令

仅仅依靠上边这些, Brainfuck 成为了最简单的图灵彻底的编程语言之一.

前面 6 条指令能够被转化为 Nim 当中的 case 区别:

nimproc run(skip = false): bool =
  case code[codePos]
  of '+': inc tape[tapePos]
  of '-': dec tape[tapePos]
  of '>': inc tapePos
  of '<': dec tapePos
  of '.': stdout.write tape[tapePos]
  of ',': tape[tapePos] = stdin.readChar
  else: discard

到这里咱们是处理单个字符的输入, 而后咱们写一个处理所有字符的循环:

nimproc run(skip = false): bool =
  while tapePos >= 0 and codePos < code.len:
    case code[codePos]
    of '+': inc tape[tapePos]
    of '-': dec tape[tapePos]
    of '>': inc tapePos
    of '<': dec tapePos
    of '.': stdout.write tape[tapePos]
    of ',': tape[tapePos] = stdin.readChar
    else: discard

    inc codePos

咱们来测试一下这样一个简单的程序:

text$ echo ">+" | nim -r c brainfuck
Welcome to brainfuck
Traceback (most recent call last)
brainfuck.nim(26)        brainfuck
brainfuck.nim(16)        run
Error: unhandled exception: index out of bounds [IndexError]
Error: execution of an external program failed

结果让人诧异, 咱们的代码 crash 了! 什么地方写错了?
tape 被认为是无限延伸的, 但咱们到如今一点都没增长它的长度!
能够在 case 代码上边很容易地 fix 掉:

nimif tapePos >= tape.len:
  tape.add '\0'

最后两条指令, [] 组成了简单的循环. 咱们也能够在代码里写出来:

nimproc run(skip = false): bool =
  while tapePos >= 0 and codePos < code.len:
    if tapePos >= tape.len:
      tape.add '\0'

    if code[codePos] == '[':
      inc codePos
      let oldPos = codePos
      while run(tape[tapePos] == '\0'):
        codePos = oldPos
    elif code[codePos] == ']':
      return tape[tapePos] != '\0'
    elif not skip:
      case code[codePos]
      of '+': inc tape[tapePos]
      of '-': dec tape[tapePos]
      of '>': inc tapePos
      of '<': dec tapePos
      of '.': stdout.write tape[tapePos]
      of ',': tape[tapePos] = stdin.readChar
      else: discard

    inc codePos

若是咱们遇到一个 [ 咱们就递归地调用 run 函数自身,
一直循环直到对应的 ] tape 上没有 \0 的一个 tapePos.

就这样. 咱们有了一个能够运行的 Brainfuck 解释器.
为了作测试, 咱们建立一个 examples 文件夹, 其中包含 3 个文件:
helloworld.b, rot13.b, mandelbrot.b.

text$ nim -r c brainfuck examples/helloworld.b
Welcome to brainfuck
Hello World!
$ ./brainfuck examples/rot13.b
Welcome to brainfuck
You can enter anything here!
Lbh pna ragre nalguvat urer!
ctrl-d
$ ./brainfuck examples/mandelbrot.b

在最后一个程序运行的时候你课以看到咱们解释器有多么.
使用 -d:release 命令编译能够显著提高性能, 但仍是花了 90 秒的时候在我电脑上画 Mandelbrot 集.
为了达到更高的性能, 后面咱们要把 brainfuck 编译到 Nim, 而不是解释它.
Nim 的元编程能力对于这项任务是完美的.

首先咱们保持它的简单. 咱们的解释器是能够运行的, 那没咱们能够把它变成一个能够重用的库.
咱们所须要作的就是把代码包含在一个大的 proc 当中:

nimproc interpret*(code: string) =
  var
    tape = newSeq[char]()
    codePos = 0
    tapePos = 0

  proc run(skip = false): bool =
    ...

  discard run()

when isMainModule:
  import os

  echo "Welcome to brainfuck"

  let code = if paramCount() > 0: readFile paramStr(1)
             else: readAll stdin

  interpret code

注意咱们在 proc 后面加上了一个 *, 这表示 proc 被暴露能够在模块外部访问.
其余一切都是私有的.

在问问的结尾咱们依然保留咱们的二进制文件.
when isMainModule 保证了代码只会在模块是主模块时才会被编译.
通过短暂的 nimble install 以后这个 Brainfuck 模块就全局可用了, 这样:

nimimport brainfuck
interpret "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."

看着不错! 到这里咱们已经能跟别人共享代码了, 不过咱们仍是先加上一些文档:

nimproc interpret*(code: string) =
  ## Interprets the brainfuck `code` string, reading from stdin and writing to
  ## stdout.
  ...

执行 nim doc brainfuck 能够生成文档, 你能够[在线上看到][bf-docs]所有.

元编程

就像前面说的, 咱们的解释器对于 Mandelbrot 程序来讲仍是很是慢的.
咱们仍是来写一个 procedure 在编译时成成 Nim 代码的 AST 吧.

nimimport macros

proc compile(code: string): PNimrodNode {.compiletime.} =
  var stmts = @[newStmtList()]

  template addStmt(text): stmt =
    stmts[stmts.high].add parseStmt(text)

  addStmt "var tape: array[1_000_000, char]"
  addStmt "var tapePos = 0"

  for c in code:
    case c
    of '+': addStmt "inc tape[tapePos]"
    of '-': addStmt "dec tape[tapePos]"
    of '>': addStmt "inc tapePos"
    of '<': addStmt "dec tapePos"
    of '.': addStmt "stdout.write tape[tapePos]"
    of ',': addStmt "tape[tapePos] = stdin.readChar"
    of '[': stmts.add newStmtList()
    of ']':
      var loop = newNimNode(nnkWhileStmt)
      loop.add parseExpr("tape[tapePos] != '\\0'")
      loop.add stmts.pop
      stmts[stmts.high].add loop
    else: discard

  result = stmts[0]
  echo result.repr

其中的 addStmt template 只是用来减小代码模版的.
咱们也彻底能够在目前用了 addStmt 的未必谬次明确写上相同的操做.
(那也就是如今的 template 所作的事情!)
parseStmt 把一段 Nim 代码转换成对应的 AST, 而后咱们把他存放在数组里.

大部分的代码跟解释器是类似的, 出来代码如今不是立刻被执行的, 而是被添加到语句的列表里.
[] 就更复杂了, 它们被翻译到一个加载了代码的 while 循环.

这里咱们取巧了, 使用定长的 tape 而再也不去检查是否在范围内, 有没有溢出.
这只是为了简化一下. 要了解代码的行为, 在最后一行, echo result.repr 能够打印出生成的 Nim 代码.

而后在一个 static 的代码块里调用一下, 这能够强制在编译时运行:

nimstatic:
  discard compile "+>+[-]>,."

编译过程当中生成的代码会被打印出来:

nimvar tape: array[1000000, char]
var codePos = 0
var tapePos = 0
inc tape[tapePos]
inc tapePos
inc tape[tapePos]
while tape[tapePos] != '\0':
  dec tape[tapePos]
inc tapePos
tape[tapePos] = stdin.readChar
stdout.write tape[tapePos]

一般能够用到 dumpTree 这个宏, 能够打印代码真实的 AST 出来, 好比:

nimimport macros

dumpTree:
  while tape[tapePos] != '\0':
    inc tapePos

会显示出以下的树:

nimStmtList
  WhileStmt
    Infix
      Ident !"!="
      BracketExpr
        Ident !"tape"
        Ident !"tapePos"
      CharLit 0
    StmtList
      Command
        Ident !"inc"
        Ident !"tapePos"

好比我就是经过这个办法知道须要的是 StmtList.
用 Nim 进行元编程的时候, 一般用 dumpTree 打印出从 AST 生成的代码会颇有用.

宏生成的代码能够被直接插入到程序当中:

nimmacro compileString*(code: string): stmt =
  ## 编译 Brainfuck `code` 字符串到 Nim 代码,
  ## 从 stdin 读取数据, 在 stdout 写输出内容
  compile code.strval

macro compileFile*(filename: string): stmt =
  ## 编译过程从 `filename` 读取 Brainfuck 代码编译到 Nim
  ## 从 stdin 读取, 在 stdout 写输出的内容
  compile staticRead(filename.strval)

这样能够就能够很容易地吧 Mandelbrot 程序编译到 Nim 了:

nimproc mandelbrot = compileFile "examples/mandelbrot.b"

mandelbrot()

开启所有的优化仅限编程的话时间会很长(大约 4s), 由于 Mandelbrot 程序很大, GCC 须要时间优化.
最终结果程序的运行只须要一秒钟:

text$ nim -d:release c brainfuck
$ ./brainfuck

编译器的设置

Nim 默认使用 GCC 来编译到中间层的 C 代码, 不过 Clang 常常编译得更快, 获得的代码也更高效.
因此值得试一试. 要用 Clang 编译的话, 使用 nim -d:release --cc:clang c hello.
若是你打算一直使用 Clang 编译 hello.nim, 能够建立 hello.nim.cfg 文件, 内容写 cc = clang.
还能够编辑 Nim 目录中的 config/nim.cfg 文件修改默认的编译后端.

说到改变编译器默认的选项, Nim 编译器有时挺多嘴的, 能够在 config/nim.cfg 里设置 hints = off 关闭.
一个更意想不到的编译器警告是使用 l(小写的 L)做为标识符, 由于它看起来像 1(壹):

texta.nim(1, 4) Warning: 'l' should not be used as an identifier; may look like '1' (one) [SmallLshouldNotBeUsed]

若是你看不上的话, 写上 warning[SmallLshouldNotBeUsed] = off 就可让编译器安静.

Nim 还有个好处是可使用 C 支持的 debugger, 好比 GDB.
nim c --linedir:on --debuginfo c hello 命令编译而后运行 gdb ./hello 进行 debug.

解析命令行参数

前面一直是用手写的代码解析命令行参数. 既然已经安装了 dotopt.nim, 如今来用一下:

nimwhen isMainModule:
  import docopt, tables, strutils

  proc mandelbrot = compileFile("examples/mandelbrot.b")

  let doc = """
brainfuck

Usage:
  brainfuck mandelbrot
  brainfuck interpret [<file.b>]
  brainfuck (-h | --help)
  brainfuck (-v | --version)

Options:
  -h --help     Show this screen.
  -v --version  Show version.
"""

  let args = docopt(doc, version = "brainfuck 1.0")

  if args["mandelbrot"]:
    mandelbrot()

  elif args["interpret"]:
    let code = if args["<file.b>"]: readFile($args["<file.b>"])
               else: readAll stdin

    interpret(code)

docopt 模块一个好处是文档写在函数当中做为规范, 很容易使用:

text$ nimble install
...
brainfuck installed successfully.
$ brainfuck -h
brainfuck

Usage:
  brainfuck mandelbrot
  brainfuck interpret [<file.b>]
  brainfuck (-h | --help)
  brainfuck (-v | --version)

Options:
  -h --help     Show this screen.
  -v --version  Show version.
$ brainfuck interpret examples/helloworld.b
Hello World!

重构

随着项目变大, 能够把代码移到 src 目录, 再添加一个 test 目录,
很快咱们会须要这个目录, 最终文件结构是这样的:

text$ tree
.
├── brainfuck.nimble
├── examples
│   ├── helloworld.b
│   ├── mandelbrot.b
│   └── rot13.b
├── license.txt
├── readme.md
├── src
│   └── brainfuck.nim
└── tests
    ├── all.nim
    ├── compile.nim
    ├── interpret.nim
    └── nim.cfg

这样 nimble 文件也须要修改一下:

nimsrcDir = "src"
bin    = "brainfuck"

为了让代码容易重用, 咱们作一些重构. 同时保证程序使用读取 stdin 和写入stdout.

在直接接受 code: string 这样的命令行参数以外, 扩展 interpret procedure 来接收输入输出的流.
引入一个 streams 模块FileStreamsStringStream 进行支持:

nim## :Author: Dennis Felsing
##
## This module implements an interpreter for the brainfuck programming language
## as well as a compiler of brainfuck into efficient Nim code.
##
## Example:
##
## .. code:: nim
##   import brainfuck, streams
##
##   interpret("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.")
##   # Prints "Hello World!"
##
##   proc mandelbrot = compileFile("examples/mandelbrot.b")
##   mandelbrot() # Draws a mandelbrot set

import streams

proc interpret*(code: string; input, output: Stream) =
  ## Interprets the brainfuck `code` string, reading from `input` and writing
  ## to `output`.
  ##
  ## Example:
  ##
  ## .. code:: nim
  ##   var inpStream = newStringStream("Hello World!\n")
  ##   var outStream = newFileStream(stdout)
  ##   interpret(readFile("examples/rot13.b"), inpStream, outStream)

这里还为模块添加了文档, 模块代码作为类库怎样使用. 看一下生成的文档.

大部分代码能够不变, 除了与 Brainfuck 操做符 ., 相关的代码,
后面将使用 output 替代 stdout, 用 input 代替 stdin:

nimof '.': output.write tape[tapePos]
        of ',': tape[tapePos] = input.readCharEOF

为何有个奇怪的 readCharEOF 而不是 readChar, 做用是什么呢?
不少系统的 EOF(end of file) 表明 -1, 咱们这个 Brainfuck 程序也常常这样用.
这也意味着这个 Brainfuck 程序实际上不会在全部的系统都能运行.
同时 streams 模块也会处理系统不一致, 在 EOF 时返回 0.
这里用 readCharEOF 显式地转化到 -1:

nimproc readCharEOF*(input: Stream): char =
  result = input.readChar
  if result == '\0': # Streams 返回 0 表示 EOF
    result = 255.chr # BF 但愿 EOF 是 -1

这里你可能注意到了标识符声明的顺序在 Nim 当中是有影响的.
若是你在 interpret 后面声明 readCharEOF, 就不能在 interpret 中调用到.
我我的但愿遵循这一点, 由于这构成了每一个模块中一个简单代码到复杂代码这样的层级.
若是你仍是但愿绕过这一点, 就把 readCharEOF 的声明从定义拆分出来放到 interpret 前面:

nimproc readCharEOF*(input: Stream): char

而后能够像以前同样去使用解释器, 也很简单:

nimproc interpret*(code, input: string): string =
  ## 解释执行 Brainfuck `code` 字符串, 从 `input` 读取内容,
  ## 直接打印出结果.
  var outStream = newStringStream()
  interpret(code, input.newStringStream, outStream)
  result = outStream.data

proc interpret*(code: string) =
  ## 解释执行 Brainfuck `code` 字符串, 从 stdin 读取内容,
  ## 输出写到 stdout.
  interpret(code, stdin.newFileStream, stdout.newFileStream)

如今的 interpret procedure 能够返回一个字符串. 这对后边的测试来讲很重要:

nimlet res = interpret(readFile("examples/rot13.b"), "Hello World!\n")
interpret(readFile("examples/rot13.b")) # with stdout

编译器部分的重写有点复杂. 首先要把 inputoutput 做为字符串,
那么用户使用这个 proc 的时候就能够用任何他们想要的 stream 了:

nimproc compile(code, input, output: string): PNimrodNode {.compiletime.} =

还须要两条语句对输入跟输出的 stream 进行初始化而后做为字符串参数:

nimaddStmt "var inpStream = " & input
  addStmt "var outStream = " & output

固然我在咱们就要用 outStreaminpStream 来代替 stdout 跟 stdin 了, 还有 readCharEOF 代替 readChar.
主要能够直接用解释器已有的 readCharEOF procedure, 不须要重复写:

nimof '.': addStmt "outStream.write tape[tapePos]"
    of ',': addStmt "tape[tapePos] = inpStream.readCharEOF"

咱们还能够加上语句在用户用法有误时弹出好懂的错误信息:

nimaddStmt """
    when not compiles(newStringStream()):
      static:
        quit("Error: Import the streams module to compile brainfuck code", 1)
  """

而后把 compile procedure 链接到 compileFile 这个宏, 再使用 stdin 跟 stdout:

nimmacro compileFile*(filename: string): stmt =
  compile(staticRead(filename.strval),
    "stdin.newFileStream", "stdout.newFileStream")

读取输入的字符串, 写入输出的字符串:

nimmacro compileFile*(filename: string; input, output: expr): stmt =
  result = compile(staticRead(filename.strval),
    "newStringStream(" & $input & ")", "newStringStream()")
  result.add parseStmt($output & " = outStream.data")

这段复杂的代码让咱们可以编译 rot13 procedure, 链接 input 字符串跟 result 内容到编译后的程序:

nimproc rot13(input: string): string =
  compileFile("../examples/rot13.b", input, result)
echo rot13("Hello World!\n")

将来方便我对给 compileString 写了同样的代码. 能够在 GitHub 上看 brainfuck.nim 完整代码.

测试

未翻译

持续继承

未翻译

总结

Nim 的生态系统到这里已经介绍完了, 但愿你喜欢, 并且能跟我同样享受写 Nim 代码.

你要继续学习 Nim 的话, 我最近写了 what is special about Nim
what makes Nim practical, 还有个小程序的珍贵的收藏.

若是你想要用更传统的方法开始学 Nim, 官方教程Nim by Example 对你会有用.

Nim 社区仍是蛮热情的. 谢谢你们.

相关文章
相关标签/搜索