Язык программирования 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. Таким образом, он быстрее Go, Rust, 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, ваш код будет работать практически везде.
ссылка на оригинал статьи http://habrahabr.ru/post/258119/
Добавить комментарий