[Перевод] Что такого особенного в Nim?
Язык программирования 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©] result = not result
# String conversion proc $, automatically called by echo proc `$`(c: CRC32): string = int64©.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 … # 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
Так как Nim транслируется в C (C++/Obj-C), использование сторонних функций не составляет никакой проблемы.Вы можете легко использовать ваши любимые функции из стандартной библиотеки:
proc printf (formatstr: cstring)
{.header:»
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)
Часто вам может быть нужно математическое множество со значениями, которые вы определили самостоятельно. Вот так можно это реализовать с уверенностью, что типы будут проверены компилятором:
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]
Это просто синтаксический сахар, но это определённо очень удобно (прим. пер. я считаю это ужасным!). В 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 0×06_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 и многих других.
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, ваш код будет работать практически везде.