Pull to refresh

Что такого особенного в Nim?

Reading time 7 min
Views 80K
Original author: Dennis Felsing


Язык программирования Nim (ранее именовался Nimrod) — захватывающий! В то время как официальная документация с примерами плавно знакомит с языком, я хочу быстро показать вам что можно сделать с Nim, что было бы труднее или невозможно сделать на других языках.

Я открыл для себя Nim, когда искал правильный инструмент для написания игры, HoorRace, преемник моей текущей DDNet игры/мода Teeworlds.

(прим. пер. На синтаксис Nim имели влияние Modula 3, Delphi, Ada, C++, Python, Lisp, Oberon.)

Запускаем!


Да, эта часть всё ещё не захватывает, но просто следите за продолжением поста:

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


Для запуска, естественно, потребуется компилятор Nim (прим. пер. в ArchLinux, например, пакет есть community/nim). Сохраните этот код в файл hello.nim, скомпилируйте его при помощи nim c hello.nim, и, наконец, запустите исполняемый файл ./hello. Или воспользуйтесь командой nim -r c hello.nim, которая скомпилирует и запустит полученный файл. Для сборки оптимизированной версии воспользуйтесь командой nim -d:release c hello.nim. После запуска вы увидите вот такой вывод в консоль:

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


Исполняем код во время компиляции


Для реализации эффективной процедуры CRC32 вам понадобится предвычисленная таблица. Вы можете её вычислить во время выполнения программы или сохранить её в коде в виде магического массива. Конечно мы не хотим магических цифр в нашем коде, так что мы будем вычислять таблицу на запуске программы (по крайней мере сейчас):

import 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 на следующий код:

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

Да, верно, всё что нам нужно сделать, так это заменить var на const. Прекрасно, не правда ли? Мы можем писать один и тот же код, который можно исполнять как в работе программы, так и на этапе компиляции. Никакого шаблонного метапрограммирования.

Расширяем язык


Шаблоны и макросы могут быть использованы для избегания копирования и лапши в коде, при этом они будут обработаны на этапе компиляции.

Темплейты просто заменяются на вызовы соответствующих функций во время компиляции. Мы можем определить наши собственные циклы вот так:

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

10.times:
  echo "Hello World"


Компилятор преобразует times в обычный цикл:

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


Если вас заинтересовал синтаксис 10.times, то знайте, что это просто обычный вызов times с первым аргументом 10 и блоком кода в качестве второго аргумента. Вы могли просто написать: times(10):, подробнее смотрите о Unified Call Syntax ниже.

Или инициализируйте последовательности (массивы произвольной длинны) удобнее:

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

# Create a 2-dimensional sequence of size 20,10
var seq2D = newSeqWith(20, newSeq[bool](10))

import math
randomize()
# Create a sequence of 20 random integers smaller than 10
var seqRand = newSeqWith(20, random(10))
echo seqRand


Макрос заходит на шаг дальше и позволяет вам анализировать и манипулировать AST. Например, в Nim нет списковых включений (прим. пер. list comprehensions), но мы можем добавить их в язык при помощи макроса. Теперь вместо:

var 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 и писать:

import 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 это возможно!

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

Этот (достаточно бесполезный) код может быть ускорен при помощи обучения компилятора двум оптимизациям:

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

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

В первом шаблоне мы указываем, что a * 2 может быть заменено на a + a. Во втором шаблоне мы указываем что int-переменные могут быть поменяны местами, если первый агрумент — число-константа, это нужно чтобы мы могли применить первый шаблон.

Более сложные шаблоны также могут быть реализованы, например, для оптимизации булевой логики:

template 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, r тоже оптимизируется до x, и q сразу инициализируется нулём.

Если вы хотите увидеть как применяются шаблоны для избегания выделения bigint, посмотрите на шаблоны, начинающиеся с opt в библиотеке biginsts.nim:

import bigints

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


Подключайте свои C-функции и библиотеки


Так как Nim транслируется в C (C++/Obj-C), использование сторонних функций не составляет никакой проблемы.

Вы можете легко использовать ваши любимые функции из стандартной библиотеки:

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


Или использовать свой собственный код, написанный на C:

void hi(char* name) {
  printf("awesome %s\n", name);
}

{.compile: "hi.c".}
proc hi*(name: cstring) {.importc.}
hi "from Nim"


Или любой библиотеки, какой пожелаете, при помощи c2nim:

proc set_default_dpi*(dpi: cdouble) {.cdecl,
  importc: "rsvg_set_default_dpi",
  dynlib: "librsvg-2.so".}


Управление сборщиком мусора


Для достижения «soft realtime», вы можете сказать сборщику мусора когда и сколько он может работать. Основная логика игры с предотвращением вмешательства сборщика мусора может быть реализована на Nim примерно вот так:

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


Типобезопасные множества и enum


Часто вам может быть нужно математическое множество со значениями, которые вы определили самостоятельно. Вот так можно это реализовать с уверенностью, что типы будут проверены компилятором:

type 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


Вы не можете случайно добавить значение другого типа. Внутренне это работает как эффективный битовый вектор.

То же самое возможно и с массивами, индексируйте их с помощью enum.

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


Unified Call Syntax


Это просто синтаксический сахар, но это определённо очень удобно (прим. пер. я считаю это ужасным!). В Python я всегда забываю является len и append функциями или методами. В Nim вам не нужно это помнить, потому что можно писать как угодно. Nim использует Unified Call Syntax (синтаксис унифицированного вызова), который также сейчас предложен в C++ товарищами Herb Sutter и Bjarne Stroustrup.

var 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


Производительность


(прим. пер. этот раздел в оригинальной статье «устарел», поэтому предлагаю ссылки на оригинальный обновлённый benchmark и benchmark, приведённый в оригинале статьи)

От переводчика:

Если кратко, то Nim генерирует код, который так же быстр, как и написанный человеком C/C++. Nim может транслировать код в C/C++/Obj-C (а ниже будет показано, что может и в JS) и компилировать его gcc/clang/llvm_gcc/MS-vcc/Intel-icc. Как показывают искуственные тесты, Nim сравним по скорости с C/C++/D/Rust и быстрее Go, Crystal, Java и многих других.

Транслируем в JavaScript


Nim может транслировать Nim код в JavaScript. Это позволяет писать и клиентский, и серверный код на Nim. Давайте сделаем маленький сайт, который будет считать посетителей. Это будет наш client.nim:

import 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 будет вызвана с этими данными для отображения. Для сборки нашего клиентского кода выполним команду nim js client. Результат будет сохранён в nimcache/client.js.

Для сервера нам понадобится пакетный менеджер Nimble, так как нам нужно будет установить Jester (sinatra-подобный web framework для Nim). Устанавливаем Jester: nimble install jester. Теперь напишем наш server.nim:

import 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()


При открытии http://localhost:5000/ сервер будет возвращать «пустую» страницу с подключёнными /client.js и /visitors. /client.js будет возвращать файл, полученный через nim js client, а /visitors будет генерировать JS код с вызовом printInfo(JSON).

Вы можете увидеть полученный Jester сайт онлайн, он будет показывать вот такую строку:

You're visitor number 11, unique visitor number 11 today. Your IP is 134.90.126.175.


Заключение


Я надеюсь, я смог заинтересовать языком программирования Nim.
Обратите внимание, что язык ещё не полностью стабилен. Однако, Nim 1.0 уже не за горами. Так что это отличное время для знакомства с Nim!

Бонус: так как Nim транслируется в C и зависит только от стандартной библиотеки C, ваш код будет работать практически везде.
Tags:
Hubs:
+51
Comments 93
Comments Comments 93

Articles