Nim 語言寫 Cirru Parser 的上手記錄

句句換行, 由於我寫的是代碼啊!

前端碼農, 寫了多年的動態語言了, TypeScript 算下來也用了兩年.
以前試過 Go, 可是 interface {} 簡直是 any 通常的存在.
因爲 Clojure 語言自己有開銷, 因此嘗試考慮學 Nim 來應對一些極端性能的狀況.html

性能

從網上的資料看, Nim 編譯到 C 運行, 能跑到媲美 C 的程度,
https://github.com/kostya/ben...
整體上不是最快, 可是在第一梯隊, 並且也存在一些優化的空間.
不過, 就像論壇上討論的, 關鍵性的性能仍是跟算法和數據結構等因素相關,
https://forum.nim-lang.org/t/...
Nim 提供了相對簡潔的寫法和概念, 能快速寫出性能還不錯的代碼,
若是想繼續優化, 能夠關掉自動的 GC 作更深層的優化再提升性能.前端

我在論壇上也看到個例子, 用 Nim 普通的寫法處理文件, 還不如 Python 快,
後面有人調試代碼, 優化了一個依賴庫, 幹掉了瓶頸之後十幾倍的性能提高.. 仍是算法.node

Clojure 的問題是, 做爲動態語言, 自己有巨大的運行時.
就運行的來講, 性能不會太慢的, 只是運行時自己的開銷基本上沒法優化掉.
在 ClojureScript 裏面, Closure Library 的陰影一直是在的, JavaScript 的開銷也在.
而 Nim 編譯出來的 Binary 顯然跟 C 相似, 能夠直接跳過這些東西.git

好比 Cirru Parser 完成一次文件讀取和解析, 代碼比較短, 幾毫秒就完成了,github

=>> cat ../example/echo.cirru

println 1 2
=>> time ./main ../example/echo.cirru
1 2

real  0m0.011s
user  0m0.003s
sys 0m0.006s
=>> time ./main ../example/echo.cirru
1 2

real  0m0.008s
user  0m0.003s
sys 0m0.004s
=>> time ./main ../example/echo.cirru
1 2

real  0m0.008s
user  0m0.003s
sys 0m0.004s

而 nodejs 腳本啓動一次就花比這長的時間了. 更不用說 ClojureScript 那一整套,算法

=>> cat a.js

console.log("100")
=>> time node a.js
100

real  0m0.071s
user  0m0.049s
sys 0m0.016s
=>> time node a.js
100

real  0m0.072s
user  0m0.047s
sys 0m0.019s
=>> time node a.js
100

real  0m0.068s
user  0m0.048s
sys 0m0.016s

語法

Nim 語法借鑑 Python 挺多的. 由於 CoffeeScript 當初就跟 Python 類似, 因此比較熟悉.
基本上就是借鑑來借鑑去的, 沿着縮進這一派, 簡化了不少干擾的符號.
比較不同的地方, 主要是 Nim 是帶類型的, 因此多出了一些寫法, 可能局部會遇到困惑.
普通的邏輯代碼, 總體看上去跟 Python 跟 CoffeeScript 都很是類似.npm

一些特徵的地方, Nim 裏面沒有直接用 def, lambda 或者 () ->,
Nim 的函數是用 proc 表示的, 這在高級語言裏邊很少見,
可是對於 Clojure 用戶我以爲這個就是很明確的信號 procedure 而不是純函數.
proc 能夠被寫成多行的匿名函數用, 大體感受還能夠, 但不如 CoffeeScript 方便.編程

Nim 當中的模塊, 若是暴露公共函數的話要在函數或者變量以後加上 * 做爲標記.
相比 Go 當中用大寫開頭來標記, 這個算是友好多了, 不會影響到函數名.後端

寫 JavaScript 或者 Go 的時候, 基本上用的就是 var 聲明變量.
JavaScript 後面加上了 let 加上了 const 區別也並不大.
即使 const 會要求賦值不可修改, 若是定義的是對象, 後面還會被修改掉.
在 Nim 當中比較明確還有嚴格,bash

  • let 定義的就是不可變的變量結構, 不能再被賦值, 也不能被修改熟悉,
  • var 定義可變的結構, 並且定義在參數當中若是可變, 也須要對應的 ref 標記.

編譯器會提示, 區分得至關明確了.

跟 CoffeeScript 相似, Nim 有默認的返回值. 算是語法糖.

這樣的縮進語法, 邏輯代碼寫下來, 除了類型之外, 跟 Python CoffeeScript 就及其類似了,

proc resolveComma*(expr: CirruNode): CirruNode =
  case expr.kind
  of cirruString:
    return expr
  of cirruSeq:
    var buffer: seq[CirruNode]
    for i, child in expr.list:
      case child.kind
      of cirruString:
        buffer.add child
      of cirruSeq:
        if child.list.len > 0 and child.list[0].kind == cirruString and child.list[0].text == ",":
          let resolvedChild = resolveComma(child)
          for j, x in resolvedChild.list[1..^1]:
            buffer.add resolveComma(x)
        else:
          buffer.add resolveComma(child)
    return CirruNode(kind: cirruSeq, list: buffer, line: expr.line, column: expr.column)

函數重載

關於函數的重載, 我長期用 CoffeeScript Clojure TypeScript 接觸比較少,
好比 Clojure 通常是對於不一樣參數個數存在函數重載.
TypeScript 相似, 主要仍是參數個數因此能夠重載. 對於不一樣類型的函數重載都沒用過.
主要仍是動態語言常常就是 uni-type 的習慣, 即使編譯, 編譯期函數不方便重名, Go 都不行..
而後就要等到運行時才能對函數進行重載, 除非說是不一樣參數個數算是例外..
動態類型再極端點要作重載要 Python 的 __add__ 或者 JavaScript 的 Proxy 進行騷操做了.

可是 Nim 就是靜態類型語言啊, 直接對 proc 作不一樣類型的函數重載囉.

proc zero[T: int]() = 0
proc zero[T: float]() = 0.0
proc zero[T: int32]() = 0'i32

在 Cirru Parser 當中我重載了 == 函數用於節點的比較:

proc `==`*(x, y: CirruNode): bool =
  # overload equality function
  return cirruNodesEqual(x, y)

proc `!=`*(x, y: CirruNode): bool =
  # overload equality function
  return not cirruNodesEqual(x, y)

社區氛圍

https://forum.nim-lang.org

論壇比較活躍, 跟 Clojure 社區挺像的, 比較友好.
我在上面發了幾回提問, 都很快有人回答我, 答案也是切中要害, 也不嫌棄我新手.
就文檔來講, 我感受這個是比 Clojure 好的, 容易上手.
Clojure 那邊不少東西在 Slack 上, 搜索不到, 可是 Nim 社區能搜到的東西不少.
並且 Nim 有個好處是編譯器提示比較清晰, 這比 Clojure 明確多了.

不過跟 Clojure 時不時刷上 Hacker News 不同, Nim 顯得低調不少.
Clojure 社區時不時看到有人秀 Macro, 固然, 用得也是比較隨意的,
在 Nim 論壇上沒怎麼看到, 估計是用的人不那麼多吧, 高級技巧.

包管理

https://github.com/nim-lang/n...

包管理是發佈模塊依賴的功能, nim 提供了 nimble 命令用於項目管理,
npm 的使用還好, Go 跟 Clojure 的包管理都有一些坑的,
Clojure 使用的驗證機制要搞 GPG 祕鑰, 初次使用配置起來挺煩的.
Nim 乾脆直接用 GitHub 來維護模塊列表, 放在一個倉庫裏, 簡單粗暴.
因此發佈模塊的時候須要 fork 倉庫提交信息, 等待人工合併, 相對麻煩一點.
目前 Nim 的總共的模塊數量相對來講不是那麼多, 不像 Web 開發圈這麼多樣.
不過上手的門檻確實不高, 相信有 GitHub 使用經驗的人很快都能搞定.

另外對於腳手架, 對於測試, 對於本地安裝, 對於依賴管理等等, 都提供了簡單的方案.
剛開始按照教程一步步走下來, 基本沒有遇到什麼大的坑, 提示也比較明確.

項目最初的開發, 因爲比較簡單, 也就很快能用命令直接運行, 比較省事,

nim c -r main.nim

Object Variants

具體到 Cirru Parser 的開發, 因爲用到遞歸的數據結構, 剛開始遇到的麻煩,
Nim 不像動態語言輕易定義任意結構, 也不像 Haskell 直接有代數類型的遞歸結構,
論壇問了一圈, 意識到 Nim 須要用 Object Variants 的用法專門處理.
https://nim-lang.org/docs/man...

好比 Cirru 表達式的結構就須要這樣定義出來,

type
  CirruNodeKind* = enum
    cirruString,
    cirruSeq

  CirruNode* = object
    line*: int
    column*: int
    case kind*: CirruNodeKind
    of cirruString:
      text*: string
    of cirruSeq:
      list*: seq[CirruNode]

跟 Clojure 的 dispatch function 有點類似, 須要選定字段專門用於表示類型,
後面都在運行時基於這個字段作判斷, 而後定義不一樣的邏輯,
我的感受遠遠沒有代數類型裏面的設計優雅, 也沒有 TypeScript 直觀, 可是使用當中仍是夠用的.

其餘

用了 Clojure 之後我比較習慣用 Persistent Data 和尾遞歸作抽象了,
固然, 尾遞歸相對來講是在編程語言里加限制了, 編碼的靈活性反而少一點,
此次在 Nim 當中爲了性能, 所有用的是可變數據的操做, 仍是蠻新鮮的...
確實用 mutable 寫法, 有點黑科技的感受, 算法很巧妙, 也很髒, 恰恰性能很快.

Nim 編譯器還支持 WebAssembly 和 JavaScript 的後端, 目前沒有用到.
最初選 Nim 有一個緣由也是考慮之後上手 WebAssembly 但願能夠方便一點吧.

目前使用比較淺, 數據結構用得也比較單一, 基本參考文檔仍是能解決.
完成的代碼在 https://github.com/Cirru/pars...等到後續有想法再記錄.

相關文章
相關標籤/搜索