開始使用 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 社區仍是蠻熱情的. 謝謝你們.

相關文章
相關標籤/搜索