Что такого особенного в 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(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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *