Що такого особливого в Nim?



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

Я відкрив для себе 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 = result not

# 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 .. <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 =
нехай 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 і 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 і 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 "be Here 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

Data type = object
відвідувачі {.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.відвідувачі,
", 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, ваш код буде працювати практично скрізь.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.