Nim что это
Перейти к содержимому

Nim что это

  • автор:

Записки программиста

Вам доводилось когда-нибудь мечтать о языке, похожем на Scala, только без привязки к JVM и чтобы в нем было поменьше объектов? Или, возможно, как Python, только с типами и по скорости сравнимом с Си? Или, быть может, чтобы Rust имел человеческий синтаксис, поддерживал исключения и был в состоянии без помощи программиста понять, какие переменные класть в стек, а какие в кучу? Если что-то из названного верно, думаю, вам понравится Nim.

Основные сведения о Nim:

  • Язык раньше назывался Nimrod, потом сократили до Nim;
  • Создан товарищем по имени Andreas Rumpf в 2008-м году. Хотя язык уже довольно зрелый, он все еще продолжает разрабатываться, версии 1.0 пока что нет (UPD: Версия 1.0 вышла 23 сентября 2019);
  • Nim является мультипарадигменным языком. Он одновременно процедурный, объектно-ориентированный и функциональный, притом примерно поровну. Также есть поддержка метапрограммирования, и весьма мощная, а не так, для галочки;
  • Язык очень простой, можно освоить по туториалу на официальном сайте за считанные часы. Синтаксис мне больше всего напомнил Python, только с типами и без объектов на каждый чих;
  • В языке используется строгая статическая типизация. Есть REPL, вполне классические генерики и исключения. Вместо трейтов или интерфейсов язык предлагает концепты, которые, насколько я понимаю, являются значительно более мощным средством;
  • Язык транслируется в Си, C++, Obj-C, а потом компилируется в обычные бинарники. Благодаря этому Nim очень быстр, с легкостью использует любые сишные библиотеки и работает практически где угодно. Как оказалось, код на Nim отличается еще и довольно высокой скоростью компиляции. Кроме того, поддерживается трансляция и в JavaScript;
  • По умолчанию сборка мусора в Nim автоматическая, с использованием локальных куч, по одной на поток, и отложенного подсчета ссылок с дополнительными обходами для поиска циклов в тех структурах, где циклы возможны. Никаких фоновых процессов, собирающий мусор, в Nim нет. За счет всего это GC в Nim является, что называется, soft realtime, и никогда не делает stop the world. До выхода Nim 1.0 планируется добавить поддержку групп процессов с общей кучей. Кроме того, язык позволяет управлять памятью вручную, прямо как в Си. В этом случае данные размещаются в общей куче и лишены накладных расходов на копирование между процессами;
  • Нити в Nim — это обычные нити операционной системы, хранящиеся в пуле. За счет этого их создание и уничтожение дешевле, чем у обычных ниток. Но сами по себе эти нити дороже, чем потоки в Haskell или горутины в Go. Взаимодействуют нити при помощи асинхронных каналов или общей кучи, той, что с ручным управлением памятью. Ввод-вывод поддерживается как синхронный, так и асинхронный. Последний основывается на epoll в Linux и IOCP в Windows. Также в Nim есть футуры, похожие на футуры в Scala;
  • Имеет впечатляющую стандартную библиотеку. Есть менеджер пакетов под названием Nimble. Репозиторий централизованный, в виде JSON файла на GitHub — привет, Perl 6! Это хорошо, так как позволяет избежать проблемы с десятками форков одного пакета, как это случилось в мире Erlang. Готовых пакетов уже сейчас на удивление много. Есть клиенты для PostgreSQL, MySQL и MongoDB, легковесный веб-фреймворк Jester, биндинги к OpenGL и GTK, и много чего еще. Также, что касается инструментария, ведется работа над самописной IDE под названием Aporia;
  • В исходном коде на Nim запрещено использовать табуляцию. Ура, ну наконец-то кто-то до этого додумался! Поставил таб — все, код не компилируется;
  • Nim имеет null pointer’ы. Видимо, они нужны для взаимодействия с кодом на Си. Но также язык поддерживает и множество средств для нахождения сопутствующих ошибок, в том числе на этапе компиляции, например, аннотацию not nil. Насколько я понял, тут ситуация примерно как в случае со Scala и Java. Null есть, но на практике NPE может возникнуть только на границе взаимодействия языков или в чем-то вроде собственной реализации двусвязных списков;
  • Есть и ряд других примечательных особенностей. Компилятор имеет множество флагов, позволяющий, например, включать и выключать проверки на выход за границы массивов и такого рода вещи. По умолчанию в debug сборках такие проверки включаются по максимуму, а в release сборке отключаются. Есть еще система эффектов, при необходимости позволяющая сказать, что вот у этой функции нет побочных эффектов (код в стиле proc . ), вот эта никогда не бросает исключения, а вот эта бросает, но только такие-то;
  • Компилятор и библиотеки распространяются под лицензией MIT. За счет этого ваши собственные программы на Nim вы можете распространять под какой угодно лицензией;

Теперь давайте попробуем поиграться со всем этим.

На официальном сайте доступно описание нескольких способов установки компилятора Nim. Так, например, выглядит сборка из исходников на GitHub:

git clone -b master git: // github.com / Araq / Nim.git
cd Nim
git clone -b master —depth 1 git: // github.com / nim-lang / csources
cd csources && sh build.sh
cd ..
bin / nim c koch
. / koch boot -d:release

Путь до Nim/bin прописываем в переменную окружения $PATH. Заценим REPL:

>>> echo(«Hello, world!»)
Hello, world!

Для выхода используйте сочетание Ctr+D.

Также есть полезная утилита nimsuggest, которая, в частности, используется в Aporia. О том, как правильно собирать nimsuggest, мне удалось узнать только при помощи IRC-канала #nim во FreeNode:

cd $NIM_PATH / compiler / nimsuggest
nim c -d:release nimsuggest.nim
cp nimsuggest .. / .. / bin /
git clone https: // github.com / nim-lang / nimble
cd nimble
nim c -r src / nimble install

Каталог ~/.nimble/bin/ также добавляем в $PATH. Проверяем, что все работает:

nimble update
nimble list
nimble install rbtree

Установка и запуск Aporia:

nimble install aporia @ #head
aporia

Если увидите ошибку:

could not load: libgtksourceview-2.0.so(|.0)
sudo apt-get install libgtksourceview2.0-dev

Вот так примерно Aporia выглядит после запуска:

Aporia, текстовый редактора для Nim

По возможностям, правда, Aporia оказалась ближе к обычному текстовому редактору, чем IDE. Подсветка синтаксиса нормальная. Притом, подсветка есть не только для Nim. В настройках можно включить автокомплит, но работает он пока что как-то слабенько, не все методы предлагает на выбор. Есть табы, но я не нашел хоткеев для переключения между ними. Даже элементарного фолдинга нет. Кроме того, я словил core dump при попытке использовать goto definition, после чего Aporia не запускалась, пока я вручную не прибил nimsuggest. Как видите, что касается Aporia, тут пока все очень сыро, и по возможностям сильно уступает простому Vim.

Создание нового проекта на Nim происходит следующим образом:

mkdir hello_world
cd hello_world
nimble init hello_world

В hello_world.nimble можно изменить название проекта, описание, лицензию и тд. Дописываем:

bin = «hello_world»

Создаем hello_world.nim следующего содержания:

echo(«Hello, world!»)
nimble build
. / hello_world

В общем и целом, что касается сборки, тут все примерно как обычно.

Опасения в отношении Nim лично у меня довольно традиционные — отсутствие вакансий, небольшой размер сообщества, сырость библиотек и инструментов, тот факт, что язык все еще продолжает немного изменяться, отсутствие каких-либо success stories, отсутствие большой и серьезной компании, которая продвигала бы все это хозяйство, отсутствие нормальной IDE, и тот факт, что создание такой IDE усложняется наличием в языке макросов. Также меня немного печалит отсутствие в Nim нормальных легковесных процессов. С другой стороны, подозреваю, что не так-то просто их сделать, сохранив при этом в языке возможность использовать как синхронный, так и асинхронный ввод-вывод. Также подозреваю, что при желании их можно успешно реализовать на макросах Nim, ибо по своей мощи они, судя по всему, не уступают макросам Lisp.

Несмотря на все это, язык выглядит очень и очень годно. На мой взгляд, куда более годно, чем тот же Rust с этими его постоянными Arc и Box . Зато операционные системы писать можно, ведь нам с вами так не хватает новых операционных систем! Так о чем я? Да, уже сейчас количество готовых библиотек для Nim впечатляет. Не уверен, что у Erlang наберется столько же. Хотя, конечно, остается открытым вопрос о качестве этих библиотек. Также подкупает наличие очень подробной документации и отзывчивость сообщества, по крайней мере, на канале #nim во FreeNode.

Не знаю, выстрелит ли Nim, но мне очень хочется, чтобы выстрелил. Уже сейчас мне очень хочется пописывать на нем pet project’ы. И, кто знает, может через годик-другой мне даже захочется переписать на нем какой-нибудь не самый критичный кусочек боевой системы.

  • Официальный сайт языка;
  • Очень много примеров кода есть на RosettaCode;
  • Список рассылки nim-dev@;
  • Поддержка различными текстовыми редакторами;
  • Конечно же, Reddit и StachOverflow;

А что вы думаете о Nim?

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

Nim (язык программирования)

2023: Вышел новый язык программирования Nim, компилирующий код на C++ и JavaScript

1 августа 2023 года состоялся релиз языка системного программирования Nim 2.0. Исходный код на языке Nim компилируется в представление на C, C++, Objective-C и JavaScript.

Nim — язык программирования со статической типизацией, поддерживающий процедурный, объектно-ориентированный, функциональный и обобщённый стили программирования. По утверждениям разработчиков, платформа сочетает мощь Лиспа (List Processing language — «язык обработки списков»), простоту и понятность Python и высокую производительность Си.

Состоялся релиз языка системного программирования Nim 2.0

В языке Nim, как и в Python, в качестве разделителей блоков используются отступы (так называемое правило обязательных отступов), хотя в фильтрах препроцессора блоки могут разделяться и по ключевым словам. Язык является частично регистро-независимым (учитывается только регистр первой буквы в идентификаторах). Довольно необычной особенностью является то, что подчеркивания в идентификаторах игнорируются.

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

Одним из преимуществ Nim является то, что его относительно легко освоить. В составе языка имеется множество высокоуровневых типов, от обычных строк и массивов до последовательностей, множеств, кортежей, перечислений и так далее. Низкоуровневые системные данные могут быть неуправляемыми, но основные объекты, созданные в куче, обслуживаются сборщиком мусора, освобождая программиста от большинства проблем управления памятью. [1]

Nim: идеальный язык программирования

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

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

Идеальный язык

Сегодня мы поговорим о малоизвестном звере среди набирающих популярность языков программирования: Nim.

На текущий момент Nim является идеальным для меня языком в сравнении с C++, Java, D, Rust, Ruby, JavaScript, PHP и многими другими. Nim органично объединяет удачные концепции из других языков, таких как Python, Lisp, C, Object Pascal, Ada, Modula-3 и лишь автору известно, каких еще. Прежде чем делать выводы, мне довелось написать на нем не одну тысячу строк. Предупреждаю, что некоторые детали будут не сразу понятны людям из «мейнстримовых» языков: чтобы осознать эти вещи, их нужно потрогать.

Итак, приступим. Характеристики моего идеального языка:

Продуктивность. Строгая система типизации позволяет писать поддерживаемый и самодокументируемый код, в то же время не препятствует ваянию быстрых прототипов, как это возможно, к примеру, на Python.

Безопасность. Работа с данными в куче не обременяет управлением памятью. Освобождение системных ресурсов должно происходить автоматически. В той или иной степени это обеспечивают современные языки со сборщиком мусора и Rust.

Простота синтаксиса. Не более пары часов на изучение. Новички, приходящие на проект, должны разобраться в коде так же быстро, как если бы он был написан на известном им языке.

Выразительность. Язык должен быть расширяемым и дружелюбным к разного рода DSL. Ruby, Perl и некоторые другие языки имеют в этом определенные успехи.

Скорость. Разумеется, мы хотим, чтобы наши программы использовали CPU лишь на полезные вычисления. Это требование отсеивает большинство динамически типизированных языков.

Низкий уровень. Мы его не хотим, но иногда его не избежать. Ручное управление памятью, где это необходимо, адресная арифметика, задачи жесткого реального времени. И в таких случаях мы не хотим использовать другой, «более низкоуровневый» язык.

Метапрограммирование. Мы не хотим использовать инструменты, которые генерируют код на нашем языке. Мы хотим, чтобы это происходило в пределах нашего языка, на стадии компиляции. Среди компилируемых языков на это способны Lisp, D и, в меньшей степени, Rust.

Портируемость. Код на нашем языке должен запускаться везде, где запускается код на C. Любая ОС, любая архитектура, включая ARM, PIC и AVR, используемый в некоторых Arduino. Даже больше, было бы здорово запускать его в браузерах, поддерживающих Javascript!

Совместимость с религиозно-несовместимыми экосистемами. За декады существования C, C++ и Java было написано множество отличных библиотек. Было бы здорово в случае необходимости использовать их в проекте, написанном на нашем идеальном языке.

Звучит утопично, но Nim соответствует всему вышесказанному, и я бы не поверил, если бы не убедился в этом на собственном опыте.

О том, как начать писать на Nim, о его синтаксисе и других банальностях вам непременно следует почитать здесь:
— Nim by example
— How I start
— Официальное руководство

В данной статье я хочу показать более изощренные возможности Nim, позаимствовав некоторые детали из презентации создателя языка Nim, Андреаса Румпфа (Andreas Rumpf), недавно прошедшей в рамках OSCON.

В том, что такие возможности представляют не только академический интерес, можно убедиться, рассмотрев исходники проекта nimx, который мы разрабатываем и используем в нашем игровом проекте. В частности, nimx поддерживает не только компиляцию в нативный код, но и в Javascript+WebGL.

Шаблоны

Начнем с выразительности и расширяемости синтаксиса:

Выполнение этого кода выведет результат:

Если любопытный читатель заглянет в промежуточный С-код, то он увидит, что весь HTML-код записан одной За это отвечает мощный механизм сворачивания констант (constant folding), реализованный в компиляторе Nim.

Макросы

Если же мощь шаблонов по какой-то причине вас не убедила, на помощь приходит тяжелая артиллерия — макросы. В Nim это процедуры, принимающие узлы AST (Abstract Syntax Tree — результат синтаксического анализа языка) в качестве аргументов и возвращающие модифицированный AST.

Следующий пример требует углубленных познаний в языке. Для примера попробуем добавить в Nim анализ покрытия кода (code coverage). Возьмем подопытную процедуру:

proc toTest(x, y: int) = try: case x of 8: if y > 9: echo "8.1" else: echo "8.2" # не покрыто of 9: echo "9" # не покрыто else: echo "else" echo "no exception" except IoError: echo "IoError" # не покрыто toTest(8, 10) toTest(10, 10)

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

# Это код, который будет сгенерирован нашим макросом! var track = [("line 11", false), ("line 15", false), . ] # Флаги о прохождении потока управления через контрольные строки proc toTest(x, y: int) = try: case x of 8: if y > 9: track[0][1] = true # Контрольная строка echo "8.1" else: track[1][1] = true # Контрольная строка echo "8.2" of 9: track[2][1] = true # Контрольная строка echo "9" else: track[3][1] = true # Контрольная строка echo "foo" echo "no exception" except IoError: track[4][1] = true # Контрольная строка echo "IoError" toTest(8, 10) toTest(1, 2) # Выводим результат анализа покрытия кода proc listCoverage(s: openArray[(string, bool)]) = for x in s: if not x[1]: echo "NOT COVERED ", x[0] listCoverage(track)

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

import macros macro cov(n: untyped): untyped = # Наша цель result = n # AST остается прежним echo treeRepr n # Вывести структуру AST cov: # Применяем макрос proc toTest(x, y: int) = try: case x of 8: if y > 9: echo "8.1" else: echo "8.2" of 9: echo "9" else: echo "foo" echo "no exception" except IoError: echo "IoError" toTest(8, 10) toTest(10, 10)

В результате компиляции мы увидим следующее:

. TryStmt StmtList CaseStmt Ident !"x" OfBranch IntLit 8 StmtList IfStmt ElifBranch Infix Ident !">" Ident !"y" IntLit 9 StmtList [. ] Else StmtList [. ] OfBranch IntLit 9 StmtList Command Ident !"echo" StrLit 9 Else StmtList Command Ident !"echo" StrLit foo Command [. ] ExceptBranch [. ]

Теперь, когда структура кода ясна, мы можем полностью реализовать макрос. Следующий пример требует немного более глубоких знаний Nim. Если вы его не понимаете, то просто пропустите.

## Code coverage macro import macros # Для манипуляции узлами AST мы используем функции из стандартного модуля macros proc transform(n, track, list: NimNode): NimNode = # Вспомогательная процедура transform, вызываемая макросом во время компиляции result = copyNimNode(n) for c in n.children: result.add c.transform(track, list) # Рассматриваем AST ветвления if n.kind in : let lineinfo = result[^1].lineinfo template trackStmt(track, i) = track[i][1] = true result[^1] = newStmtList(getAst trackStmt(track, list.len), result[^1]) template tup(lineinfo) = (lineinfo, false) list.add(getAst tup(lineinfo)) macro cov(body: untyped): untyped = # Собственно, макрос var list = newNimNode(nnkBracket) let track = genSym(nskVar, "track") result = transform(body, track, list) result = newStmtList(newVarStmt(track, list), result, newCall(bindSym"listCoverage", track)) echo result.toStrLit # Ради отладки, выведем измененный код cov: # Применяем макрос proc toTest(x, y: int) = . toTest(8, 10) toTest(10, 10)
8.1 no exception else no exception NOT COVERED coverage.nim(42,14) NOT COVERED coverage.nim(43,12) NOT COVERED coverage.nim(47,6)

Таким образом, менее чем в 30 строках кода мы реализовали полезный функционал, для которого в иных языках потребовались бы отдельные инструменты. С помощью макросов вы можете добавлять в язык различные функции, которых вам не хватает: интерполяция строк, pattern matching или что-то еще. Выверенный синтаксис Nim обеспечивает однозначность грамматики при использовании расширений. Увидев «загадочный» DSL-код, вы всегда знаете, с какими аргументами вызвать grep ;).

Управление памятью

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

Рассмотрим их назначение на примере объектов:

type Person = object name: string

Теперь у нас есть некий новый тип, и мы можем создавать объекты этого типа несколькими способами. Первый способ — создание объекта по значению:

var p = Person(name: “John”)

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

С другой стороны, мы можем выполнять выделение памяти под объект в управляемой сборщиком мусора куче, используя функцию new :

var pp = Person.new()

В таком случае переменная pp будет ссылаться на объект, который находится в хранилище, управляемом сборщиком мусора, а реальный тип переменной будет ref Person (ref — управляемая ссылка). Такие объекты передаются в коде по ссылкам и умирают тогда, когда ссылки на данный объект в памяти отсутствуют.

Теперь перейдем к последнему, самому низкоуровневому способу работы с памятью, а именно — к работе с неуправляемым хранилищем памяти, которая осуществляется с помощью функций alloc, realloc и dealloc , по поведению напоминающие malloc, realloc и free.

var up = cast[ptr Person](alloc(sizeof(Person))) # . dealloc(up)

Как можно определить из примера, в данном случае переменная up , как и в предыдущем случае, изменяет тип, то есть ее тип — не Person , а ptr Person , что обозначает небезопасный неуправляемый указатель, хранящий адрес объекта в памяти.

При этом в случае необходимости, для удобства использования таких типов с модификаторами, их можно явно обозначить с помощью ключевого слова type:

type PPerson = ref Person UPerson = ptr Person

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

Взаимодействие с другими экосистемами

Под капотом Nim использует C, C++, Objective-C или JavaScript как промежуточный код. Это значит, что использование библиотек, написанных на этих языках, довольно тривиально. Другие языки, как правило, предполагают механизмы расширения через И здесь Nim справляется прекрасно, позволяя писать бриджи к другим языкам, как библиотеки.

Недолго думая, я набросал небольшую библиотеку jnim, доступную на GitHub. jnim позволяет «импортировать» модули Java. Выглядит это так:

import jnim jnimport: # Импортируем пару классов import java.lang.System import java.io.PrintStream # Импортируем статическое свойство proc `.out`(s: typedesc[System]): PrintStream # Импортируем метод proc println(s: PrintStream, str: string) # Запускаем JVM. Это делать необязательно, если JVM уже запущен, к примеру, на Android. let jvm = newJavaVM() # Вызываем! :) System.`.out`.println("This string is printed with System.out.println!")

Вся магия происходит внутри jnim. Для каждого определения jnimport создается одноименная сущность в Nim, и генерируется весь необходимый glue-код. Дальнейшим развитием jnim будет возможность не указывать поля и процедуры, а автоматически импортировать определения классов из Java-окружения на этапе компиляции.

Заключение

Nim — это мощный и практичный инструмент, достоинства которого трудно осветить в одной статье. Мы в ZEO Alliance недавно начали писать игровой проект на Nim, который, насколько я знаю, станет одной из первых коммерческих игр, написанных на этом языке.

Также мы популяризируем этот язык внутри Альянса, проводим ряд образовательных мероприятий для наших сотрудников и планируем пригласить в Украину автора Nim, Андреаса Румпфа.

Интересно, кто еще работает в этом направлении в Украине? Буду рад прочесть в комментариях ваши отзывы и мнения. Есть ли у вас опыт использования Nim? Сталкивались ли вы с задачами, для решения которых Nim был бы более эффективным инструментом?

Благодарю Ростислава Дзинько за секцию об управлении памятью в Nim и Андреаса Румпфа за вычитку и правку статьи.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

�� Подобається Сподобалось 0

До обраного В обраному 0

Схожі статті

Організація процесу CI для швидкої доставки збірок. Контроль якості на його етапахОрганізація процесу CI для швидкої доставки збірок. Контроль якості на його етапах

Mykola Mokhnach 5 квітня 2016

Експеримент із технологією доповненої реальності у вебі (front-end only)Експеримент із технологією доповненої реальності у вебі (front-end only)

Taras Chaykivskyy 7 квітня 2016

Тестування коду: об’єднання звіту покриття для android- та unit-тестів з Jacoco і SonarQubeТестування коду: об’єднання звіту покриття для android- та unit-тестів з Jacoco і SonarQube

Torba Igor 7 вересня 2016

48 коментарів

Vitaly Chernooky C, Embedded and Linux в Self-Emplyed 13.06.2016 00:51

C моей колокольни смахивает на удачный эксперимент.

Когда увидел комментирование в стиле баш и других, сразу подумал о том, а как же множественное комментирование?
А вот так:
discard «»» You can have any Nim code text commented out inside this with no indentation restrictions. yes(«May I ask a pointless question?») «»»
И это снова напомнило мне баш. И я люблю баш, но я люблю его в консоли, а не в IDE, вот и из-за такой реализации комментирования.

Denis Olehov Web Developer 22.08.2015 17:48

ничто не идеально, даже nim.

Oleg Korol POWER Ops, again 20.08.2015 20:08

зря Nimrod переименовали в уже занятое другим программным продуктом сочетание из трех букв
www.redbooks.ibm.com/. tracts/sg247296.html?Open
могут быть и иски. либо будет еще одно переименование 🙂

Yuriy Glukhov Desktop Engineering Director в ZEO Alliance 20.08.2015 19:47

Постараюсь прояснить некоторые вопросы, озвученные в комментариях. Дисклеймер: в любом случае я призываю попробовать Nim на ощупь, прежде чем его критиковать ;).
* Чувствительность пробелов. Имхо, этот фукционал спорный, он отключен по умолчанию, включить его можно с помощью специальной прагмы. Я с трудом верю, что он когда-либо будет включен.
* Бывают ли инструменты для всего. C++ был инструментом для всего, пока «всего» не стало больше. С течением времени, в программировании появляются новые потребности, для которых создаются специальные инструменты. Впоследствии, специальные инструменты могут быть унифицированы, если досконально изучить их области применения. конкурирующий стандарт свидетельствует лишь о том, что над ним плохо поработали =).
* Количество библиотек. Вопрос курицы и яйца. В то же время, биндинги для существующих библиотек на C/C++/Obj-C можно сваять, практически не отвлекаясь от первоочередной задачи. Еще есть инструмент c2nim, который делает это в автоматическом режиме, и даже умеет транслировать код на C в nim (иногда требуется небольшая помощь)! Таким образом можно довольно быстро перевести всю старую кодбазу, и продолжить разработку на nim. Также следует заметить, что мощный механизм макросов и шаблонов позволяет почти бесшовно интегрироваться со всеми языками, допускающими написание расширений на C, то есть буквально, со всеми языками. Я пытался это показать в последней секции статьи.
* Синтаксис. Это холивар. Скажу лишь, что на мой взгляд синтаксис выверен достаточно скрупулезно. При всей своей лаконичности, грамматика лишена многозначительности. Расширяемость синтаксиса сделана чище, чем в Ruby, и на много порядков чище, чем в Perl, где к каждому незнакомому слову следует относиться с подозрением =).
* Конфликт в моих требованиях к идеальному языку. Заметьте, что это требования к языку, а не к коду. В Nim очень хорошо действует принцип pay-as-you-go. Вы всегда знаете, какова цена райнтайм-проверке выхода за пределы массива. И в Nim у вас всегда есть выбор между бешеной скоростью, и безопаностью. Однако, под скоростью я больше подразумевал близость промежуточного кода к железу, и отсутствие проверок типов в рантайме, чего не избежать в том же питоне. Если сравнивать скорость алгоритмов на ниме и питоне, то при всех безопасностях нима, он будет на порядок быстрее.
* Статическая типизация. Ним — исключительно статически типизацированный. Однако, в нем есть механизм type inference, который позволяет не указывать типы переменных/констант. Так же есть generics, но в отличие от общепринятого понимания этого слова, дженерики в ниме по своей мощи больше похожы на темплейты в D, которые в свою очередь выводят темплейты C++ на новый космический уровень.
* Выразительные языки проигрывают в скорости и простотой синтаксиса. Это следует формулировать так: «Известные мне выразительные языки . » 😉
* Портируемость и скорость совершенно не совместимы. Думаю, разработчики Assasin’s Creed с вами не согласятся. Разумеется, используемое апи всех поддерживаемых платформ следует свести к одному интерфейсу. Как правило, толщина такого портинг-слоя всецело зависит лишь от умений программиста и может быть достаточно производительной. Но даже в этом вопросе Nim может прийти вам на помощь, «заинлайнив» использование нативного API. Конечно, тут тоже трубется некоторое понимание принципов языка.

Если вдруг не на все вопросы ответил, буду очень рад вашим комментариям по теме ;).
ЗЫ. Реквестирую статью о том, как количество виртуальности в C++ (что бы это ни значило) влияет на производительность. Возможно, она перевернет мой мир 😉

Дзен Nim

Это расшифровка выступления Арака (Andreas Rumpf, создатель языка Nim — прим. пер.) на NimConf2021, случившегося 26 июня (вот запись на youtube и слайды на github). Пьетро Петерлонго адаптировал текст к публикации в блоге, а Арак дополнительно проверил его.

Zen of Nim

  1. Копирование плохого дизайна — так себе дизайн.
  2. Если компилятор не может рассуждать о коде, то и программист не может.
  3. Не стой на пути у программиста.
  4. Перенеси работу на этап компиляции: программы запускаются гораздо чаще, чем компилируются.
  5. Настраиваемое управление памятью.
  6. Лаконичный код не мешает читабельности, он ей способствует.
  7. (Задействовать метапрограммирование, чтобы оставить язык компактным).
  8. Оптимизация это специализация: если вам нужно больше скорости, пишите кастомный код.
  9. Должен быть только один язык программирования для всего. Этот язык — Nim.

Примечание редактора.
В оригинале выступления Zen of Nim был дан в конце (и без нумерации). Здесь мы размещаем его в самом начале, нумеруя, чтобы было проще ссылаться. Дальнейшее раскрытие этих правил происходит в контексте обсуждения языка вообще и не пытается воспроизвести указанный выше порядок. Статья следует за выступлением, от материала на слайдах до расшифровки с минимальной редактурой (это отразилось в неформальном тоне текста).

Введение

В этом посте я собираюсь объяснить философию языка Nim и почему Nim может быть полезен для широкого спектра областей применения, таких как:

  • научные вычисления
  • игры
  • компиляторы
  • разработка операционных систем
  • написание скриптов
  • и многих других

«Дзен» в заглавии означает, что мы придём к набору правил (показанных выше), которые направляют разработку языка и его эволюцию, но я буду говорить об этих правилах с помощью примеров.

Синтаксис

Позвольте мне представить Nim через его синтаксис. Я понимаю, что многие из вас, возможно, уже знают этот язык, но чтобы обеспечить плавный вход тем, кто никогда его ранее не видел, я объясню базовый синтаксис и надеюсь придти к интересным выводам.

Nim использует синтаксис, основанный на отступах, вдохновлённый Haskell или Python. Это решение также сочетается с системой макросов.

Применение функции

Nim различает инструкции и выражения, и большинство выражений это применение функции (или «вызов процедуры»). Применение функции использует традиционную математическую запись со скобками: f() , f(a) , f(a, b) .

Но есть и сахар:

spawn log(«some message»)

echo «hello «, «world»

  • По правилам 1 и 2 вы можете опустить скобки. Здесь же есть пример, где и почему это бывает полезно: spawn выглядит как ключевое слово, что неплохо, поскольку оно делает что-то особенное; echo также известен своей необязательностью скобок, потому что обычно вы пишете его для отладки, а значит уже торопитесь всё скорее закончить.
  • Вам доступна запись через точку, и в ней вы тоже можете опускать скобки (3–6).
  • Правило 7 про строковые литералы: f , за которой следует строка без пробелов, это всё ещё вызов, но строка превращается в сырую, что очень сподручно для регулярных выражений, поскольку у них свои представления о том что должен означать бэкслеш.
  • Наконец, в последнем правиле мы видим, что вы можете передать блок кода в f с помощью : . Блок кода обычно это последний аргумент, который вы передаёте функции. Это может быть использовано для создания кастомной инструкции lock .

Есть одно исключение для пропуска скобок, в случае, если вы ссылаетесь на f напрямую: f не означает f() .

В конструкции myarray.map(f) вы не хотите вызывать f , вместо этого вы просто хотите передать саму f в map .

Операторы

В Nim есть бинарные и унарные операторы:

  • В большинстве случаев бинарные операторы вызываются как x @ y , а унарные как @x .
  • Нет явного различия между операторами и функциями, а также между бинарными и унарными операторами.
func `++`(x: var int; y: int = 1; z: int = 0) = x = x + y + z var g = 70 ++g g ++ 7 # оператор в в обратных апострофах обрабатывается как 'f': g.`++`(10, 20) echo g # выведет 108
  • Операторы это просто сахар для функций.
  • Токен для оператора даётся в обратных апострофах (см. ++ ) для определения функции и вызова его, собственно, как функции.

Напомним, что ключевое слово var указывает на изменяемость:

  • параметры доступны только для чтения, пока не объявлены как var
  • var означает «передавать по ссылке» (это реализовано как скрытый указатель)
Инструкции vs выражения

Инструкции требуют отступ:

# отступ не требуется для односложных инструкций: if x: x = false # отступ требуется для вложенных инструкций: if x: if y: y = false else: y = true # отступ требуется, потому что две инструкции # следуют за одним условием: if x: x = false y = false

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

Выражения же в действительности не основаны на отступах, так что с ними вы вольны использовать дополнительное белое пространство как захотите:

if thisIsaLongCondition() and thisIsAnotherLongCondition(1, 2, 3, 4): x = true

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

Наконец, инструкции if , case и подобные также доступны в виде выражений, так что они могут возвращать значение.

В качестве простого примера, чтобы закончить этот раздел, вот законченная программа на Nim, демонстрирующая ещё немного синтаксиса. Если вы знакомы с Python, вам должно быть несложно это прочитать:

func indexOf(s: string; x: set[char]): int = for i in 0..) echo whitespacePos
  • Nim использует статическую типизацию, поэтому за параметрами следуют типы: входной параметр s имеет тип string ; x имеет тип «множество символов»; функция, именуемая indexOf , возвращает в конечном итоге целочисленное значение.
  • Вы можете итерироваться по индексу строки с помощью цикла for , цель здесь — найти позицию первого символа внутри строки, совпадающего с одним из данного множества.
  • При вызове функции мы конструируем множество символов, условно отвечающих критерию «пробел», с помощью фигурных скобок ( <> )

Поговорив немного о синтаксисе, мы можем сформулировать наше первое правило дзен:

Лаконичный код не мешает читабельности, он ей способствует.

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

Типичный аргумент против: «синтаксис слишком сжатый, это нечитабельно, и всё что вы хотите сделать это сократить усилия по набору кода»; для меня это пример непонимания, дело не в экономии нажатий или усилий по набору, а в экономии усилий в тот момент, когда вы смотрите на получившийся код. Программы гораздо чаще читают, чем пишут, и когда вы их читаете, очень уместно, если они короче.

Умный компилятор

Второе правило Nim:

Компилятор должен быть способным рассуждать о коде.

Это означает, что мы хотим:

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

Дальше мы увидим в деталях, что всё это значит.

Структурное программирование

Задача следующего примера — посчитать слова в файле (заданном через параметр filename типа string ) и вернуть таблицу подсчёта строк, чтобы в итоге там была запись на каждое слово и как часто слово появляется в тексте.

import tables, strutils proc countWords(filename: string): CountTable[string] = ## Counts all the words in the file. result = initCountTable[string]() for word in readFile(filename).split: result.inc word # 'result' вместо 'return', никакого не структурного потока управления

Стандартная библиотека Nim, к счастью, уже предлагает нам CountTable , так что первая строчка нашей proc это новая таблица подсчета.

result встроен в Nim и он представляет собой возвращаемое значение, так что вам не нужно писать return result , что не является примером структурного программирования, потому что return незамедлительно покидает любую область видимости и возвращает результат. Nim предоставляет возможность использовать инструкцию return , но мы рекомендуем остерегаться её, поскольку это не является структурным программированием.

В оставшейся части тела proc мы читаем файл в простой буфер, делим его на отдельные слова и считаем слова с помощью result.inc

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

В следующем примере, я выхожу из цикла for более затейливо, с помощью инструкции continue :

for item in collection: if item.isBad: continue # что нам известно на данный момент? use item
  • Для каждого элемента коллекции, если он нас устраивает, мы продолжаем со следующим, либо используем его.
  • Что я могу знать после инструкции continue? Ну, допустим, я знаю, что элемент подходит.

Почему бы не переписать это используя структурное программирование:

for item in collection: if not item.isBad: # что нам известно на данный момент? # что элемент подходит. use item
  • Отступ здесь даёт нам подсказку об инвариантах в нашем коде, так что теперь нам гораздо яснее, что когда я использую item, инвариант говорит нам, что элемент подходит.

Если вы предпочитаете инструкции continue и return, ну и отлично, нет никакого криминала в том, чтобы ими пользоваться, я сам пользуюсь ими в случаях, когда больше ничего не сработает. Но вы должны стараться избегать их. И, что более важно, всё это означает, что мы, вероятно, никогда не добавим более общей инструкции go-to в Nim, потому что go-to ещё больше противоречит парадигме структурного программирования. Мы хотим быть в том положении, которое позволит доказывать всё больше и больше свойств вашего кода, и структурное программирование значительно упрощает механику доказательства, что помогает нам.

Статическая типизация

Ещё одним аргументом в пользу статической типизации является то, что мы действительно хотели бы, чтобы вы использовали собственные типы, определяемые областью применения.

Вот небольшой пример про отделённые строки ( distinct string , distinct делает новый тип несовместимым с базовым — прим. пер.), а также enum и set :

type SandboxFlag = enum ## что интерпретатор должен разрешать allowCast, ## разрешить не безопасный 'cast' allowFFI, ## разрешить FFI allowInfiniteLoops ## разрешить бесконечные циклы NimCode = distinct string proc runNimCode(code: NimCode; flags: set[SandboxFlag] = ) = . 
  • NimCode хранится как string , но это distinct string , то есть особый тип строки со своими правилами.
  • proc runNimCode выполняет произвольный код на Nim, который вы ей передаёте, и, по сути, это виртуальная машина, выполняющая код. Она может ограничить что возможно, а что нет.
  • Здесь у нас что-то вроде песочницы, и разные свойства, которые вы можете использовать. Например, вы можете сказать: разреши операцию cast ( allowCast ) или разреши FFI ( allowFFI ); последняя опция позволит Nim’у выполнять код в бесконечном цикле ( allowInfiniteLoops ).
  • Мы перечислили опции обычном enum , после чего мы можем класть их во множество ( set ), обозначая таким образом, что каждая опция никак не зависит от других.

Сравним, для примера, код выше с аналогичным кодом на C, где часто прибегают к подобной практике. Но тут мы теряем типобезопасность:

#define allowCast (1 
  • Во время вызова runNimCode , flags это просто беззнаковые целые и никто не помешает вам передать значение 700, например, даже если это не имеет никакого смысла.
  • Вам придётся прибегнуть к манипуляции битами (в оригинале «bit twiddling», т. е. акцент на неочевидности манипуляций— прим. пер.), чтобы определить allowCast , … allowInfiniteLoops .

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

Статическое связывание

Мы хотим, чтобы Nim использовал статическое связывание. Вот модифицированный пример «hello world»:

echo "hello ", "world", 99

Что здесь произойдёт? Компилятор перепишет это следующим образом:

echo([$"hello ", $"world", $99])
  • echo объявлено так: proc echo(a: varargs[string, `$`]);
  • $ (оператор toString в Nim) применяется к каждому аргументу.
  • Мы задействуем здесь перегрузку (оператора $ в данном случае) вместо динамического связывания (как это было бы, например, в C#)

Это масштабируемая механика:

proc `$`(x: MyObject): string = x.s var obj = MyObject(s: "xyz") echo obj # работает
  • Здесь у меня мой пользовательский тип MyObject и я определяю для него оператор $ , чтобы он возвращал только поле s .
  • Далее, я конструирую MyObject со значением «xyz» .
  • echo понимает как как вывести объекты типа MyObject , потому для них определён оператор $ .
Типы данных, основанные на значениях

Мы хотим типы данных, основанные на значениях, потому что это поможет программе рассуждать о коде. Я уже говорил, что мы хотели бы ограничить разделяемое изменяемое (shared mutable) состояние. Решение, которое всё время упускается из виду в функциональных языках программирования, это ограничить алиасинг, а не изменяемость. Изменяемость это очень прямой, удобный и эффективный способ действия.

type Rect = object x, y, w, h: int # конструктор: let r = Rect(x: 12, y: 22, w: 40, h: 80) # доступ к полям: echo r.x, " ", r.y # присвоение создаст копию: var other = r other.x = 10 assert r.x == 12

То, что присвоение other = r создаст копию, означает, что никакого запутанного действия со стороны здесь не возникнет, есть только один путь к r.x и other.x не создаёт дополнительного доступа по тому же адресу в памяти.

Отслеживать сайд-эффекты

Мы хотим иметь возможность отслеживать сайд-эффекты. В следующем примере цель — подсчитать количество вхождений подстроки в строку.

import strutils proc count(s: string, sub: string): int = result = 0 var i = 0 while true: i = s.find(sub, i) if i < 0: break echo "i is: ", i # ошибка: 'echo' имеет сайд-эффекты i += sub.len inc result

Давайте представим, что это не корректный код и в нём есть отладочный echo. Компилятор выдаст жалобу: вы сказали, что proc не имеет сайд-эффектов, но echo их производит, так что вы ошиблись, идите и почините свой код!

Другой аспект языка Nim в том, что несмотря на сообразительность компилятора, который может здорово помочь, иногда вам надо просто закончить свою работу и у вас должна быть возможность ситуативно изменить эту прекрасную установку по-умолчанию.

Так что если я скажу: «окей, я знаю, что здесь появляется сайд-эффект, но мне не важно, потому что это просто код, который я добавил для отладки», вы можете сказать: «эй, преобразуй эту часть кода эффектом noSideEffect », тогда компилятор останется доволен и ответит: «окей, продолжаем»:

import strutils proc count(s: string, sub: string): int = result = 0 var i = 0 while true: i = s.find(sub, i) if i < 0: break : echo "i is: ", i # 'cast', так что продолжаем i += sub.len inc result

cast означает: «Я знаю что я делаю, отстань».

Отслеживать исключения

Мы хотим отслеживать за исключения!

Здесь у меня главная процедура proc main и я хочу сказать, что она не вызывает никаких исключений, я хочу иметь возможность удостовериться, что я обработал все исключения, которые могут возникнуть:

import os proc main() = copyDir("from", "to") # Error: copyDir("from", "to") can raise an # unlisted exception: ref OSError

Компилятор будет недоволен и скажет: «слушай, это не так, copyDir может выбросить незарегистрированное исключение, а именно OSError» . Так что вы скажете: «хорошо, вообще-то я действительно его не отработал», так что я теперь могу указать, что main вызывает OSError и компилятор скажет: «да, ты прав!»:

import os proc main() = copyDir("from", "to") # скомпилировалось :-)

Мы хотим иметь возможность небольшой параметризации над всем этим:

proc x[E]() = raise newException(E, "text here") try: x[ValueError]() except ValueError: echo "good"
  • Тут у меня дженерик proc x[E] ( E это обобщённый тип) и я говорю: «что бы ты не направил в x , это то, что я хотел бы здесь выбросить как исключение»
  • Потом я ввожу этот x с исключением ValueError и компилятор счастлив!

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

Ограничения изменяемости

Я собираюсь показать и объяснить, что делает экспериментальный ключ strictFuncs :

 type Node = ref object next, prev: Node data: string func len(n: Node): int = var it = n result = 0 while it != nil: inc result it = it.next
  • Здесь описан тип Node , который представляет из себя ref object , его next и prev это указатели на объекты того же типа (это двусвязный список). Так же в нём есть поле data типа string .
  • Дальше идёт функция len , которая считает количество нод в моём связном списке.
  • Реализация очень прямолинейная: пока мы не упрёмся в nil , посчитать текущую ноду и перейти к следующей.

Важным здесь является то, что с помощью strictFuncs мы сообщаем компилятору, что объекты, доступные через аргументы теперь глубоко неизменяемы. Компилятор спокойно воспринимает этот код. А также он спокойно воспринимает и такой пример:

 func insert(x: var seq[Node]; y: Node) = let L = x.len x.setLen L + 1 x[L] = y
  • Я бы хотел insert что-нибудь, но это func , а значит она строго ограничивает изменения, которые я делаю.
  • Я буду добавлять в x , которая является последовательностью нод, поэтому x явно обозначается изменяемой через ключевое слово var (а вот y — не изменяемая).
  • Я могу выставить длину x как старую длину плюс один и уже тогда переписать то, что там внутри, замечательно.

Наконец, я по прежнему могу изменять локальное состояние:

func doesCompile(n: Node) = var m = Node() m.data = "abc"

Здесь у меня переменная m типа Node , но только что созданная. Я могу изменять её и выставить её поле data , так как она не присоединена к n . Компилятор доволен.

Семантика такая: «вы не можете изменять то, что доступно через параметр, пока этот параметр не будет явно помечен как var» .

Вот пример, где компилятор скажет: «Хоба! Вы пытаетесь изменить n, но находитесь в режиме strictFunc, так что не выйдет»

 func doesNotCompile(n: Node) = n.data = "abc"

Можем поиграть в эту игру и посмотреть насколько он умён.

В этом примере я пытаюсь сыграть с компилятором в напёрстки, чтобы он принял код, но терплю неудачу:

 func select(a, b: Node): Node = b func mutate(n: Node) = var it = n let x = it let y = x let z = y # 
  • select это вспомогательная функция, которая принимает две ноды и просто возвращает вторую.
  • Потом я хочу изменить n , но присваиваю её в it , потом it в x , x в y и, наконец, y в z .
  • После я выбираю x или z и тогда изменяю поле data и перезаписываю строку на значение "tricky" .

Компилятор скажет вам: «Ошибочка, объект, достижимый через n потенциально изменяем» и укажет на инструкцию, которая соединяет граф с этим аргументом. Внутри там происходит следующее: у него есть представление в виде абстрактного графа, который задан с условием «каждый строящийся граф является непересекающимся», но в зависимости от тела вашей функции, эти непересекающиеся графы могут соединяться. Когда вы что-то изменяете, изменяется граф, и если он соединён с аргументом, компилятор вам сообщит.

А вот и ещё одно правило:

Если компилятор не может рассуждать о коде, то и программист не может.

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

Возможности метапрограммирования

Следующее правило широко известно в наши дни:

Копирование плохого дизайна — так себе дизайн.

Если вы скажете: «Эй, в языке X есть возможность F, давай тоже её сделаем!», вы скопируете это решение, но не будете знать, хорошее оно или плохое, потому что вы не начали с самого начала.

Например, «В C++ есть выполнение функций во время компиляции, давай тоже сделаем!». Это не причина, чтобы добавить выполнение функций во время компиляции, наша причина (и, кстати, мы сделали совершенно не так как в C++) в следующем: «У нас очень много ситуаций для применения F».

В этом случае F это система макросов: «Нам надо иметь возможность делать блокировки, логирование, ленивые вычисления, типобезопасные Writeln/Printf, декларативный язык для UI, асинхронность и параллельное программирование! И вместо того, чтобы встраивать всё это в язык, давайте сделаем систему макросов.»

Посмотрим, что из себя представляют эти возможности метапрограммирования. Nim предлагает шаблоны ( template ) и макросы ( macro ) для этих целей.

Шаблоны для ленивых вычислений

template это просто механизм подстановки. Вот template , названный log :

template log(msg: string) = if debug: echo msg log("x: " & $x & ", y: " & $y)

Вы можете читать их как разновидность функции, но принципиальное отличие в том, что они разворачиваются в коде прямо на месте (там, где вы вызываете log ).

Сравните код выше со следующим кодом на C, где log это #define :

#define log(msg) \ if (debug) < \ print(msg); \ >log("x: " + x.toString() + ", y: " + y.toString());

Очень похоже! Причина почему это template (или #define ) в том, что мы хотим, чтобы сообщение в параметре вычислялось лениво, потому что в этом примере я задействую дорогие операции, такие как конкатенация строк и обращение переменных в строки, и если debug выключен, этот код не должен быть выполнен. Семантика передачи простого аргумента такая: «выполни это выражение и потом вызови функцию», но потом внутри функции вы обнаруживаете, что debug выключен и вся эта информация вам не нужна, её вообще можно было не вычислять. Это и есть то, что что нам позволяет template , поскольку он разворачивается непосредственно при вызове: если debug равен false , тогда это сложное выражение из конкатенаций не будет выполняться вообще.

Шаблоны для абстракции потока управления:

Мы можем воспользоваться template для абстракции потока управления. Если мы хотим инструкцию withLock , C# предлагает примитив языка, а в Nim вам вообще не нужно встраивать это в язык, вы просто пишете withLock шаблон и он запрашивает блокировку:

template withLock(lock, body) = var lock: Lock try: acquire lock body finally: release lock withLock myLock: accessProtectedResource()
  • withLock запрашивает блокировку и в конце отпускает её.
  • внутри куска, где происходит блокировка, целиком выполняется body , которое может быть передано в withLock через конструкцию с двоеточием и отступами.
Макрос для реализации DSL

Вы можете использовать макросы для реализации DSL .

Пример DSL, описывающий код на html:

html mainPage: head: title "Zen of Nim" body: ul: li "A bunch of rules that make no sense." echo mainPage()

Этот код производит следующее:

Лифтинг

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

Например, у нас есть квадратный корень для чисел с плавающей точкой, и теперь мы хотим операцию квадратного корня, которая будет работать для списка чисел с плавающей точкой. Я мог бы использовать вызов map, но также я могу создать выведенную функцию sqrt :

import math template liftFromScalar(fname) = proc fname[T](x: openArray[T]): seq[T] = result = newSeq[typeof(x[0])](x.len) for i in 0.. @[2.0, 4.0, 5.0, 6.0]
  • Мы передаём fname в шаблон и fname применяется к каждому элементу последовательности.
  • Конечное имя процедуры ( proc ) такое же, как fname ( sqrt в этом случае)
Декларативное программирование

Вы можете превратить императивный код в декларативный.

Вот пример, вытащенный из нашего инструментария тестирования:

proc threadTests(r: var Results, cat: Category, options: string) = template test(filename: untyped) = testSpec r, makeTest("tests/threads" / filename, options, cat, actionRun) testSpec r, makeTest("tests/threads" / filename, options & " -d:release", cat, actionRun) testSpec r, makeTest("tests/threads" / filename, options & " --tlsEmulation:on", cat, actionRun) test "tactors" test "tactors2" test "threadex"

Это несколько потоков тестов с именами tactors , tactors2 и threadex , и каждый из них выполняется в трёх разных конфигурациях: с параметрами по дефолту, дефолт плюс флаг release, дефолт плюс эмуляции локальной памяти потока. Вызов threadTests требует множество параматров (категория, опции и имя файла), что утомительно, если вы просто копируете их снова и снова, так что здесь я бы хотел сказать: «Это будет тест под названием tactors , вот этот tactors2 , а вот этот тест будет называться threadex », и сократив всё это, мы оказываемся на том уровне абстракции, на котором вы действительно собирались работать:

test "tactors" test "tactors2" test "threadex"

Можно даже ещё сократить, поскольку все эти вызовы test немного раздражают. На самом деле я бы хотел сказать следующее:

test "tactors", "tactors2", "threadex"

А вот простой макрос, который это осуществляет:

import macros macro apply(caller: untyped; args: varargs[untyped]): untyped = result = newStmtList() for a in args: result.add(newCall(caller, a)) apply test, "tactors", "tactors2", "threadex"

Поскольку он очень прост, он не может довести дело до конца, и от вас требуется сказать apply test . Этот макрос создаёт список инструкций и каждая инструкция в этом списке на самом деле это вызов выражения, вызывающего этот тест с a ( a это текущий аргумент, мы итерируемся по всем аргументам).

Детали не так важны, главный инсайт здесь в том, что Nim даёт вам возможность делать подобные вещи. И как только вы немного привыкнете, это окажется удивительно просто.

Типобезоапсные Writeln/Printf

Следующий пример это макрос, дающий нам типобезопасный printf :

proc write(f: File; a: int) = echo a proc write(f: File; a: bool) = echo a proc write(f: File; a: float) = echo a proc writeNewline(f: File) = echo "\n" macro writeln*(f: File; args: varargs[typed]) = result = newStmtList() for a in args: result.add newCall(bindSym"write", f, a) result.add newCall(bindSym"writeNewline", f)
  • Как и ранее, мы создаём список инструкций в первой строчке макроса, и далее, итерируясь по каждому аргументу, вызваем функцию, вызывающую write .
  • bindSym"write" биндится с write , но это не один и тот же write , а перегружающаяся операция, потому что в начале примера стоят три операции write (для int , bool и float ), и перегрузка разрешает выбор правильной операции write .
  • Наконец, в последней строчке макроса стоит вызов функции writeNewline , объявленной ранее (она делает отбивку строки)

Практичный язык

Компилятор умён, но:

Не стой на пути у программиста

Существует огромное количество кода, написанного на C++, C и Javascript, который программистам очень нужно переиспользовать. Мы имеем совместимость с C++, C и JavaScript, потому что мы можем скомпилировать Nim в эти языки. Заметьте, что это реализация именно идеи совместимости, философия за этим решением вовсе не в том, что «давайте использовать C++ плюс Nim, потому что Nim не предоставляет некоторых функций, которые нам нужны, чтобы закончить работу». Nim действительно предлагает низкоуровневые возможности, такие как:

  • bit twiddling,
  • небезопасная конвертация типов ( cast ),
  • сырые указатели.

Взаимодействие с C++ — это крайняя мера, обычно мы хотим, чтобы вы писали Nim-код и не покидали Nim. Но тут в дело вступает реальный мир и говорит: «Эй, есть куча кода, уже написанного на этих языках, как насчет того, чтобы сделать взаимодействие с ним очень хорошим?».

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

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

 proc embedsC() = var nimVar = 89 embedsC()

Вы можете emit static int cvariable , при этом коммуникация работает в обе стороны, так что вы также можете emit инструкцию fprintf , где переменная nimVar , на самом деле, приходит из Nim (квадратные скобки позволяют использовать строки и именованные выражения одновременно в одном окружении). Код на C может использовать код на Nim и наоборот. Тем не менее, это не самый хороший способ взаимодействия языков, это просто демонстрация того, что мы хотим, чтобы вы могли сделать это в случае необходимости.

Гораздо лучший способ взаимодействия когда вы просто говорите Nim’у: «Эй, вот здесь функция fprintf , она приходит из C, а это её типы, я бы хотел иметь возможность её вызывать». Тем не менее, прагма emit хорошо показывает, что мы хотим, чтобы этот язык был практичным.

Настраиваемое управление памятью

И теперь совсем другая тема, так как мы совсем не поговорили об управлении памятью. В новой версии Nim базируется на деструкторах, которые вызываются в режиме gc:arc или gc:orc . Деструкторы и владение, я предполагаю, знакомые вам понятия из C++ и Rust.

Параметр sink здесь означает, что функция получает во владение строку (и потом не делает ничего с x ):

func f(x: sink string) = discard "do nothing" f "abc"

Вопрос в следующем: «произвёл ли я утечку памяти? что произошло?». Вы можете попросить компилятор Nim: «Слушай, разверни эту функцию f для меня; покажи где там стоят деструкторы, где происходят перемещения (moves), а где глубокое копирование» (скомпилируем с nim c --gc:orc --expandArc:f $file ).

Компилятор вам ответит: «Смотри, функция f это, по сути, твоя инструкция discard и я добавил вызов деструктора в самом конце»:

func f(x: sink string) = discard "do nothing" `=destroy`(x)

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

Вот другой пример:

var g: string proc f(x: sink string) = g = x f "abc"

Теперь я беру x во владение и действительно что-то делаю, пока владею ей, а именно кладу x в глобальную переменную g . Снова, мы можем спросить компилятор что он сделает и компилятор ответит: «Это операция перемещения (move) , она называется =sink ». Так мы перемещаем x в g , и это перемещение позаботится о том, чтобы освободить то, что находится в g (если там что-то было), а затем поместить туда значение x :

var g: string proc f(x: sink string) = `=sink`(g, x) f "abc"

Так вот, на самом деле здесь происходит, и, к сожалению, это не совсем очевидно, то, что компилятор сообщает: «ладно, x перемещается в g , а когда будет сказано, что x перемещён, вызвать деструктор». Но вот это wasMoved и =destroy отменяют друг друга, так что компилятор провёл для нас здесь оптимизацию:

var g: string proc f(x: sink string) = `=sink`(g, x) # optimized out: wasMoved(x) `=destroy`(x) f "abc"
Собственный контейнер

Вы можете использовать эти перемещения, деструкторы и присвоения копированием (copy assignments) для создания собственных структур данных.

У меня есть несколько коротких примеров, но я не буду останавливаться на их деталях.

Деструктор:

type myseq*[T] = object len, cap: int data: ptr UncheckedArray[T] proc `=destroy`*[T](x: var myseq[T]) = if x.data != nil: for i in 0..

Оператор перемещения:

proc `=sink`*[T](a: var myseq[T]; b: myseq[T]) = # move assignment, optional. # Compiler is using `=destroy` and # `copyMem` when not provided `=destroy`(a) a.len = b.len a.cap = b.cap a.data = b.data

Оператор присвоения:

proc `=copy`*[T](a: var myseq[T]; b: myseq[T]) = # do nothing for self-assignments: if a.data == b.data: return `=destroy`(a) a.len = b.len a.cap = b.cap if b.data != nil: a.data = cast[typeof(a.data)](alloc(a.cap * sizeof(T))) for i in 0..

Предоставление доступа:

proc add*[T](x: var myseq[T]; y: sink T) = if x.len >= x.cap: resize(x) x.data[x.len] = y inc x.len proc `[]`*[T](x: myseq[T]; i: Natural): lent T = assert i < x.len x.data[i] proc `[]=`*[T](x: var myseq[T]; i: Natural; y: sink T) = assert i < x.len x.data[i] = y

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

И это очередное правило Nim:

Настраиваемое управление памятью

Zen of Nim

Давайте повторим все правила ещё раз в качестве итога:

  • Копирование плохого дизайна это плохой дизайн: мы хотим принимать хорошие решения, основываясь на первичных принципах, идущих непосредственно от проблемы.
  • Если компилятор не может рассуждать о коде, то и программист не может.
  • Тем не менее, не стой у программиста на пути. Компилятор как умный пёсик: вы можете обучить его новым трюкам и он действительно помогает вам, он может выполнять какие-то задания для вас, принести газету. Но в конечном счёте программист умнее компилятора.
  • Мы хотим перенести работу на время компиляции, потому что программы гораздо чаще запускаются, чем компилируются.
  • Мы хотим настраиваемое управление памятью.
  • Лаконичный код не мешает читабельности, он ей способствует.
  • Было ещё одно правило Дзена, призывающее задействовать метапрограммирование, чтобы оставить язык компактным. Однако сложно оставаться абсолютно искренним в этом месте, учитывая, сколько возможностей предлагает Nim. Есть некоторое напряжение между «мы хотим, чтобы язык был полным» и «мы хотим, чтобы язык был минималистичным». Чем старше становится Nim, тем больше он склоняется к полноте (все минималистичные языки вырастают, чтобы удовлетворять определённым потребностям).
  • Оптимизация это специализация. Я ещё не говорил про это правило, но если вам нужно больше скорости, вы действительно должны подумать о том, чтобы написать собственный код. Стандартная библиотека Nim не может предложить всё для всех, и для нас также гораздо сложнее предоставить вам лучшую библиотеку для всего, потому что лучшая библиотека должна быть общего назначения, она должна быть самой быстрой библиотекой, она должна иметь наименьшее количество накладных расходов для вашего времени компиляции, и этого действительно трудно достичь. Гораздо проще сказать: «хорошо, Nim предлагает это в качестве стандартной библиотеки, но здесь я сам написал 10 строчек, я могу забенчмаркнуть их и скорее всего мой собственный код будет быстрее, потому что он подогнан вручную для моего приложения». Так на самом деле: специализируйте свой код и он будет выполняться быстрее.
  • Наконец, должен быть только один язык программирования для всего.
    И этот язык — Nim.

Спасибо за чтение!

  • nim
  • zen of nim
  • design principles
  • компилятор
  • метапрограммирование
  • управление памятью
  • C
  • структурное программирование
  • декларативное программирование

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

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