Мой опыт разработки на языке Nim


    Привет, Хабр!


    Уже довольно давно я пишу свой игровой фреймворк — такой pet project для души. А так как для души нужно выбирать что-то, что нравится (а в данном случае — на чём нравится писать), то выбор мой пал на nim. В этой статье я хочу поговорить именно про nim, про его особенности, плюсы и минусы, а тема геймдева лишь задаёт контекст моего опыта — какие задачи я решал, какие трудности возникли.


    Давным-давно, когда трава была зеленее, а небо чище, я встретил nim. Хотя нет, не так. Давным-давно я хотел заниматься разработкой игр, чтобы написать свою Самую Классную Игру — думаю, многие проходили через это. В те времена Unity и Unreal Engine только-только стали появляться на слуху и, вроде как, ещё не были бесплатными. Я не стал их использовать, не столько из-за жадности, сколько из-за желания написать всё самому, создать игровой мир полность с нуля, с самого первого нулевого байта. Да, долго, да, сложно, зато сам процесс приносит удовольствие — а что ещё для счастья надо?


    Вооружившись Страуструпом и Qt, я хлебнул говна по самое небалуй, потому что, во-первых, не был одним из 10 человек в мире, знающих C++ хорошо, а, во-вторых, плюсы активно вставляли мне палки в колёса. Не вижу смысла повторять то, что за меня уже замечательно написал platoff:


    Как я нашел лучший в мире язык программирования. Часть 1
    Как я нашел лучший в мире язык программирования. Часть 2
    Как я нашел лучший в мире язык программирования. Часть Йо (2.72)


    Это безумный кайф, когда ты пишешь код свободно, почти не думая, не ожидая core dumped перед каждым запуском, когда фичи добавляются прямо на глазах, вот теперь мы так можем, а теперь еще так, то скажите мне пожалуйста, какая мне разница что у меня нет темплейтов, если я даже не скучал по ним? Продуктивность — вот главная цель программиста, который делает вещи, и единственная задача инструмента который он использует.

    Работая с C++, я постоянно думал, как мне написать то, что я хочу, а не что мне написать. Поэтому я перешёл на nim. С историей покончено, давайте же я поделюсь с вами опытом после нескольких лет работы на nim.


    Общие сведения для тех, кто не в курсе


    • Компилятор открытый (MIT), разрабатывается энтузиастами. Создатель языка — Andreas Rumpf (Araq). Второй разработчик — Dominik Picheta (dom96), написавший книгу Nim in action. Также некоторое время назад компания Status стала спонсировать разработку языка, благодаря чему у nim'а появились ещё 2 фуллтайм-разработчика. Помимо них, разумеется, контрибутят и другие люди.
    • Недавно вышла версия 1.0, а это значит, что язык стабилен и "breaking changes" больше не ожидаются. Если раньше вы не хотели использовать unstable версию, потому что обновления могли сломать приложение, то теперь самое время попробовать nim в своих проектах.
    • Nim компилируется (или транспилируется) в C, C++ (которые далее компилируются в нативный код) или JS (с некоторыми ограничениями). Соотвественно, при помощи FFI вам доступны все существующие библиотеки для C и C++. Если нет нужного пакета на nim — поищите на си или плюсах.
    • Ближайшие языки — python (по синтаксису, на первый взгляд) и D (по функционалу) — имхо

    Документация


    Вообще-то с этим плохо. Проблемы:


    1. Документация раскидана по разным источникам
    2. Документация гавно не в полной мере описывает все возможности языка
    3. Документация порой слишком лаконична

    Пример: хотите вы написать многопоточное приложения, ядер-то много, а девать некуда.
    Вот раздел официальной документации про потоки. Нет, понимаете, потоки — это отдельная большая часть языка, его фича, которую даже нужно включать флагом --threads:on при компиляции. Там shared или thread-local heap в зависимости от сборщика мусора, всякие shared memory и locks, thread safety, специальные shared-модули и хрен знает что ещё. Откуда я про это всё узнал? Правильно, из книги nim in action, форума, stack overflow, телевизора и от соседа, в общем откуда угодно, но не из официальной документации.


    Или вот есть т.н. "do notation" — очень хорошо заходит при использовании шаблонов и тд, вообще везде где надо передать callback или просто блок кода. Где про это можно почитать? Ага, в мануале по экспериметальным фичам.


    Согласитесь, собирать информацию по разным малоинформативным источникам — то ещё удовольствие. Если вы пишете на nim — вам придётся это делать.


    На форуме и в github issues проскакивали предложения по улучшению документации, но дело так и не сдвинулось. Мне кажется, не хватает какой-то жёсткой руки, которая скажет "всё, комьюнити, берём лопаты и идём разгребать эту кучу г… ениальных разрозненных кусков текста."


    К счастью, я отстрадал своё, поэтому представляю вам список nim-чемпиона


    Документация


    • Tutorial 1, Tutorial 2 — с них начинать
    • Nim in action — толковая книжка, которая действительно хорошо объясняет многие аспекты языка, порой намного лучше оф. документации
    • Nim manual — собственно, мануал — описано практически всё, но нет
    • Nim experimental manual — а почему бы, собственно, не продолжить документацию на отдельной страничке?
    • The Index — тут собраны ссылки на всё, то есть вообще всё что можно найти в nim'е. Не нашли нужного в туториалзах и мануале — в индексе точно найдёте.

    Уроки и туториалы


    • Nim basics — самые основы для новичков, сложные темы не раскрыты
    • Nim Days — небольшие проекты (live examples)
    • Rosetta Code — очень прикольно сравнивать решение одних и тех же задач на разных ЯП, в т.ч. nim
    • Exercism.io — здесь можно пройти "путь nim", выполняя задания
    • Nim by Example

    Помощь


    • IRC — основное место обитания… ниммеров?, которое транслируется в Discord и Gitter. Никогда не пользовался IRC (да и сейчас не пользуюсь). Вообще это очень странный выбор. Есть ещё голубиная почта по ниму… ладно, шучу.
    • Nim forum Возможности форума минимальны, но 1) тут можно найти ответ 2) тут можно задать вопрос, если п.1 не сработал 3) вероятность ответа больше 50% 4) на форуме сидят разработчики языка и активно отвечают. Кстати, форум написан на nim, и поэтому функциональность никакая
    • Nim telegram group — есть возможность задать вопрос и [не]получить ответ.
    • Есть ещё русская телеграм-группа, если вы устали от nim и не хотите о нём ничего слышать — вам туда :) (отчасти шутка)

    Playground


    • Nim playground — тут можно запустить программу на nim прямо в браузере
    • Nim docker cross-compiling — тут можно почитать, как запустить докер-образ и скомпилировать программу для разных платформ.

    Пакеты


    • nimble.directory — тут собраны все опубликованные пакеты, доступные для установки через пакетный менеджер nimble.
    • Curated list of packages — собранный энтузиастами список более-менее живых пакетов

    Переход на nim с других языков



    Что нравится


    Нет смысла перечислять все возможности языка, но вот некоторые особенности:


    Фрактал сложности


    Nim предоставляет вам "фрактал сложности". Вы можете писать высокоуровневый код. Можете бодаться с сырыми указателями и радоваться каждой attempt to read from nil. Можете вставлять C-код. Можете писать вставки на ассемблере. Можете писать процедуры (static dispatch). Не хватает — есть "методы" (dynamic dispatch). Ещё? Есть дженерики, и есть дженерики, мимикрирующие под функции. Есть шаблоны (templates) — механизм замены, но не такой блевотный, как в C++ (там макросы — это всё ещё просто текстовая замена, или уже что-то поумнее?). Есть макросы, в конце концов — это как IDDQD, они включают режим бога и позволяют работать напрямую с AST и буквально заменять куски синтаксического дерева, или самостоятельно расширять язык как хотите.
    То есть на "высоком" уровне вы можете писать хелловорлды и горя не знать, но никто вам не запрещает проводить махинации любой сложности.


    Скорость разработки


    Кривая обучения — не кривая. Это прямая. Установив nim, вы в первую же минуту запустите ваш первый hello world, а в первый же день вы напишете простую утилиту. Но и через пару месяцев вам будет что изучать. Например, я начинал с процедур, потом мне понадобились методы, через какое-то время мне очень пригодились дженерики, недавно я открыл для себя шаблоны в полной их красе, и при этом я ещё вообще не трогал макросы. Сравнивая с тем же rust или c++, "влиться" в nim гораздо проще.


    Package management


    Есть package manager под названием nimble, которые умеет устанавливать, удалять, создавать пакеты и подгружать зависимости. Когда создаёте свой пакет (= проект), в nimble можно прописать разные задачи (при помощи nimscript, который подмножество nim, исполняемый на VM), например, генерацию документации, запуск тестов, копирование ассетов итд. Nimble не только поставит нужные зависимости, но и вообще позволит сконфигурировать рабочее окружение для вашего проекта. То есть nimble — это, грубо говоря, CMake, который написали не извращенцы, а нормальные люди.


    Читаемость и выразительность


    Внешне nim очень похож на python с type annotations, хотя nim это не python вообще ни разу. Питонистам придётся забыть динамическую типизацию, наследование, декораторы и прочие радости, и вообще перестроить мышление. Не стоит пытаться перенести свой python-опыт в nim, ибо разница слишком большая. Поначалу очень хочется гетерогенных коллекций и миксинов с декораторами. но потом как-то привыкаешь жить в лишениях :)


    Вот пример программы на 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 и о других вариантах не вспоминать.


    Method call syntax


    Вызов функции может быть записан по-разному:


        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. То есть по умолчанию методы не работают, что слегка подталкивает к работе без оных.


    Compile-time execution


    Const — возможность вычислять что-то на этапе компиляции и "зашивать" это в результирующий бинарник. Это круто и удобно. Вообще в nim особое отношение ко "времени компиляции", даже есть ключевое слово when — это как if, но сравнение идёт на этапе компиляции. Можно написать что-то вроде


      when defined(SDL_VIDEO_DRIVER_WINDOWS):
        import windows  ## oldwinapi lib
      elif defined(SDL_VIDEO_DRIVER_X11):
        import x11/x, x11/xlib  ## x11 lib

    Это очень удобно, хотя и есть ограничения на то, что можно вытворять на этапе компиляции (например, нельзя делать FFI вызовы).


    Reference type


    Ref type — аналог shared_ptr в C++, о котором позаботится сборщик мусора. Но можно и самому вызывать сборщик мусора в те моменты, когда это вам удобно. А можно попробовать разные варианты сборщиков мусора. А можно вообще отключить сборщик мусора и использовать обычные указатели.


    В идеале, если не использовать сырые указатели и FFI, вы вря ли сможете получить ошибки сегментации. На практике пока без FFI никуда.


    Lambdas


    Есть анонимные процедуры (aka лямбды в питоне), но в отличие от питона в анонимной процедуре можно использовать несколько statements:


    someProc(callback=proc(a: int) -> int = var b = 5*a; result = a)

    Exceptions


    Есть исключения, их очень неудобно бросать: на 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")

    Generics


    Дженерики очень выразительные, например, можно ограничивать возможные типы


    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


    Шаблоны (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()

    Interactive shell


    Если нужно быстро что-то протестировать, то есть возможность вызвать "интерпретатор" или "nim shell" (как если вы запустите python без параметров). Для этого воспользуйтесь командой nim secret или скачайте пакет inim.


    FFI


    FFI — возможность взаимодействовать со сторонними библиотеками на C/C++. К сожалению, для использования внешней библиотеки вы должны написать враппер, объясняющий, откуда и что импортировать. Например:


    {.link: "/usr/lib/libOgreMain.so".}
    type ManualObjectSection* {.importcpp: "Ogre::ManualObject::ManualObjectSection", bycopy.} = object

    Есть инструменты, делающие этот процесс полуавтоматическим:



    Что не нравится


    Сложность


    Слишком много всего. Язык задумывался как минималистичный, но сейчас это очень далеко от правды. Вот например за что мы получили code reordering?!


    Избыточность


    Много говнища: system.addInt — "Converts integer to its string representation and appends it to result". Мне кажется, это очень удобная функция, я её использую в каждом проекте. Вот ещё интересное: fileExists and existsFile (https://forum.nim-lang.org/t/3636)


    Нет унификации


    "There's only one way to do smth" — вообще нет:


    • Method call syntax — пиши вызов функции как хочешь
    • fmt vs &
    • camelCase и underscore_notation
    • this и tHiS (спойлер: это одно и то же)
    • function vs procedure vs template

    Баги (нет, БАГИ!)


    Баги есть, примерно 1400. Или просто зайдите на форум — там постоянно какие-то баги находят.


    Стабильность


    В дополнение к предыдущему пункту, v1 подразумевает стабильность, да? И тут на форум залетает создатель языка Araq и говорит: "чуваки, я тут запилил ещё один (шестой) сборщик мусора, он круче, быстрее, молодёжнее, даёт вам shared memory для потоков (ха-ха, а раньше для этого вы страдали и использовали костыли), качайте develop ветку и пробуйте". И все такие "Вау, как круто! А что это значит для простых смертных? Нам теперь опять весь код менять?" Вроде как нет, поэтому я обновляю 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?


    Приятно осознавать, что я не один


    Методы и многопоточность


    По умолчанию флаги multimethods и threads выключены — вы ведь не собираетесь в 2019 2020 году писать многопоточное приложение с переопределением методов?! А уж как здорово, если ваша библиотека создавалась без учёта потоков, а потом пользователь их включил… Ах да, для наследования есть замечательные прагмы {.inheritable.} и {.base.}, чтобы ваш код не был слишком лаконичен.


    Object variants


    Вы можете избежать наследования, используя т.н. 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 все поля жёстко заданы в одной секции.
    В-третьих, что бесит больше всего — нельзя "переиспользовать" поля в разных типах:


    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

    Do notation


    Просто процитирую:


    • do with parentheses is an anonymous proc
    • do without parentheses is just a block of code
      Одно выражение означает разные вещи ¯_(ツ)_/¯

    Когда что использовать


    Итак, у нас есть функции, процедуры, дженерики, мультиметоды, шаблоны и макросы. Когда лучше использовать шаблон, а когда процедуру? Шаблон или дженерик? Функция или процедура? Так, а макросы? Я думаю, вы поняли.


    Custom pragma


    В питоне есть декораторы, которые можно применять хоть к классам, хоть к функциям.
    В nim для этого есть прагмы. И вот что:


    • Вы можете написать свою прагму, которая будет декорировать процедуру:
      proc fib(n : int) : int {.cached.} =
      # do smth
    • Вы не можете написать свою прагму, которая будет декорировать тип (=класс).

    Nimble


    Что мертво — умереть не может. В nimble куча проектов, которые уже давно не обновлялись (а в nim это смерти подобно) — и их не убирают. Никто за этим не следит. Понятно, обратная совместимость, "нельзя просто взять и удалить пакет из репы", но всё же… Ладно, спасибо, что хоть не как npm.


    Дырявые абстракции


    Есть такой закон дырявых абстракций — вы используете какую-то абстракцию, но рано или поздно вы обнаружете в ней "дыру", которая приведёт вас на уровень ниже. 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 должен был стать такой машиной. Отчасти он и стал, но в то же время, когда я мчусь на этой машине по хайвею, у меня отваливается колесо, а заднее зеркало показывает вперёд. За мной бегут инженеры и на ходу что-то приделывают ("теперь с этим новым спойлером ваша машина ещё быстрее"), но от этого у меня отваливается багажник. И знаете что? Мне всё равно чертовски нравится эта машина, ведь это лучшая из всех машин, что я видел.

    Комментарии 51

      +1

      Когда я первый раз увидел bin, у меня первый раз возникло желание написать свой язык. Там были просто гениальные идеи, и были такие как синтаксис вызова методов. Когда метод может быть вызван четырьмя вариантами, пример есть в почте автора, у вас будет четыре не пересекающихся группы программистов на нем. Почему это плохо.
      Вспомните почему заказали Perl.

        +1

        Почему не пересекающихся? :) Я в проекте использую 3 из 4 перечисленных.


        Ну например для логирования я не использую скобки:


        logging.debug "Oops"  # better
        logging.debug("Oops")  # worse but not restricted
        "Oops".debug()  # wat
        "Oops".debug  # insane

        Type convertion удобно писать так:


        someProc(10.unit8)  # better
        someProc(unit8(10))  # worse but not restricted

        Для методов (т.е. процедур, которые относятся к какому-то объекту) я использую первое:


        someObject.someProc(arg1: 5, arg2: 10)  # better
        someProc(someObject, arg1: 5, arg2: 10)  # worse but not restricted

        Т.е. правило "пиши как лучше читается". Хорошо это или плохо — пока не знаю, но более склоняюсь к "строгому" языку, чем к таким вольностям.

          0
          Т.е. правило «пиши как лучше читается». Хорошо это или плохо — пока не знаю, но более склоняюсь к «строгому» языку, чем к таким вольностям.

          А для меня это киллер-фича. :) Самое главное для меня — это как логично(и быстро) читается исходник.

          И по поводу отступов — вот тут как раз «строгость» в тему. Это тоже в плюс читаемости.
            0

            Функции можно вызывать как методы и в D. Так что, такая киллер-фича не уникальна

          0
          Это называется UFSC
          Для ФП стиля очень удобно.

          Вообще, Nim стоит поздравить с релизом.
            0

            Это называлось UFCS в ниме раньше. Потом они стали называть это Method Call Syntax. Потому что могут ¯_(ツ)_/¯

          +1
          Было бы неплохо выделить киллер-фичи Nim, для тех кто не может выделить их сам из статьи, например для меня)
            0

            Сильвупле (имхо):


            1. Быстрая компиляция в быстрый нативный код
            2. Доступна вся экосистема C/C++
            3. Простой в освоении
            4. Позволяет писать код любой сложности
              +3

              Если С++ ругают за медленную компиляцию, то как компиляция Nim (которая ведь транспиляция в С++ сперва) может быть быстрой?

                0

                В этом плане, увы, я могу оперировать только собственным опытом — деталей реализации я не знаю. Разумеется, компиляция не может быть быстрее копиляции C++ кода, но, во-первых, есть кэш компиляции, так что вы не будете пересобирать мир при небольших изменениях, а во-вторых, одно дело когда C++ код пишете вы для себя (вам его ещё читать потом), а другое дело когда это делает транспайлер и он может оптимизировать как хочет:


                struct tySequence__K284t8D0DApfLbw9c7cpnKw {
                  NI len; tySequence__K284t8D0DApfLbw9c7cpnKw_Content* p;
                };
                struct NimStrPayload {NI cap;
                AllocatorObj* allocator;
                NIM_CHAR data[SEQ_DECL_SIZE];
                };
                struct NimStringV2 {NI len;
                NimStrPayload* p;
                };
                typedef N_NIMCALL_PTR(void, tyProc__l0xby6CKnyVDN9bJs3WwRgw) (NI16 entity);
                typedef NU8 tyEnum_Level__pW4mH4lipH6u2NKDGEWdGg;
                typedef NimStringV2 tyArray__nHXaesL0DJZHyVS07ARPRA[1];
                struct TNimType {void* destructor;
                NI size;
                NCSTRING name;
                void* traceImpl;
                void* disposeImpl;
                };

                Но это не точно.

                  0

                  В целом логично, спасибо. Вероятно, генерированный код просто не будет особо напирать на "тяжелые" фишки из С++.

                  0
                  Медленая компиляция идет в основном от сложного использования шаблонов. Если nim генерирует простой C-подобный код с минимальным использованием фич из C++ компиляция будет вполне быстрой.
                    0

                    Ну, да, я вроде уже сам додумался чуть выше, спасибо :)

                  +5
                  Позволяет писать код любой сложности

                  Любой тьюринг-полный язык позволяет писать код любой сложности

                    +1

                    Согласен, тут не хватает какого-то слова, но мне казалось, что статья доносит посыл.


                    Позволяет эффективно писать код любой сложности.

                      0
                      Как вы на Питоне напишете драйвера для принтера?
                        +1

                        А что? Не знаю, как в Linux, но в Винде драйвер принтера это user-mode program. Оберточку сделать и все, запускай Питон

                          0

                          Не то, чтобы стоит так делать, конечно

                  +1
                  Итак, у нас есть функции, процедуры, дженерики, мультиметоды, шаблоны и макросы. Когда лучше использовать шаблон, а когда процедуру? Шаблон или дженерик? Функция или процедура? Так, а макросы?


                  Функции, процедуры, дженерики, мультиметоды, шаблоны, макросы.
                  Именно в таком порядке по приоритету. Если первого пункта недостаточно для реализации необходимого функционала, переходим к следующему и так далее. К сожалению, не могу вспомнить где в документации видел приоритетность, но она была очень похожа на эту. Разве что мультиметодов и функций там не было. Мультиметоды я бы вообще исключил, поскольку они почти всё время были сломаны.

                    0

                    Наверно, всё так, но ещё не забывайте, что шаблоны работают как подстановка, в отличие от процедур, поэтому теоретически они быстрее — нету прыжка в функцию и передачи параметров. Плюс иногда код сильно упрощается.


                    Например, сначала у меня был бесконечный цикл (gamedev, как-никак), который был функцией, принимающей коллбек:


                    proc loop(frequency: float, callback: proc(dt)) =
                      # ...
                      var dt = ...
                      callback(dt)
                    
                    proc doSmth(dt: float) =
                      echo &"{dt} seconds passed in this loop iteration"
                    
                    loop(30, doSmth)

                    А потом я вдруг понял, что с шаблоном будет проще и быстрее:


                    template loop(frequency: float, code: untyped) =
                      # ...
                      var dt {.inject.} = ...
                      code
                    
                    loop(30) do:
                      echo &"{dt} seconds passed in this loop iteration"
                    +3
                    Nim не первый и не лучший(ИМХО)кто пытается сделать ядреный сплав простоты и эффективности (Pythonic-like, macros, compiled,..). Был такой Boo (остановился в 2010 из-за перехода автора в Unity), имел на 25% короче синтаксис, макросы, работал на .Net. Все было круто до 2010, поддержка со стороны SharpDevelop лучше чем у Nim сейчас. Был Nererle c директивой pragma=indent программы были размером равны аналогу на Python, царь метапрограммирования. Убит Microsoftом ввиду опасности для С# и F# и прочих его унылых языков. Авторы взяты на работу в MS на левые проекты за мешок денег. Cobra,… все ушли. Слишком инертный мир в программинге. Слишком.
                      +1
                      C#, F#

                      Вот так, через запятую? Вы F# хотя бы издалека видели?

                        0
                        Тоже вспоминаю Nemerle. Очень жаль, что закопали. По сей день смотрится очень круто.
                        0

                        К сожалению, поддержка от Status проекта nim не вечна. Работал там, в течение последнего года компания оптимизируется и оптимизируется, судя по финансам, в новом году оптимизируется окончательно.
                        И это определённая проблема nim. Интересный язык, но нужна поддержка компаний.

                          0

                          В точку. Сравнивая с rust, я прям вижу, как проекту не хватает внимания, как со стороны разработчиков, так и со стороны компаний.

                            +2
                            >внимание со стороны разработчиков

                            я сколько ни смотрел на ним, я не понял, зачем он. точка.

                            нужен понятный таргетинг и обоснование
                              –1

                              Ну, знаете… Зачем плюсы?

                                +1
                                Десктоп, веб, эмбеддед, энтерпрайз, геймдев, расчеты — а что может Ним?

                                Для каждой области нужен свой тулинг, библиотеки
                                  0

                                  Скажу так: ним может всё, что могут C и C++. По собственному опыту не могу сказать, что есть прям какая-то ниша — nim это general purpuse.
                                  Desktop — да, есть несколько библиотек для гуя, но можно написать свой враппер для любой либы из c/c++.
                                  Web — есть jester и karax (фреймворки на ниме), есть всякие сокеты и вебсерверы в стандартной библиотеке. Но можно написать свой враппер для любой либы из c/c++.
                                  Эмбеддед — можно, там надо спец флаги указать.
                                  Геймдев — можно, пишу сейчас сам. Ядро на ниме, графика — на ogre3d или sdl, ввод и окна — на sdl, сеть — на enet (но есть и сетевые библиотеки на pure nim).
                                  Расчёты — есть куча всего, типа arraymancer, neo и что-то ещё. Но можно написать свой враппер для… Вы поняли :)


                                  Конечно, хотелось бы видеть всё на чистом ниме, но это утопия, для этого нужно большое комьюнити, которое это всё напишет, а чтобы было большое комьюнити, нужно чтобы много всего было написано.

                          +2
                          Что меня больше всего поразило в Nim, когда я некоторое время назад на него смотрел — то что язык позиционируется как замена C/C++ и при этом там есть сборка мусора (но уже какбы нет), причем там целая куча разных сборщиков на выбор, но ни по одному не найти нормальной документации, того какие гарантии риалтайма даются, и как они обеспечивается.

                          image

                          Вот эта картинка с их сайта на самом деле сделана с отключенным сборщиком циклов, по сути это просто reference counting. Я понимаю что технически это тоже «сборка мусора», но нельзя просто одно заменять на другое, нужно чтобы программы изначально писалась с учетом этого.

                          Сейчас автор языка решил всё таки сделать reference counting основным режимом, но из-за этого почему-то отвалилось куча всего и вылезло дикое количество багов. Очень странно называть язык в таком состоянии 1.0.
                            0

                            Стоит отметить, что в версии 1.0 по умолчанию старый сборщик мусора, с которым всё норм. Новый сборщик ещё не пришёл на замену, он включается опционально. Проблема именно в количестве сборщиков мусора (при том они разные: shared heap vs per-thread heap), и в том, что сейчас появился ещё один, вместо того чтобы делать реально полезные вещи.

                              0
                              Со старым сборщиком не норм то, что когда новый доделают, старый скорее всего выбросят (задепрекейтят). А с новым не то, то что его непонятно когда вообще доделают. Поэтому реально полезно было бы доделать это до конца.
                                0
                                Извиняюсь, что отвечаю на комментарий полугодовой давности, но ARC это таки очень полезная вещь — ARC это по сути вставка компилятором различных хуков типа =destroy, = и так далее (всё это происходит благодаря compile-time data flow analysis и компилятор создаёт CFG), и reference counting. ARC и не будет дефолтным, т.к. он не может работать с циклами — для этого есть ORC (это ARC с добавленным собирателем циклов).

                                В ARC есть shared-heap, не нужно каких-то setupForeignThreadGc или похожих для работы с C библиотеками (или наоборот, если нужно писать shared библиотеку на Nim), ARC намного лучше для embedded.

                                Пока что планируется, что в 1.4 (может и позже, не знаю) ORC станет дефолтным GC (но это произойдёт как минимум тогда, когда сам компилятор будет работать с ARC/ORC и все популярные библиотеки).

                                Ещё с ARC возможно больше потенциальных оптимизаций благодаря анализу использования данных, к примеру вот самый недавний PR насчёт создания оптимизатора для ARC — github.com/nim-lang/Nim/pull/14962

                                Больше информации — www.youtube.com/watch?v=aUJcYTnPWCg (презентация Andreas'а с NimConf об ARC/ORC), forum.nim-lang.org/t/5734 (там в постах много интересного), nim-lang.org/docs/destructors.html про деструкторы (они являются важной частью ARC)
                                0

                                Заинтересовал источник данных для последней строки, а на сайте я эту картинку сходу не нашел. Не поделитесь ссылкой?

                                  0
                                    0
                                    Эти картинки доверия не вызывают. Особенно HellWorld binary size. Для D/Rust так точно липа (

                                    Наверно, как обычно, нубобенчмаркеры с компиляцией по умолчанию. Именно так и написано =)
                                    what are the typical flags an application is compiled under

                                    А лаги ГЦ так типичные для RSS 1.5-2Гб
                                      +1

                                      Тащем, да. Я у себя хаскель-версию запустил с дополнительным флагом -A32m и получил 20 мс максимальную паузу (у го, для сравнения, получилось 8 мс).

                                +2

                                Самое яркое впечатление от моего опыта с nim: ты пытаешья сделать что-то незадокументированное (или просто совершаешь ошибку) и в ответ получаешь километровый стектрейс… который абсолютно нечитаем и абсолютно не помогает в решении проблемы. И приходится разбираться в этом самом "фрактале сложности", не всегда с успехом.


                                Следствие от того же самого: если какая-то фича в библиотеке не задокументирована, то понять, как её использовать можно только с очень большим трудом (исходники не особо-то и помогают).

                                  0

                                  Возможно, сейчас с этим лучше. Километровые стектрейсы появляются, если всё падает в gcc, и да, это больно отлаживать. В самом же ниме ошибки вполне понятные и лично я с ними не так часто встречаюсь.

                                  +1
                                  но вообще я просто хочу сесть и ехать (причём быстро) на вечеринку. Машина — не цель, а средство достижения цели.

                                  но от этого у меня отваливается багажник. И знаете что? Мне всё равно чертовски нравится эта машина, ведь это лучшая из всех машин, что я видел.

                                  Но приходится бросать ее на обочине и ехать на вечеринку на другой. :)
                                  Ох и намучился я тоже с глюками Nim. Даже при том, что очень хочется этот язык использовать, вышеупомянутые минусы заставили его отложить в сторону.
                                    –1

                                    Нет, на другой (c++) я бы так далеко никогда не доехал :)
                                    Жаль, что у вас был негативный опыт. Попробуйте v1 — всё-таки разрабы проделали много работы над стабильностью. Я тут конечно угораю над ними в статье, но вообще стало лучше.

                                    +3
                                    Есть шаблоны (templates) — механизм замены, но не такой блевотный, как в C++ (там это всё ещё просто текстовая замена, или уже что-то поумнее?).

                                    Так, а чем плохи шаблоны в плюсах? Имхо явно лучше, чем в этом вашем nim, где можно пихать что угодно куда угодно.
                                    Вообще от nim осталось впечатление какого-то франкенштейна, который сам не знает чем является.
                                      +1
                                      По моему мнению шаблоны в плюсах плохи:
                                      1. Временем компиляции
                                      2. Негибкостью (хороши только если одну функцию надо написать для нескольких типов, но как только начинают решаться проблемы из реального мира, приходится изобретать монструозную конструкцию из шаблонов, а то и вообще откатываться к макросам из C)
                                      3. Сообщениями об ошибках (без парсера в них можно утонуть)

                                      В Nim метапрограммирование на порядок лучше плюсового реализовано.
                                        0

                                        Я извиняюсь, кажется, я налажал в терминологии. Я имел ввиду механизм подстановки, когда пишешь #define one two, и препроцессор не глядя заменяет одно на другое. В Ниме templates — шаблоны — как раз делают постановку кода, вот меня и переклинило.
                                        Если я всё правильно понял, то шаблоны из c++ — это generic в ниме.

                                          0

                                          В C++ шаблоны позволяют подставлять в код типы или значения времени компиляции (constexpr).


                                          Для подстановки кода в функцию, как в нимовских шаблонах, в плюсах передают аргументом лямбду, компиляторы довольно неплохо умеют такой код оптимизировать.


                                          Макросы в плюсах приходится использовать чаще всего для генерации определений функций или типов. В Nim, если не ошибаюсь, макросы для того же используют.

                                        0
                                        Какого размера получается файл, насколько больше он потребляет оперативной памяти, по сравнению с C? Используется трансплайнер в по-настоящему нативный C-код, или же используется некая стандартная библиотека от nim при компиляции? Может ли nim заменить C в embedded, когда нужно запускать программу на устройствах с 4 МБ ROM и 32 МБ RAM, при том, что занято из этого уже половина?
                                          0
                                          Минимальный hello world получается размером в 80 КБ, линкуется с libc. Думаю, жить можно.
                                            0
                                            Извиняюсь за бамп старого треда, но таки с LTO (который по сути можно «бесплатно» использовать с Nim, так как он компилируется в С) и статической линковкой с musl можно получить бинарники меньше 40-30кб, а так ещё если использовать LTO, ARC и --panics:on -d:noSignalHandler --opt:size при статической линковке с musl, то у меня от echo «Hello, world!» выходил статический бинарник <10кб размером :)
                                            +2

                                            Ни про какую стандартную либу я не слышал, вроде просто компилируется в c (или cpp) и оно самодостаточно.
                                            По поводу размера — нет предела совершенству: https://hookrace.net/blog/nim-binary-size/

                                            0

                                            Последний раз, когда я смотрел Nim, там не было ни интерфейсов, ни трейтов. Для их эмуляции предлагалось руками собирать структуру из нужных замыканий (найдено в недрах форума, ссылку сейчас не найду). Также несколько странное решение для discriminated unions в виде явно выделенного поля под дискриминант.
                                            Как с этим сейчас?

                                              0

                                              Интерфейсов всё так же нет (ну или, как вы и говорили, собирать из замыканий, что я считаю извращением). Зато есть концепты, там много всего: https://nim-lang.org/docs/manual_experimental.html#concepts


                                              Вообще не вижу проблемы с полем под дискриминант, даже не представлял что можно по-другому. А что не так?

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое