- PVSM.RU - https://www.pvsm.ru -
Привет!
Уже довольно давно я пишу свой игровой фреймворк — такой pet project для души. А так как для души нужно выбирать что-то, что нравится (а в данном случае — на чём нравится писать), то выбор мой пал на nim. В этой статье я хочу поговорить именно про nim, про его особенности, плюсы и минусы, а тема геймдева лишь задаёт контекст моего опыта — какие задачи я решал, какие трудности возникли.
Давным-давно, когда трава была зеленее, а небо чище, я встретил nim. Хотя нет, не так. Давным-давно я хотел заниматься разработкой игр, чтобы написать свою Самую Классную Игру — думаю, многие проходили через это. В те времена Unity и Unreal Engine только-только стали появляться на слуху и, вроде как, ещё не были бесплатными. Я не стал их использовать, не столько из-за жадности, сколько из-за желания написать всё самому, создать игровой мир полность с нуля, с самого первого нулевого байта. Да, долго, да, сложно, зато сам процесс приносит удовольствие — а что ещё для счастья надо?
Вооружившись Страуструпом и Qt, я хлебнул говна по самое небалуй, потому что, во-первых, не был одним из 10 человек в мире, знающих C++ хорошо, а, во-вторых, плюсы активно вставляли мне палки в колёса. Не вижу смысла повторять то, что за меня уже замечательно написал platoff [1]:
Как я нашел лучший в мире язык программирования. Часть 1 [2]
Как я нашел лучший в мире язык программирования. Часть 2 [3]
Как я нашел лучший в мире язык программирования. Часть Йо (2.72) [4]
Это безумный кайф, когда ты пишешь код свободно, почти не думая, не ожидая core dumped перед каждым запуском, когда фичи добавляются прямо на глазах, вот теперь мы так можем, а теперь еще так, то скажите мне пожалуйста, какая мне разница что у меня нет темплейтов, если я даже не скучал по ним? Продуктивность — вот главная цель программиста, который делает вещи, и единственная задача инструмента который он использует.
Работая с C++, я постоянно думал, как мне написать то, что я хочу, а не что мне написать. Поэтому я перешёл на nim. С историей покончено, давайте же я поделюсь с вами опытом после нескольких лет работы на nim.
Вообще-то с этим плохо. Проблемы:
Пример: хотите вы написать многопоточное приложения, ядер-то много, а девать некуда.
Вот раздел официальной документации про потоки [8]. Нет, понимаете, потоки — это отдельная большая часть языка, его фича, которую даже нужно включать флагом --threads:on
при компиляции. Там свои сборщики мусора, local heap, всякие shared memory и locks, thread safety, специальные shared-модули и хрен знает что ещё. Откуда я про это всё узнал? Правильно, из книги nim in action, форума, stack overflow, телевизора и от соседа, в общем откуда угодно, но не из официальной документации.
Или вот есть т.н. "do notation" — очень хорошо заходит при использовании шаблонов и тд, вообще везде где надо передать callback или просто блок кода. Где про это можно почитать? Ага, в мануале по экспериметальным фичам [9].
Согласитесь, собирать информацию по разным малоинформативным источникам — то ещё удовольствие. Если вы пишете на nim — вам придётся это делать.
На форуме и в github issues проскакивали предложения по улучшению документации, но дело так и не сдвинулось. Мне кажется, не хватает какой-то жёсткой руки, которая скажет "всё, комьюнити, берём лопаты и идём разгребать эту кучу г… ениальных разрозненных кусков текста."
К счастью, я отстрадал своё, поэтому представляю вам список nim-чемпиона
Нет смысла перечислять все возможности языка, но вот некоторые особенности:
Nim предоставляет вам "фрактал сложности". Вы можете писать высокоуровневый код. Можете бодаться с сырыми указателями и радоваться каждой attempt to read from nil
. Можете вставлять C-код. Можете писать вставки на ассемблере [31]. Можете писать процедуры (static dispatch). Не хватает — есть "методы" (dynamic dispatch). Ещё? Есть дженерики, и есть дженерики, мимикрирующие под функции. Есть шаблоны (templates) — механизм замены, но не такой блевотный, как в C++ (там это всё ещё просто текстовая замена, или уже что-то поумнее?). Есть макросы, в конце концов — это как IDDQD, они включают режим бога и позволяют работать напрямую с AST и буквально заменять куски синтаксического дерева, или самостоятельно расширять язык как хотите.
То есть на "высоком" уровне вы можете писать хелловорлды и горя не знать, но никто вам не запрещает проводить махинации любой сложности.
Кривая обучения — не кривая. Это прямая. Установив nim, вы в первую же минуту запустите ваш первый hello world, а в первый же день вы напишете простую утилиту. Но и через пару месяцев вам будет что изучать. Например, я начинал с процедур, потом мне понадобились методы, через какое-то время мне очень пригодились дженерики, недавно я открыл для себя шаблоны в полной их красе, и при этом я ещё вообще не трогал макросы. Сравнивая с тем же rust или c++, "влиться" в nim гораздо проще.
Есть package manager под названием nimble, которые умеет устанавливать, удалять, создавать пакеты и подгружать зависимоти. Когда создаёте свой пакет (= проект), в nimble можно прописать разные задачи (при помощи nimscript, который подмножество nim, исполняемый на VM), например, генерацию документации, запуск тестов, копирование ассетов итд. Nimble не только поставит нужные зависимости, но и вообще позволит сконфигурировать рабочее окружение для вашего проекта. То есть nimble — это, грубо говоря, CMake, который написали не извращенцы, а нормальные люди.
Внешне nim очень похож на python с type annotations, хотя nim это не python вообще ни разу. Питонистам придётся забыть динамическую типизацию, наследование, декораторы и прочие радости, и вообще перестроить
Вот пример программы на nim:
type
NumberGenerator = object of Service # this service just generates some numbers
NumberMessage = object of Message
number: int
proc run(self: NumberGenerator) =
if not waitAvailable("calculator"):
echo "Calculator is unavailable, shutting down"
return
for number in 0..<10:
echo &"Sending number {number}"
(ref NumberMessage)(number: number).send("calculator")
Всё разбито на модули, которые можно как угодно импортировать — импортировать только определённые символы, или все кроме определённых, или все, или ни одного и заставить пользователя указывать полный путь а-ля module.function()
, и ещё импортировать под другим именем. Разумеется, всё это многообразие очень пригодится как ещё один агрумент в споре "какой язык программирования лучше", ну а в своём проекте вы будете тихонько везде писать import mymodule
и о других вариантах не вспоминать.
Вызов функции может быть записан по-разному:
double(2)
double 2
2.double()
2.double
С одной стороны, теперь каждый… пишет как ему нравится (а всем нравится по-разному, разумеется, причём по-разному даже в рамках одного проекта). Но зато все функции могут быть записаны как вызов метода, что очень сильно улучшает читаемость. В питоне может быть такое:
list(set(some_list)) # араб-стайл: читаем справа налево, а ещё можно добавить map и filter и уехать в дурку
Тот же код в nim можно было бы переписать более логично:
some_list.set.list # читаем слева направо
ООП хоть и присутствует, но отличается от оного в плюсах и питоне: объекты и методы — разные сущности, и вполне могут существовать в разных модулях. Более того, вы можете написать свои методы для базовых типов вроде int
proc double(number: int): int =
number * 2
echo $2.double() # prints "4"
С другой стороны, в nim присутствует инкапсуляция (первое правило модуля в nim: никому не рассказывать о идентификаторах без символа звёздочки). Вот пример стандартного модуля:
# sharedtables.nim
type SharedTable*[A, B] = object ## generic hash SharedTable
data: KeyValuePairSeq[A, B]
counter, dataLen: int
lock: Lock
Тип SharedTable*
помечен звёздочкой, значит, он "виден" в других модулях и его можно импортировать. Но вот data
, counter
и lock
— приватные члены, и "снаружи" sharedtables.nim
они недоступны. Это меня очень обрадовало, когда я решил написать некоторые дополнительные функции для типа SharedTable
, навроде len
или hasKey
, и обнаружил, что у меня нет доступа ни к counter
, ни к data
, и единственный способ "расширить" SharedTable
— написать свой, с бл
Вообще наследование используется намного реже, чем в том же питоне (по личному опыту), потому что есть method call syntax (см. выше) и Object Variants (см ниже). Путь nim — это скорее композиция, а не наследование. Так же и с полиморфизмом: в nim'е есть методы, которые могут быть переопределены в классах-наследниках, но это нужно явно указать при компиляции, используя флаг --multimethods:on
. То есть по умолчанию методы не работают, что слегка подталкивает к работе без оных.
Const — возможность вычислять что-то на этапе компиляции и "зашивать" это в результирующий бинарник. Это круто и удобно. Вообще в nim особое отношение ко "времени компиляции", даже есть ключевое слово when
— это как if
, но сравнение идёт на этапе компиляции. Можно написать что-то вроде when defined(linux): echo "Compiling on linux machine"
— и это очень удобно, хотя и есть ограничения на то, что можно вытворять на этапе компиляции (например, нельзя делать FFI вызовы).
Ref type — аналог shared_ptr в C++, о котором позаботится сборщик мусора. Но можно и самому вызывать сборщик мусора в те моменты, когда это вам удобно. А можно попробовать разные варианты сборщиков мусора. А можно вообще отключить сборщик мусора и использовать обычные указатели.
В идеале, если не использовать сырые указатели и FFI, вы вря ли сможете получить ошибки сегментации. На практике пока без FFI никуда.
Есть анонимные процедуры (aka лямбды в питоне), но в отличие от питона в анонимной процедуре можно использовать несколько statements:
someProc(callback=proc(a: int) -> int = var b = 5*a; result = a)
Есть исключения, их очень неудобно бросать: на python raise ValueError('bad value')
, на nim raise newException(ValueError, "bad value)
. Больше ничего необычного — try, except, finally, всё как у всех. Я, как сторонник исключений, а не кодов ошибок, ликую. Кстати, для функций можно указывать, какие исключения они могут бросить, и компилятор будет это проверять:
proc p(what: bool) {.raises: [IOError, OSError].} =
if what: raise newException(IOError, "IO")
else: raise newException(OSError, "OS")
Дженерики очень выразительные, например, можно ограничивать возможные типы
proc onlyIntOrString[T: int|string](x, y: T) = discard # только int и string
А можно передавать тип вообще как параметр — выглядит как обычная функция, а на самом деле дженерик:
proc p(a: typedesc; b: a) = discard
# is roughly the same as:
proc p[T](a: typedesc[T]; b: T) = discard
# hence this is a valid call:
p(int, 4)
# as parameter 'a' requires a type, but 'b' requires a value.
Шаблоны (templates) — что-то вроде шаблонов в C++, только сделанных правильно :) — вы можете безопасно передавать в шаблоны целые блоки кода, и не думать о том, что подстановка что-то испортит в outer коде (но можно, опять же, сделать, чтобы испортила, если очень надо).
Вот пример шаблона app
, который в зависимости от значения переменной вызывает один из блоков кода:
template app*(serverCode: untyped, clientCode: untyped) =
# ...
case mode
of client:
clientCode
of server:
serverCode
else:
discard
При помощи do
я могу передавать целы блоки в шаблон, например:
app do: # serverCode
echo "I'm server"
serverProc()
do: # clientCode
echo "I'm client"
clientProc()
Если нужно быстро что-то протестировать, то есть возможность вызвать "интерпретатор" или "nim shell" (как если вы запустите python
без параметров). Для этого воспользуйтесь командой nim secret
или скачайте пакет inim [33].
FFI — возможность взаимодействовать со сторонними библиотеками на C/C++. К сожалению, для использования внешней библиотеки вы должны написать враппер, объясняющий, откуда и что импортировать. Например:
{.link: "/usr/lib/libOgreMain.so".}
type ManualObjectSection* {.importcpp: "Ogre::ManualObject::ManualObjectSection", bycopy.} = object
Есть инструменты, делающие этот процесс полуавтоматическим:
Слишком много всего. Язык задумывался как минималистичный, но сейчас это очень далеко от правды. Вот например за что мы получили code reordering [36]?!
Много говнища: system.addInt [37] — "Converts integer to its string representation and appends it to result". Мне кажется, это очень удобная функция, я её использую в каждом проекте. Вот ещё интересное: fileExists and existsFile (https://forum.nim-lang.org/t/3636 [38])
"There's only one way to do smth" — вообще нет:
fmt
vs &
[39]Баги есть, примерно 1400 [43]. Или просто зайдите на форум — там [44] постоянно [45] какие-то [46] баги находят.
В дополнение к предыдущему пункту, v1 подразумевает стабильность, да? И тут на форум залетает создатель языка Araq и говорит: "чуваки, я тут запилил ещё один (шестой) [47] сборщик мусора, он круче, быстрее, молодёжнее, даёт вам shared memory для потоков (ха-ха, а раньше для этого вы страдали и использовали костыли), качайте develop ветку и пробуйте". И все такие "Вау, как круто! А что это значит для простых смертных? Нам теперь опять весь код менять?" [48] Вроде как нет, поэтому я обновляю nim, запускаю новый сборщик мусора --gc:arc
и моя программа падает где-то на этапе компиляции c++ кода (т.е. не в nim, а в gcc):
/usr/lib/nim/system.nim:274:77: error: ‘union pthread_cond_t’ has no member named ‘abi’
274 | result = x
Великолепно! Теперь вместо того, чтобы писать новый код, я должен чинить старый. Не от этого ли я бежал, когда выбирал nim?
Приятно осознавать, что я не один [49]
По умолчанию флаги multimethods и threads выключены — вы ведь не собираетесь в 2019 2020 году писать многопоточное приложение с переопределением методов?! А уж как здорово, если ваша библиотека создавалась без учёта потоков, а потом пользователь их включил [50]… Ах да, для наследования есть замечательные прагмы {.inheritable.} и {.base.}, чтобы ваш код не был слишком лаконичен.
Вы можете избежать наследования, используя т.н. object variants:
type
CoordinateSystem = enum
csCar, # Cartesian
csCyl, # Cylindrical
Coordinates = object
case cs: CoordinateSystem: # cs is the coordinate discriminator
of csCar:
x: float
y: float
z: float
of csCyl:
r: float
phi: float
k: float
В зависимости от значения cs
, вам будут доступны либо x, y, z поля, либо r, phi и k.
В чём минусы?
Во-первых, память резервируется для всех полей, хотя каждый объект использует только часть полей.
Во-вторых, наследование всё равно более гибкое — всегда можете создать потомка и добавить ещё полей, а в object variant все поля жёстко заданы в одной секции.
В-третьих, что бесит больше всего — нельзя "переиспользовать" [51] поля в разных типах:
type
# The 3 notations refer to the same 3-D entity, and some coordinates are shared
CoordinateSystem = enum
csCar, # Cartesian (x,y,z)
csCyl, # Cylindrical (r,φ,z)
Coordinates = object
case cs: CoordinateSystem: # cs is the coordinate discriminator
of csCar:
x: float
y: float
z: float # z already defined here
of csCyl:
r: float
phi: float
z: float # fails to compile due to redefinition of z
Просто процитирую [9]:
Итак, у нас есть функции, процедуры, дженерики, мультиметоды, шаблоны и макросы. Когда лучше использовать шаблон, а когда процедуру? Шаблон или дженерик? Функция или процедура? Так, а макросы? Я думаю, вы поняли.
В питоне есть декораторы, которые можно применять хоть к классам, хоть к функциям.
В nim для этого есть прагмы. И вот что:
proc fib(n : int) : int {.cached.} =
# do smth
Что мертво — умереть не может. В nimble куча проектов, которые уже давно не обновлялись (а в nim это смерти подобно) — и их не убирают. Никто за этим не следит. Понятно, обратная совместимость, "нельзя просто взять и удалить пакет из репы", но всё же...
Есть такой закон дырявых абстракций [53] — вы используете какую-то абстракцию, но рано или поздно вы обнаружете в ней "дыру", которая приведёт вас на уровень ниже. Nim — это абстракция над C и C++, и рано или поздно вы туда "провалитесь". Спорим, вам там не понравится?
Error: execution of an external compiler program 'g++ -c -w -w -fpermissive -pthread -I/usr/lib/nim -I/home/user/c4/systems/network -o
/home/user/.cache/nim/enet_d/@m..@s..@s..@s..@s..@s..@s.nimble@spkgs@smsgpack4nim-0.3.0@smsgpack4nim.nim.cpp:6987:136: note: initializing argument 2 of ‘void unpack_type__k2dhaoojunqoSwgmQ9bNNug(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA*, NU&)’
6987 | N_LIB_PRIVATE N_NIMCALL(void, unpack_type__k2dhaoojunqoSwgmQ9bNNug)(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA* s, NU& val) { nimfr_("unpack_type", "/home/user/.nimble/pkgs/msgpack4nim-0.3.0/msgpack4nim.nim");
|
/usr/bin/ld: /home/user/.cache/nim/enet_d/stdlib_dollars.nim.cpp.o: in function `dollar___uR9bMx2FZlD8AoPom9cVY9ctA(tyObject_ConnectMessage__e5GUVMJGtJeVjEZUTYbwnA*)':
stdlib_dollars.nim.cpp:(.text+0x229): undefined reference to `resizeString(NimStringDesc*, long)'
/usr/bin/ld: stdlib_dollars.nim.cpp:(.text+0x267): undefined reference to `resizeString(NimStringDesc*, long)'
/usr/bin/ld: stdlib_dollars.nim.cpp:(.text+0x2a2): undefined reference to `resizeString(NimStringDesc*, long)'
Я тупой программист. Я не хочу знать, как работает GC, что там и как линкуется, куда кэшируется и как убирается мусор. Это как с машиной — я в принципе знаю, как она устроена, немного про сход-развал, немного про коробку передач, масло там надо заливать и прочее, но вообще я просто хочу сесть и ехать (причём быстро) на вечеринку. Машина — не цель, а средство достижения цели. Если она сломается — я не хочу лезть в капот, а просто отвезу её на сервис (в смысле, открою issue на гитхабе), и было бы здорово, если бы чинили её быстро.
Nim должен был стать такой машиной. Отчасти он и стал, но в то же время, когда я мчусь на этой машине по хайвею, у меня отваливается колесо, а заднее зеркало показывает вперёд. За мной бегут инженеры и на ходу что-то приделывают ("теперь с этим новым спойлером ваша машина ещё быстрее"), но от этого у меня отваливается багажник. И знаете что? Мне всё равно чертовски нравится эта машина, ведь это лучшая из всех машин, что я видел.
Автор: kesn
Источник [54]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/341433
Ссылки в тексте:
[1] platoff: https://habr.com/ru/users/platoff/
[2] Как я нашел лучший в мире язык программирования. Часть 1: https://habr.com/ru/post/259831/
[3] Как я нашел лучший в мире язык программирования. Часть 2: https://habr.com/ru/post/259841/
[4] Как я нашел лучший в мире язык программирования. Часть Йо (2.72): https://habr.com/ru/post/260149/
[5] Nim in action: https://www.manning.com/books/nim-in-action
[6] Status: https://github.com/status-im
[7] вышла версия 1.0: https://nim-lang.org/blog/2019/09/23/version-100-released.html
[8] Вот раздел официальной документации про потоки: https://nim-lang.org/docs/manual.html#threads
[9] мануале по экспериметальным фичам: https://nim-lang.org/docs/manual_experimental.html#do-notation
[10] Tutorial 1: https://nim-lang.org/docs/tut1.html
[11] Tutorial 2: https://nim-lang.org/docs/tut2.html
[12] Nim in action: https://book.picheta.me/
[13] Nim manual: https://nim-lang.org/docs/manual.html
[14] Nim experimental manual: https://nim-lang.org/docs/manual_experimental.html
[15] The Index: https://nim-lang.org/docs/theindex.html
[16] Nim basics: https://narimiran.github.io/nim-basics/
[17] Nim Days: https://xmonader.github.io/nimdays/
[18] Rosetta Code: http://rosettacode.org/wiki/Category:Nim
[19] Exercism.io: https://exercism.io/my/tracks/nim
[20] Nim by Example: https://nim-by-example.github.io/getting_started/
[21] Nim forum: https://forum.nim-lang.org
[22] Nim telegram group: https://t.me/nim_lang
[23] IRC: http://irc://freenode.net/nim
[24] Nim playground: http://play.nim-lang.org
[25] Nim docker cross-compiling: https://forum.nim-lang.org/t/5569
[26] nimble.directory: https://nimble.directory/
[27] Curated list of packages: https://github.com/nim-lang/Nim/wiki/Curated-Packages
[28] Imports in Nim: https://narimiran.github.io//2019/07/01/nim-import.html
[29] Nim for python programmers: https://github.com/nim-lang/Nim/wiki/Nim-for-Python-Programmers
[30] Nim for C programmers: https://github.com/nim-lang/Nim/wiki/Nim-for-C-programmers
[31] вставки на ассемблере: https://nim-lang.org/docs/manual.html#statements-and-expressions-assembler-statement
[32] мышление: http://www.braintools.ru
[33] inim: https://github.com/AndreiRegiani/INim
[34] c2nim: https://github.com/nim-lang/c2nim
[35] nimterop: https://github.com/nimterop/nimterop
[36] code reordering: https://nim-lang.org/docs/manual.html#scope-rules-code-reordering
[37] system.addInt: https://nim-lang.org/docs/system.html#addInt%2Cstring%2Cint64
[38] https://forum.nim-lang.org/t/3636: https://forum.nim-lang.org/t/3636
[39] fmt
vs &
: https://nim-lang.org/docs/strformat.html#fmt-vsdot-amp
[40] function: https://nim-lang.org/docs/manual.html#procedures-func
[41] procedure: https://nim-lang.org/docs/manual.html#procedures
[42] template: https://nim-lang.org/docs/manual.html#templates
[43] примерно 1400: https://github.com/nim-lang/Nim/issues
[44] там: https://forum.nim-lang.org/t/5620
[45] постоянно: https://forum.nim-lang.org/t/5615
[46] какие-то: https://forum.nim-lang.org/t/5590
[47] ещё один (шестой): https://nim-lang.org/docs/gc.html#garbage-collector-options
[48] "Вау, как круто! А что это значит для простых смертных? Нам теперь опять весь код менять?": https://forum.nim-lang.org/t/5734
[49] не один: https://forum.nim-lang.org/t/5746
[50] пользователь их включил: https://forum.nim-lang.org/t/5321#33482
[51] нельзя "переиспользовать": https://forum.nim-lang.org/t/5729
[52] не можете: https://forum.nim-lang.org/t/3705
[53] закон дырявых абстракций: https://en.wikipedia.org/wiki/Leaky_abstraction
[54] Источник: https://habr.com/ru/post/462577/?utm_source=habrahabr&utm_medium=rss&utm_campaign=462577
Нажмите здесь для печати.