Nim 語言有哪些特色

原文: http://hookrace.net/blog/what-is-special-about-nim/javascript


Nim 編程語言 很讓人振奮. 官方教程雖然很棒, 但只是慢吞吞介紹語言.
而我打算快速向你棧是用 Nim 能作的, 在其餘語言很難或者作不到的事情.html

我發現 Nim 是在我爲開發遊戲(HookRace)尋找一個正確的工具的時候,
這個遊戲是個人 DDNet 遊戲(mod of Teeworlds)後續的版本.
由於我最近忙着別的項目, 因此這個博客主要就關於 Nim 了, 直到我有時間繼續開發遊戲.java

容易運行

好吧, 這個部分不見得有意思, 但我邀請你跟着文章一塊兒來執行代碼:node

nimfor i in 0..10:
  echo "Hello World"[0..i]

這以前, 安裝 Nim 編譯器.
將代碼保存爲 hello.nim, 用 nim c hello 編譯, 再用 ./hello 運行二進制文件.
要同時編譯和運行, 使用 nim -r c hello.
要使用優化過 release build, 而不是 debug build 的話, 使用 nim -d:release c hello.
上面全部的配置你均可以看到下面的輸出:python

H
He
Hel
Hell
Hello
Hello
Hello W
Hello Wo
Hello Wor
Hello Worl
Hello World

在編譯期間運行普通代碼

要實現一個高效的 CRC32 程序你須要查一個表.
你能夠在運行時計算, 或者在源碼當中使用 magic array 寫好.
咱們這裏明確一下不想要任何的 magic number 出如今代碼中, 因此(這時候)咱們在運行時作:git

nimimport unsigned, strutils

type CRC32* = uint32
const initCRC32* = CRC32(-1)

proc createCRCTable(): array[256, CRC32] =
  for i in 0..255:
    var rem = CRC32(i)
    for j in 0..7:
      if (rem and 1) > 0: rem = (rem shr 1) xor CRC32(0xedb88320)
      else: rem = rem shr 1
    result[i] = rem

# Table created at runtime
var crc32table = createCRCTable()

proc crc32(s): CRC32 =
  result = initCRC32
  for c in s:
    result = (result shr 8) xor crc32table[(result and 0xff) xor ord(c)]
  result = not result

# String conversion proc $, automatically called by echo
proc `$`(c: CRC32): string = int64(c).toHex(8)

echo crc32("The quick brown fox jumps over the lazy dog")

好, 運行成功了, 咱們獲得輸出 414FA339.
可是若是咱們能在編譯過程當中計算 CRC 表就更好了.
在 Nim 當中這很是容易, 替換掉 crc32table 的代碼, 咱們使用:github

nim# Table created at compile time
const crc32table = createCRCTable()

是的, 就是這樣: 咱們索要作的僅僅是把 var 換成是 const. 很妙吧?
咱們能夠寫一樣的代碼, 讓它在運行時, 或者編譯時執行. 不須要 template, 不須要元編程.編程

對語言進行擴展

templatesmacros 可用於替換模版, 在編譯時變換代碼.json

templates 僅僅在是在編譯時把調用它們的代碼換成它們的實際代碼.
咱們能夠本身定義一個循環:瀏覽器

nimtemplate times(x: expr, y: stmt): stmt =
  for i in 1..x:
    y

10.times:
  echo "Hello World"

那麼編譯器就會吧 times 循環的代碼變換爲普通的 for 循環:

nimfor i in 1..10:
  echo "Hello World"

若是你想問 10.times 的語法.. 它就是一個一般的 times 的調用,
10 是這個調用的第一個參數, 後面跟着一個 block 做爲第二個參數
換句話說你也能夠寫 times(10);, 參考統一的調用語法.

或者更輕鬆地初始化序列(變長的 array):

nimtemplate newSeqWith(len: int, init: expr): expr =
  var result = newSeq[type(init)](len)
  for i in 0 .. <len:
    result[i] = init
  result

# 建立一個 2 維的序列, 大小爲 20,10
var seq2D = newSeqWith(20, newSeq[bool](10))

import math
randomize()
# 建立一個序列, 其中有 20 個隨機整數, 每一個小於 10
var seqRand = newSeqWith(20, random(10))
echo seqRand

macros 走得更遠異步, 讓你能分析還有操做 AST.
在 Nim 當中沒有列表剖析, 可是, 你能夠作到好比說[用 macro 把這個功能加進來].
那麼對於下面的代碼:

nimvar res: seq[int] = @[]
for x in 1..10:
  if x mod 2 == 0:
    res.add(x)
echo res

const n = 20
var result: seq[tuple[a,b,c: int]] = @[]
for x in 1..n:
  for y in x..n:
    for z in y..n:
      if x*x + y*y == z*z:
        result.add((x,y,z))
echo result

你能夠藉助 future 模塊寫成:

nimimport future
echo lc[x | (x <- 1..10, x mod 2 == 0), int]
const n = 20
echo lc[(x,y,z) | (x <- 1..n, y <- x..n, z <- y..n,
                   x*x + y*y == z*z), tuple[a,b,c: int]]

向編譯器加入加入你本身的優化

相對於優化本身的代碼, 你不會考慮把編譯器變得更聰明嗎? 在 Nim 你就能夠!

nimvar x: int
for i in 1..1_000_000_000:
  x += 2 * i
echo x

這些代碼(實際上沒啥用)課以經過教會編譯器兩種優化來加速:

nimtemplate optMul{`*`(a,2)}(a: int): int =
  let x = a
  x + x

template canonMul{`*`(a,b)}(a: int{lit}, b: int): int =
  b * a

第一個是 term rewriting template 咱們指定 a * 2 能夠替換爲 a + a.
第二個咱們指定乘法當中若是第一個是整型的字面量那麼 int 能夠被交換, 因而就有可能應用第一個 tempalte.

更復雜的模式也能夠實現, 好比優化 boolean 的邏輯:

nimtemplate optLog1{a and a}(a): auto = a
template optLog2{a and (b or (not b))}(a,b): auto = a
template optLog3{a and not a}(a: int): auto = 0

var
  x = 12
  s = x and x
  # Hint: optLog1(x) --> ’x’ [Pattern]

  r = (x and x) and ((s or s) or (not (s or s)))
  # Hint: optLog2(x and x, s or s) --> ’x and x’ [Pattern]
  # Hint: optLog1(x) --> ’x’ [Pattern]

  q = (s and not x) and not (s and not x)
  # Hint: optLog3(s and not x) --> ’0’ [Pattern]

s 直接被優化爲 x, 經過兩次連續的模式應用優化爲 2, q 立刻獲得 0.

若是你想看用 term rewriting tempalte 怎麼避免寫大整數的內存分配,
查一下 bigints 模塊 當中 opt 開頭的 templates:

nimimport bigints

var i = 0.initBigInt
while true:
  i += 1
  echo i

綁定你喜歡的 C 函數和類庫

由於 Nim 是編譯的 C 的, 外部函數接口頗有意思.

你能夠很容易用上 C 模塊庫當中你喜歡的函數:

nimproc printf(formatstr: cstring)
  {.header: "<stdio.h>", varargs.}
printf("%s %d\n", "foo", 5)

或者使用你本身用 C 寫的代碼:

cvoid hi(char* name) {
  printf("awesome %s\n", name);
}
nim{.compile: "hi.c".}
proc hi*(name: cstring) {.importc.}
hi "from Nim"

或者藉助 c2nim 使用任何你想要的:

控制垃圾回收

爲了達到 soft realtime, 你能夠指定垃圾收集器何時還有多久被容許運行.
遊戲的主要邏輯在 Nim 當中能夠這樣實現, 用來避免垃圾收集器致使卡頓:

nimgcDisable()
while true:
  gameLogic()
  renderFrame()
  gcStep(us = leftTime)
  sleep(restTime)

類型安全的集合和 enums 的 array

你常常用到數學的集合來容納你本身定義的值, 這是類型安全的寫法:

nimtype FakeTune = enum
  freeze, solo, noJump, noColl, noHook, jetpack

var x: set[FakeTune]

x.incl freeze
x.incl solo
x.excl solo

echo x + {noColl, noHook}

if freeze in x:
  echo "Here be freeze"

var y = {solo, noHook}
y.incl 0 # Error: type mismatch

你不會意外地加入另外一個類型的值. 集合的內部實現是一個高效的 bitvector.

對 array 來講能夠能夠的, 用 enum 來索引它們:

nimvar a: array[FakeTune, int]
a[freeze] = 100
echo a[freeze]

統一的調用語法

這只是語法糖, 可是有的話是很好的. 在 Python 裏我常忘記 lenappend 是函數是方法.
在 Nim 裏我不須要記憶, 由於兩種寫法是同樣. Nim 使用統一的調用語法,
這也被人提議給了 C++, 兩我的是 [Herb Sutter][HB] 和 Bjarne Stroustrup.

nimvar xs = @[1,2,3]

# Procedure call syntax
add(xs, 4_000_000)
echo len(xs)

# Method call syntax
xs.add(0b0101_0000_0000)
echo xs.len()

# Command invocation syntax
xs.add 0x06_FF_FF_FF
echo xs.len

優良的性能

用 Nim 寫高效的代碼很容易, 這能夠在 [Longest Path Finding Benchmark][BPATH] 看出來,
其中 Nim 用至關漂亮的代碼完成了.

測試最開始發佈的時候我在本身的機器上作了一些測算:
(Linux x86-64, Intel Core2Quad Q9300 @2.5GHz, state of 2014-12-20)

代碼體積的壓縮使用了 gzip -9 < nim.nim | wc -c. 也移除了 Haskell 中無用的代碼.
編譯時間就是完整的編譯, 若是你用 nimcache 緩存了標準庫的預編譯, Nim 就只要 323ms

我作過另外一個小的測試, 計算開頭 100M 天然數當中那些是質數, 對比 Python, Nim 和 C:

Python (運行時間: 35.1s)

pythondef eratosthenes(n):
  sieve = [1] * 2 + [0] * (n - 1)
  for i in range(int(n**0.5)):
    if not sieve[i]:
      for j in range(i*i, n+1, i):
        sieve[j] = 1
  return sieve

eratosthenes(100000000)

Nim(運行時間: 2.6s)

nimimport math

proc eratosthenes(n): auto =
  result = newSeq[int8](n+1)
  result[0] = 1; result[1] = 1

  for i in 0 .. int sqrt(float n):
    if result[i] == 0:
      for j in countup(i*i, n, i):
        result[j] = 1

discard eratosthenes(100_000_000)

C(運行時間: 2.6s)

c#include <stdlib.h>
#include <math.h>
char* eratosthenes(int n)
{
  char* sieve = calloc(n+1,sizeof(char));
  sieve[0] = 1; sieve[1] = 1;
  int m = (int) sqrt((double) n);

  for(int i = 0; i <= m; i++) {
    if(!sieve[i]) {
      for (int j = i*i; j <= n; j += i)
        sieve[j] = 1;
    }
  }
  return sieve;
}

int main() {
  eratosthenes(100000000);
}

編譯到 JavaScript

你能夠[把 Nim 編譯到 JavaScript], 而不是 C.
這樣你就能夠直接用 Nim 寫一些客戶端, 也能夠寫服務端.
咱們來寫一個服務端的訪問用戶統計, 在瀏覽器上顯示出來. 這是 client.nim:

nimimport htmlgen, dom

type Data = object
  visitors {.importc.}: int
  uniques {.importc.}: int
  ip {.importc.}: cstring

proc printInfo(data: Data) {.exportc.} =
  var infoDiv = document.getElementById("info")
  infoDiv.innerHTML = p("You're visitor number ", $data.visitors,
    ", unique visitor number ", $data.uniques,
    " today. Your IP is ", $data.ip, ".")

咱們定義 Data 類型, 用來從服務器傳遞給客戶端.
printInfo 程序會用 data 調用, 而後顯示.
使用 nim js client 編譯. 變異結果的 JavaScript 文件在 nimcache/client.js.

對於服務器咱們須要用到 Nimble 包管理器而後運行 nimble install jester.
以後咱們能夠用上 Jest Web 框架來寫 server.nim:

nimimport jester, asyncdispatch, json, strutils, times, sets, htmlgen, strtabs

var
  visitors = 0
  uniques = initSet[string]()
  time: TimeInfo

routes:
  get "/":
    resp body(
      `div`(id="info"),
      script(src="/client.js", `type`="text/javascript"),
      script(src="/visitors", `type`="text/javascript"))

  get "/client.js":
    const result = staticExec "nim -d:release js client"
    const clientJS = staticRead "nimcache/client.js"
    resp clientJS

  get "/visitors":
    let newTime = getTime().getLocalTime
    if newTime.monthDay != time.monthDay:
      visitors = 0
      init uniques
      time = newTime

    inc visitors
    let ip =
      if request.headers.hasKey "X-Forwarded-For":
        request.headers["X-Forwarded-For"]
      else:
        request.ip
    uniques.incl ip

    let json = %{"visitors": %visitors,
                 "uniques": %uniques.len,
                 "ip": %ip}
    resp "printInfo($#)".format(json)

runForever()

這個服務器就包含了主要的網頁. 一樣也包含了 client.js, 用過在編譯時讀取編譯 client.nim.
邏輯在 /visitors 當中處理.
nim -r c server 編譯運行, 打開 http://localhost:5000/ 查看效果.

你能夠在 Jester 生成的網站上看代碼執行效果, 或者下面內聯的:

內聯 HTML

尾聲

我但願我能激發你對 Nim 語言的興趣.

注意這門語言尚未徹底穩定下來. 特別是一些含糊的功能你可能會遇到 bug.
可是好的一面是, Nim 1.0 計劃在將來 3 個月裏發佈! 因此如今開始學習 Nim 是很好的時機.

獎勵: 由於 Nim 編譯到 C 並且只依賴 C 標準庫, 你能夠在任何地方部署,
包括 x86-64, ARM 和 Intel Xeon Phi accelerator cards.

評論的話用 Reddit, Hacker News, 或者在 IRC(#nim on freenode) 上直接問 Nim 社區
你能夠經過個人我的郵件 dennis@felsin9.de 找到我.

感謝 Andreas Rumpf 和 Dominik Picheta 審閱這篇文章.

相關文章
相關標籤/搜索