5

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


Nim 编程语言 很让人振奋. 官方教程虽然很棒, 但只是慢吞吞介绍语言.
而我打算快速向你栈是用 Nim 能做的, 在其他语言很难或者做不到的事情.

我发现 Nim 是在我为开发游戏(HookRace)寻找一个正确的工具的时候,
这个游戏是我的 DDNet 游戏(mod of Teeworlds)后续的版本.
因为我最近忙着别的项目, 所以这个博客主要就关于 Nim 了, 直到我有时间继续开发游戏.

容易运行

好吧, 这个部分不见得有意思, 但我邀请你跟着文章一起来执行代码:

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.
上面所有的配置你都可以看到下面的输出:

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

在编译期间运行普通代码

要实现一个高效的 CRC32 程序你需要查一个表.
你可以在运行时计算, 或者在源码当中使用 magic array 写好.
我们这里明确一下不想要任何的 magic number 出现在代码中, 所以(这时候)我们在运行时做:

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 的代码, 我们使用:

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

是的, 就是这样: 我们索要做的仅仅是把 var 换成是 const. 很妙吧?
我们可以写同样的代码, 让它在运行时, 或者编译时执行. 不需要 template, 不需要元编程.

对语言进行扩展

templatesmacros 可用于替换模版, 在编译时变换代码.

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 审阅这篇文章.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论