Справочник по Nim
"Сложность" кажется очень похожей на "энергию": вы можете передавать ее от конечного пользователя одному или нескольким другим игрокам, но общее количество, похоже, остается практически постоянным для данной задачи. -- Ран
О данном документе
Примечание: Данный документ является черновиком! Некоторые функции Nim могут потребовать более точной формулировки. Этот справочник постоянно развивается в полноценную спецификацию.
Примечание: Экспериментальные функции Nim описаны здесь.
Примечание: Присваивания, перемещения и уничтожение описаны в документе уничтожители.
В данном документе описываются лексика, синтаксис и семантика языка Nim.
Чтобы узнать, как компилировать программы Nim и генерировать документацию, см. Руководство пользователя компилятора и Руководство по инструментам DocGen.
Конструкции языка объясняются с использованием расширенного BNF, в котором (a)* означает 0 или более a, a+ означает 1 или более a, а (a)? означает необязательное a. Скобки могут использоваться для группировки элементов.
& является оператором предпросмотра; &a означает, что a ожидается, но не потребляется. Он будет потреблен в следующем правиле.
Символы |, / используются для обозначения альтернатив и имеют наименьший приоритет. / — это упорядоченный выбор, который требует от парсера попробовать альтернативы в заданном порядке. / часто используется для обеспечения того, что грамматика не является неоднозначной.
Нетерминалы начинаются с маленькой буквы, абстрактные терминальные символы — заглавными. Буквальные терминальные символы (включая ключевые слова) заключены в кавычки с помощью '. Пример:
ifStmt = 'if' expr ':' stmts ('elif' expr ':' stmts)* ('else' stmts)? Бинарный оператор ^* используется в качестве сокращения для 0 или более повторений, разделенных вторым аргументом; аналогично, ^+ означает 1 или более повторений: a ^+ b — это сокращение от a (b a)* и a ^* b — это сокращение от (a (b a)*)?. Пример:
arrayConstructor = '[' expr ^* ',' ']'
Другие части Nim, такие как правила области видимости или семантика выполнения, описываются неформально.
Определения
Код Nim определяет вычисление, которое действует на памяти, состоящей из компонентов, называемых позициями. Переменная — это, по сути, имя для позиции. Каждая переменная и позиция имеют определенный тип. Тип переменной называется статическим типом, тип позиции называется динамическим типом. Если статический тип не совпадает с динамическим типом, он является надтипом или подтипом динамического типа.
Идентификатор — это символ, объявленный как имя переменной, типа, процедуры и т. д. Область программы, на которую распространяется объявление, называется областью видимости объявления. Области видимости могут быть вложенными. Значение идентификатора определяется самой внутренней областью видимости, в которой идентификатор объявлен, если правила разрешения перегрузки не указывают иного.
Выражение задает вычисление, которое производит значение или позицию. Выражения, которые производят позиции, называются l-значениями. L-значение может обозначать либо позицию, либо значение, содержащееся в позиции, в зависимости от контекста.
Программа Nim программа состоит из одного или нескольких текстовых исходных файлов, содержащих код Nim. Она обрабатывается компилятором Nim компилятором в исполняемый файл. Характер этого исполняемого файла зависит от реализации компилятора; например, это может быть собственный двоичный файл или исходный код JavaScript.
В типичной программе Nim большая часть кода компилируется в исполняемый файл. Однако часть кода может выполняться на стадии компиляции. Это могут быть константные выражения, определения макросов и процедуры Nim, используемые определениями макросов. Большая часть языка Nim поддерживается на стадии компиляции, но существуют некоторые ограничения — см. Ограничения на выполнение во время компиляции для получения подробной информации. Мы используем термин выполнение для обозначения как выполнения во время компиляции, так и выполнения кода в исполняемом файле.
Компилятор анализирует исходный код Nim, создавая внутреннюю структуру данных, называемую деревом абстрактного синтаксиса (AST). Затем, прежде чем выполнить код или скомпилировать его в исполняемый файл, он преобразует AST с помощью семантического анализа. Это добавляет семантическую информацию, такую как типы выражений, значения идентификаторов и в некоторых случаях значения выражений. Ошибка, обнаруженная во время семантического анализа, называется статической ошибкой. Ошибки, описанные в этом руководстве, являются статическими ошибками, если не указано иное.
Ошибка ошибка — это ошибка, которую реализация обнаруживает и сообщает во время выполнения. Способ сообщения об таких ошибках заключается в вызове исключений или выходе с ошибкой. Однако реализация предоставляет возможность отключить эти проверки во время выполнения. Подробности см. в разделе Макросы.
Результат ошибки приведёт к исключению или ошибке — зависит от конкретной реализации. Таким образом, следующая программа недействительна; хотя код, кажется, пытается перехватить IndexDefect из доступа к массиву за пределами границ, компилятор может вместо этого выбрать позволить программе выйти с ошибкой.
var a: array[0..1, char] let i = 5 try: a[i] = 'N' except IndexDefect: echo "invalid index"
Текущая реализация позволяет переключаться между этими различными поведением с помощью --panics:on|off. Если ошибки включены, программа выходит с ошибкой, если выключены, ошибки во время выполнения преобразуются в исключения. Преимущество --panics:on заключается в том, что он создает более компактный двоичный код, а компилятор имеет большую свободу в оптимизации кода.
Ошибка непроверенной ошибки во время выполнения — это ошибка, которая не гарантированно обнаруживается и может привести к произвольному поведению последующего вычисления. Непроверенные ошибки во время выполнения не могут возникнуть, если используются только безопасные функции языка и если не отключены проверки во время выполнения.
Константное выражение — это выражение, значение которого может быть вычислено во время семантического анализа кода, в котором оно появляется. Оно никогда не является l-значением и никогда не имеет побочных эффектов. Константные выражения не ограничиваются возможностями семантического анализа, такими как свёртка констант; они могут использовать все функции языка Nim, поддерживаемые для выполнения во время компиляции. Поскольку константные выражения могут использоваться в качестве входных данных для семантического анализа (например, для определения границ массивов), эта гибкость требует от компилятора чередовать семантический анализ и выполнение кода во время компиляции.
В основном, можно представить себе семантический анализ, проходящий сверху вниз и слева направо в исходном коде, с чередованием выполнения кода во время компиляции при необходимости для вычисления значений, необходимых для последующего семантического анализа. Позже в этом документе мы увидим, что вызов макроса не только требует такого чередования, но также создаёт ситуацию, в которой семантический анализ не полностью происходит сверху вниз и слева направо.
Лексический анализ
Кодировка
Все исходные файлы Nim используют кодировку UTF-8 (или ее подмножество ASCII). Другие кодировки не поддерживаются. Любые стандартные последовательности символов завершения строки платформы могут быть использованы — форма Unix с использованием ASCII LF (перевод строки), форма Windows с использованием последовательности ASCII CR LF (возврат каретки, за которым следует перевод строки) или старая форма Macintosh с использованием символа ASCII CR (возврат каретки). Все эти формы могут использоваться одинаково, независимо от платформы.
Отступы
Стандартная грамматика Nim описывает язык, чувствительный к отступам. Это означает, что все управляющие структуры распознаются по отступам. Отступы состоят только из пробелов; табуляции не допускаются.
Обработка отступов реализована следующим образом: лексерор добавляет в следующую лексему количество предшествующих пробелов; отступ не является отдельной лексемой. Этот приём позволяет парсить Nim с использованием только 1 лексемы предпросмотра.
Парсер использует стек уровней отступа: стек состоит из целых чисел, подсчитывающих пробелы. Информация об отступе запрашивается в стратегических местах в парсере, но игнорируется в остальных случаях: псевдотерминал IND{>} обозначает отступ, состоящий из большего количества пробелов, чем запись в верхней части стека; IND{=} обозначает отступ с таким же количеством пробелов. DED — это ещё один псевдотерминал, описывающий действие извлечения значения из стека; IND{>} затем подразумевает помещение в стек.
С помощью этой нотации мы можем легко определить основную часть грамматики: блок операторов (упрощенный пример):
ifStmt = 'if' expr ':' stmt
(IND{=} 'elif' expr ':' stmt)*
(IND{=} 'else' ':' stmt)?
simpleStmt = ifStmt / ...
stmt = IND{>} stmt ^+ IND{=} DED # list of statements
/ simpleStmt # or a simple statement Комментарии
Комментарии начинаются где угодно вне строки или литерала символа с символом решётки #. Комментарии состоят из конкатенации частей комментария. Часть комментария начинается с # и продолжается до конца строки. Символы конца строки входят в состав части. Если следующая строка состоит только из части комментария без других лексем между ней и предыдущей, она не начинает новый комментарий:
i = 0 # This is a single comment over multiple lines. # The scanner merges these two pieces. # The comment continues here.
Комментарии документации — это комментарии, начинающиеся с двух ##. Комментарии документации являются лексемами; они разрешены только в определённых местах в исходном файле, поскольку они принадлежат дереву синтаксического анализа!
Многострочные комментарии
Начиная с версии 0.13.0 языка Nim поддерживает многострочные комментарии. Они выглядят так:
#[Comment here. Multiple lines are not a problem.]#
Многострочные комментарии поддерживают вложенность:
#[ #[ Multiline comment in already commented out code. ]# proc p[T](x: T) = discard ]#
Многострочные комментарии документации тоже существуют и поддерживают вложенность:
proc foo = ##[Long documentation comment here. ]##
Идентификаторы и ключевые слова
Идентификаторы в Nim могут быть любыми строками букв, цифр и символов подчеркивания со следующими ограничениями:
- начинается с буквы
- не заканчивается на символ подчеркивания
_ -
два последовательных символа подчеркивания
__не допускаются::буква ::= 'A'..'Z' | 'a'..'z' | 'x80'..'xff' цифра ::= '0'..'9' ИДЕНТИФИКАТОР ::= буква ( ['_'] (буква | цифра) )*
В настоящее время любой символ Unicode с порядковым значением > 127 (не-ASCII) классифицируется как letter и может, таким образом, быть частью идентификатора, но в более поздних версиях языка некоторые символы Unicode могут быть отнесены к операторным символам.
Следующие ключевые слова зарезервированы и не могут использоваться в качестве идентификаторов:
addr and as asm bind block break case cast concept const continue converter defer discard distinct div do elif else end enum except export finally for from func if import in include interface is isnot iterator let macro method mixin mod nil not notin object of or out proc ptr raise ref return shl shr static template try tuple type using var when while xor yield
Некоторые ключевые слова не используются; они зарезервированы для будущих разработок языка.
Равенство идентификаторов
Два идентификатора считаются равными, если следующий алгоритм возвращает true:
proc sameIdentifier(a, b: string): bool =
a[0] == b[0] and
a.replace("_", "").toLowerAscii == b.replace("_", "").toLowerAscii
Это означает, что только первые буквы сравниваются в регистрозависимом режиме. Другие буквы сравниваются без учета регистра в диапазоне ASCII, а подчеркивания игнорируются.
Этот довольно нестандартный способ сравнения идентификаторов называется частичной регистронезависимостью и имеет некоторые преимущества по сравнению с обычной регистрозависимостью:
Он позволяет программистам в основном использовать свой предпочтительный стиль написания, будь то humpStyle или snake_style, и библиотеки, написанные разными программистами, не могут использовать несовместимые соглашения. Редактор или IDE, понимающий Nim, может отображать идентификаторы в предпочтительном стиле. Еще одно преимущество заключается в том, что программист освобождается от необходимости запоминать точное написание идентификатора. Исключение относительно первой буквы позволяет без проблем парсить распространенный код, например var foo: Foo.
Обратите внимание, что это правило также применяется к ключевым словам, что означает, что notin эквивалентно notIn и not_in (форма с прописными буквами (notin, isnot) является предпочтительным способом написания ключевых слов).
Исторически Nim был полностью нечувствительным к стилю языком. Это означало, что он был нечувствителен к регистру, подчеркивания игнорировались, и даже не было различия между foo и Foo.
Выделение
Если ключевое слово заключено в обратные кавычки, оно теряет свою собственность ключевого слова и становится обычным идентификатором.
Примеры
var `var` = "Hello Stropping"
type Type = object `int`: int let `object` = Type(`int`: 9) assert `object` is Type assert `object`.`int` == 9 var `var` = 42 let `let` = 8 assert `var` + `let` == 50 const `assert` = true assert `assert`
Строковые литералы
Терминальный символ в грамматике: STR_LIT.
Строковые литералы могут быть ограничены соответствующими двойными кавычками и могут содержать следующие последовательности экранирования:
| Последовательность экранирования | Значение |
|---|---|
\p |
новая строка, зависящая от платформы: CRLF в Windows, LF в Unix |
\r, \c
|
возврат каретки |
\n, \l
|
перевод строки (часто называемый новой строкой) |
\f |
формат страницы |
\t |
табуляция |
\v |
вертикальная табуляция |
\\ |
обратная косая черта |
\" |
двойная кавычка |
\' |
апостроф |
\ '0'..'9'+ |
символ с десятичным значением d; все десятичные цифры, непосредственно следующие за ним, используются для символа |
\a |
сигнал |
\b |
возврат назад |
\e |
экранирование [ESC] |
\x HH |
символ с шестнадцатеричным значением HH; допускается ровно две шестнадцатеричные цифры |
\u HHHH |
код Юникода с шестнадцатеричным значением HHHH; допускается ровно четыре шестнадцатеричные цифры |
\u {H+} |
код Юникода; все шестнадцатеричные цифры, заключенные в {}, используются для кода |
Строки в Nim могут содержать любое 8-битное значение, даже вставленные нули. Однако некоторые операции могут интерпретировать первый двоичный ноль как терминатор.
Строковые литералы с тройными кавычками
Терминальный символ в грамматике: TRIPLESTR_LIT.
Строковые литералы также могут быть ограничены тремя двойными кавычками """ ... """. Литералы в этой форме могут занимать несколько строк, могут содержать " и не интерпретируют никакие последовательности экранирования. Для удобства, когда открывающая """ следует за новой строкой (между открывающей """ и новой строкой могут быть пробелы), новая строка (и предшествующие пробелы) не включается в строку. Окончание строкового литерала определяется шаблоном """[^"], поэтому это:
""""long string within quotes""""
Производит:
"long string within quotes"
Необработанные строковые литералы
Терминальный символ в грамматике: RSTR_LIT.
Также существуют необработанные строковые литералы, которые предваряются буквой r (или R) и ограничиваются соответствующими двойными кавычками (как и обычные строковые литералы) и не интерпретируют последовательности экранирования. Это особенно удобно для регулярных выражений или путей Windows:
var f = openFile(r"C:\texts\text.txt") # a raw string, so ``\t`` is no tab
Чтобы получить одиночный " внутри необработанного строкового литерала, его необходимо удвоить:
r"a""b"
Производит:
a"b
r"""" невозможен с этой нотацией, потому что три ведущие кавычки вводят строковый литерал с тройными кавычками. r""" эквивалентно """, поскольку строковые литералы с тройными кавычками также не интерпретируют последовательности экранирования.
Обобщенные необработанные строковые литералы
Терминальные символы в грамматике: GENERALIZED_STR_LIT, GENERALIZED_TRIPLESTR_LIT.
Конструкция identifier"string literal" (без пробелов между идентификатором и открывающей кавычкой) является обобщенным необработанным строковым литералом. Это сокращение для конструкции identifier(r"string literal"), поэтому это обозначает вызов процедуры со строковым литералом в качестве единственного аргумента. Обобщенные необработанные строковые литералы особенно удобны для встраивания мини-языков непосредственно в Nim (например, регулярных выражений).
Также существует конструкция identifier"""string literal""". Это сокращение для identifier("""string literal""").
Символьные литералы
Символьные литералы заключаются в одинарные кавычки '' и могут содержать те же последовательности экранирования, что и строки, за исключением одного: зависимая от платформы новая строка (\p) не допускается, так как она может быть шире одного символа (например, часто это пара CR/LF). Вот допустимые последовательности экранирования для символьных литералов:
| Последовательность экранирования | Значение |
|---|---|
\r, \c
|
возврат каретки |
\n, \l
|
перевод строки |
\f |
формат страницы |
\t |
табуляция |
\v |
вертикальная табуляция |
\\ |
обратная косая черта |
\" |
двойная кавычка |
\' |
апостроф |
\ '0'..'9'+ |
символ с десятичным значением d; все десятичные цифры, непосредственно следующие за ним, используются для символа |
\a |
сигнал |
\b |
возврат назад |
\e |
экранирование [ESC] |
\x HH |
символ с шестнадцатеричным значением HH; допускается ровно две шестнадцатеричные цифры |
Символ не является символом Юникода, а отдельным байтом. Причиной является эффективность: для подавляющего большинства случаев использования полученные программы все равно будут правильно обрабатывать UTF-8, так как UTF-8 был специально разработан для этого. Еще одна причина заключается в том, что Nim может эффективно поддерживать array[char, int] или set[char], так как многие алгоритмы полагаются на эту функцию. Тип Rune используется для символов Юникода, он может представлять любой символ Юникода. Rune объявлен в модуле unicode.
Числовые константы
Числовые константы имеют единый тип и имеют вид:
hexdigit = digit | 'A'..'F' | 'a'..'f'
octdigit = '0'..'7'
bindigit = '0'..'1'
HEX_LIT = '0' ('x' | 'X' ) hexdigit ( ['_'] hexdigit )*
DEC_LIT = digit ( ['_'] digit )*
OCT_LIT = '0' 'o' octdigit ( ['_'] octdigit )*
BIN_LIT = '0' ('b' | 'B' ) bindigit ( ['_'] bindigit )*
INT_LIT = HEX_LIT
| DEC_LIT
| OCT_LIT
| BIN_LIT
INT8_LIT = INT_LIT ['\''] ('i' | 'I') '8'
INT16_LIT = INT_LIT ['\''] ('i' | 'I') '16'
INT32_LIT = INT_LIT ['\''] ('i' | 'I') '32'
INT64_LIT = INT_LIT ['\''] ('i' | 'I') '64'
UINT_LIT = INT_LIT ['\''] ('u' | 'U')
UINT8_LIT = INT_LIT ['\''] ('u' | 'U') '8'
UINT16_LIT = INT_LIT ['\''] ('u' | 'U') '16'
UINT32_LIT = INT_LIT ['\''] ('u' | 'U') '32'
UINT64_LIT = INT_LIT ['\''] ('u' | 'U') '64'
exponent = ('e' | 'E' ) ['+' | '-'] digit ( ['_'] digit )*
FLOAT_LIT = digit (['_'] digit)* (('.' digit (['_'] digit)* [exponent]) |exponent)
FLOAT32_SUFFIX = ('f' | 'F') ['32']
FLOAT32_LIT = HEX_LIT '\'' FLOAT32_SUFFIX
| (FLOAT_LIT | DEC_LIT | OCT_LIT | BIN_LIT) ['\''] FLOAT32_SUFFIX
FLOAT64_SUFFIX = ( ('f' | 'F') '64' ) | 'd' | 'D'
FLOAT64_LIT = HEX_LIT '\'' FLOAT64_SUFFIX
| (FLOAT_LIT | DEC_LIT | OCT_LIT | BIN_LIT) ['\''] FLOAT64_SUFFIX Как видно из правил, числовые константы могут содержать подчеркивания для лучшей читаемости. Целочисленные и вещественные литералы могут быть заданы в десятичном (без префикса), двоичном (префикс 0b), восьмеричном (префикс 0o) и шестнадцатеричном (префикс 0x) формате.
Существует литерал для каждого числового типа, который определен. Суффикс, начинающийся с апострофа ('''), называется суффиксом типа. Литералы без суффикса типа имеют целочисленный тип, если литерал не содержит точки или E|e, в противном случае он имеет тип float. Этот целочисленный тип равен int, если литерал находится в диапазоне low(i32)..high(i32), в противном случае он равен int64. Для удобства записи апостроф в суффиксе типа необязателен, если он не двусмысленен (только шестнадцатеричные вещественные литералы с суффиксом типа могут быть двусмысленными).
Суффиксы типов:
| Суффикс типа | Результат типа литерала |
|---|---|
'i8 |
int8 |
'i16 |
int16 |
'i32 |
int32 |
'i64 |
int64 |
'u |
uint |
'u8 |
uint8 |
'u16 |
uint16 |
'u32 |
uint32 |
'u64 |
uint64 |
'f |
float32 |
'd |
float64 |
'f32 |
float32 |
'f64 |
float64 |
Числовые литералы с плавающей точкой также могут быть представлены в двоичной, восьмеричной или шестнадцатеричной нотациях: 0B0_10001110100_0000101001000111101011101111111011000101001101001001'f64 приблизительно равно 1.72826e35 согласно стандарту IEEE для чисел с плавающей точкой.
Литералы проверяются на соответствие типу данных. Литералы, не использующие десятичную систему счисления, в основном применяются для флагов и представления битовых шаблонов, поэтому проверка границ выполняется по разрядности, а не диапазону значений. Если литерал помещается в разрядность типа данных, он принимается. Следовательно: 0b10000000'u8 == 0x80'u8 == 128, но 0b10000000'i8 == 0x80'i8 == -1 вместо выдачи ошибки переполнения.
Операторы
Nim позволяет определять пользовательские операторы. Оператор представляет собой любое сочетание следующих символов:
= + - * / < > @ $ ~ & % | ! ? ^ . : \
(В грамматике используется терминал OPR для обозначения символов операторов, как определено здесь.)
Эти ключевые слова также являются операторами: and or not xor shl shr div mod in notin is isnot of as from.
. =, :, :: недоступны в качестве общих операторов; они используются для других целей обозначения.
*: в качестве специального случая обрабатывается как два токена * и : (чтобы поддерживать var v*: T).
Ключевое слово not всегда является унарным оператором, a not b анализируется как a(not b), а не как (a) not (b).
Другие токены
Следующие строки обозначают другие токены:
` ( ) { } [ ] , ; [. .] {. .} (. .) [: Оператор среза .. имеет приоритет над другими токенами, содержащими точку: {..} представляют собой три токена {, .., }, а не два токена {., .}.
Синтаксис
В этом разделе перечислены стандартные синтаксические правила Nim. Способ обработки вложенности парсером уже описан в разделе Лексический анализ.
Nim позволяет определять пользовательские операторы. Бинарные операторы имеют 11 различных уровней приоритета.
Ассоциативность
Бинарные операторы, первый символ которых ^ являются правоассоциативными, все остальные бинарные операторы являются левоассоциативными.
proc `^/`(x, y: float): float = # a right-associative division operator result = x / y echo 12 ^/ 4 ^/ 8 # 24.0 (4 / 8 = 0.5, then 12 / 0.5 = 24.0) echo 12 / 4 / 8 # 0.375 (12 / 4 = 3.0, then 3 / 8 = 0.375)
Приоритет
Унарные операторы всегда связываются сильнее, чем любые бинарные операторы: $a + b является ($a) + b, а не $(a + b).
Если первый символ унарного оператора @ это оператор похожий на сигил, который связывается сильнее, чем primarySuffix: @x.abc анализируется как (@x).abc, тогда как $x.abc анализируется как $(x.abc).
Для бинарных операторов, которые не являются ключевыми словами, приоритет определяется следующими правилами:
Операторы, заканчивающиеся на ->, ~> или =>, называются операторами, похожими на стрелки, и имеют самый низкий приоритет из всех операторов.
Если оператор заканчивается на =, а его первый символ не является одним из <, >, !, =, ~, ?, он является оператором присваивания, который имеет второй по низкости приоритет.
В противном случае приоритет определяется первым символом.
| Уровень приоритета | Операторы | Первый символ | Терминальный символ |
|---|---|---|---|
| 10 (наивысший) | $ ^ |
OP10 | |
| 9 | * / div mod shl shr % |
* % \ / |
OP9 |
| 8 | + - |
+ - ~ | |
OP8 |
| 7 | & |
& |
OP7 |
| 6 | .. |
. |
OP6 |
| 5 | == <= < >= > != in notin is isnot not of as from |
= < > ! |
OP5 |
| 4 | and |
OP4 | |
| 3 | or xor |
OP3 | |
| 2 | @ : ? |
OP2 | |
| 1 |
оператор присваивания (например, +=, *=) |
OP1 | |
| 0 (наименьший) |
оператор, похожий на стрелку (например, ->, =>) |
OP0 |
Является ли оператор префиксным, также зависит от предшествующего пробела (это изменение синтаксического анализа было внесено в версию 0.13.0):
echo $foo # is parsed as echo($foo)
Пробелы также определяют, будет ли (a, b) интерпретироваться как список аргументов вызова, или как конструктор кортежа:
echo(1, 2) # pass 1 and 2 to echo
echo (1, 2) # pass the tuple (1, 2) to echo
Грамматика
Начальный символ грамматики — module.
# This file is generated by compiler/parser.nim.
module = stmt ^* (';' / IND{=})
comma = ',' COMMENT?
semicolon = ';' COMMENT?
colon = ':' COMMENT?
colcom = ':' COMMENT?
operator = OP0 | OP1 | OP2 | OP3 | OP4 | OP5 | OP6 | OP7 | OP8 | OP9
| 'or' | 'xor' | 'and'
| 'is' | 'isnot' | 'in' | 'notin' | 'of' | 'as' | 'from'
| 'div' | 'mod' | 'shl' | 'shr' | 'not' | 'static' | '..'
prefixOperator = operator
optInd = COMMENT? IND?
optPar = (IND{>} | IND{=})?
simpleExpr = arrowExpr (OP0 optInd arrowExpr)* pragma?
arrowExpr = assignExpr (OP1 optInd assignExpr)*
assignExpr = orExpr (OP2 optInd orExpr)*
orExpr = andExpr (OP3 optInd andExpr)*
andExpr = cmpExpr (OP4 optInd cmpExpr)*
cmpExpr = sliceExpr (OP5 optInd sliceExpr)*
sliceExpr = ampExpr (OP6 optInd ampExpr)*
ampExpr = plusExpr (OP7 optInd plusExpr)*
plusExpr = mulExpr (OP8 optInd mulExpr)*
mulExpr = dollarExpr (OP9 optInd dollarExpr)*
dollarExpr = primary (OP10 optInd primary)*
symbol = '`' (KEYW|IDENT|literal|(operator|'('|')'|'['|']'|'{'|'}'|'=')+)+ '`'
| IDENT | KEYW
exprColonEqExpr = expr (':'|'=' expr)?
exprList = expr ^+ comma
exprColonEqExprList = exprColonEqExpr (comma exprColonEqExpr)* (comma)?
dotExpr = expr '.' optInd (symbol | '[:' exprList ']')
explicitGenericInstantiation = '[:' exprList ']' ( '(' exprColonEqExpr ')' )?
qualifiedIdent = symbol ('.' optInd symbol)?
setOrTableConstr = '{' ((exprColonEqExpr comma)* | ':' ) '}'
castExpr = 'cast' ('[' optInd typeDesc optPar ']' '(' optInd expr optPar ')') /
parKeyw = 'discard' | 'include' | 'if' | 'while' | 'case' | 'try'
| 'finally' | 'except' | 'for' | 'block' | 'const' | 'let'
| 'when' | 'var' | 'mixin'
par = '(' optInd
( &parKeyw (ifExpr \ complexOrSimpleStmt) ^+ ';'
| ';' (ifExpr \ complexOrSimpleStmt) ^+ ';'
| pragmaStmt
| simpleExpr ( ('=' expr (';' (ifExpr \ complexOrSimpleStmt) ^+ ';' )? )
| (':' expr (',' exprColonEqExpr ^+ ',' )? ) ) )
optPar ')'
literal = | INT_LIT | INT8_LIT | INT16_LIT | INT32_LIT | INT64_LIT
| UINT_LIT | UINT8_LIT | UINT16_LIT | UINT32_LIT | UINT64_LIT
| FLOAT_LIT | FLOAT32_LIT | FLOAT64_LIT
| STR_LIT | RSTR_LIT | TRIPLESTR_LIT
| CHAR_LIT
| NIL
generalizedLit = GENERALIZED_STR_LIT | GENERALIZED_TRIPLESTR_LIT
identOrLiteral = generalizedLit | symbol | literal
| par | arrayConstr | setOrTableConstr
| castExpr
tupleConstr = '(' optInd (exprColonEqExpr comma?)* optPar ')'
arrayConstr = '[' optInd (exprColonEqExpr comma?)* optPar ']'
primarySuffix = '(' (exprColonEqExpr comma?)* ')'
| '.' optInd symbol generalizedLit?
| '[' optInd exprColonEqExprList optPar ']'
| '{' optInd exprColonEqExprList optPar '}'
| &( '`'|IDENT|literal|'cast'|'addr'|'type') expr # command syntax
pragma = '{.' optInd (exprColonEqExpr comma?)* optPar ('.}' | '}')
identVis = symbol OPR? # postfix position
identVisDot = symbol '.' optInd symbol OPR?
identWithPragma = identVis pragma?
identWithPragmaDot = identVisDot pragma?
declColonEquals = identWithPragma (comma identWithPragma)* comma?
(':' optInd typeDesc)? ('=' optInd expr)?
identColonEquals = IDENT (comma IDENT)* comma?
(':' optInd typeDesc)? ('=' optInd expr)?)
inlTupleDecl = 'tuple'
'[' optInd (identColonEquals (comma/semicolon)?)* optPar ']'
extTupleDecl = 'tuple'
COMMENT? (IND{>} identColonEquals (IND{=} identColonEquals)*)?
tupleClass = 'tuple'
paramList = '(' declColonEquals ^* (comma/semicolon) ')'
paramListArrow = paramList? ('->' optInd typeDesc)?
paramListColon = paramList? (':' optInd typeDesc)?
doBlock = 'do' paramListArrow pragma? colcom stmt
procExpr = 'proc' paramListColon pragma? ('=' COMMENT? stmt)?
distinct = 'distinct' optInd typeDesc
forStmt = 'for' (identWithPragma ^+ comma) 'in' expr colcom stmt
forExpr = forStmt
expr = (blockExpr
| ifExpr
| whenExpr
| caseStmt
| forExpr
| tryExpr)
/ simpleExpr
typeKeyw = 'var' | 'out' | 'ref' | 'ptr' | 'shared' | 'tuple'
| 'proc' | 'iterator' | 'distinct' | 'object' | 'enum'
primary = typeKeyw optInd typeDesc
/ prefixOperator* identOrLiteral primarySuffix*
/ 'bind' primary
typeDesc = simpleExpr ('not' expr)?
typeDefAux = simpleExpr ('not' expr)?
| 'concept' typeClass
postExprBlocks = ':' stmt? ( IND{=} doBlock
| IND{=} 'of' exprList ':' stmt
| IND{=} 'elif' expr ':' stmt
| IND{=} 'except' exprList ':' stmt
| IND{=} 'else' ':' stmt )*
exprStmt = simpleExpr
(( '=' optInd expr colonBody? )
/ ( expr ^+ comma
postExprBlocks
))?
importStmt = 'import' optInd expr
((comma expr)*
/ 'except' optInd (expr ^+ comma))
exportStmt = 'export' optInd expr
((comma expr)*
/ 'except' optInd (expr ^+ comma))
includeStmt = 'include' optInd expr ^+ comma
fromStmt = 'from' expr 'import' optInd expr (comma expr)*
returnStmt = 'return' optInd expr?
raiseStmt = 'raise' optInd expr?
yieldStmt = 'yield' optInd expr?
discardStmt = 'discard' optInd expr?
breakStmt = 'break' optInd expr?
continueStmt = 'break' optInd expr?
condStmt = expr colcom stmt COMMENT?
(IND{=} 'elif' expr colcom stmt)*
(IND{=} 'else' colcom stmt)?
ifStmt = 'if' condStmt
whenStmt = 'when' condStmt
condExpr = expr colcom expr optInd
('elif' expr colcom expr optInd)*
'else' colcom expr
ifExpr = 'if' condExpr
whenExpr = 'when' condExpr
whileStmt = 'while' expr colcom stmt
ofBranch = 'of' exprList colcom stmt
ofBranches = ofBranch (IND{=} ofBranch)*
(IND{=} 'elif' expr colcom stmt)*
(IND{=} 'else' colcom stmt)?
caseStmt = 'case' expr ':'? COMMENT?
(IND{>} ofBranches DED
| IND{=} ofBranches)
tryStmt = 'try' colcom stmt &(IND{=}? 'except'|'finally')
(IND{=}? 'except' exprList colcom stmt)*
(IND{=}? 'finally' colcom stmt)?
tryExpr = 'try' colcom stmt &(optInd 'except'|'finally')
(optInd 'except' exprList colcom stmt)*
(optInd 'finally' colcom stmt)?
exceptBlock = 'except' colcom stmt
blockStmt = 'block' symbol? colcom stmt
blockExpr = 'block' symbol? colcom stmt
staticStmt = 'static' colcom stmt
deferStmt = 'defer' colcom stmt
asmStmt = 'asm' pragma? (STR_LIT | RSTR_LIT | TRIPLESTR_LIT)
genericParam = symbol (comma symbol)* (colon expr)? ('=' optInd expr)?
genericParamList = '[' optInd
genericParam ^* (comma/semicolon) optPar ']'
pattern = '{' stmt '}'
indAndComment = (IND{>} COMMENT)? | COMMENT?
routine = optInd identVis pattern? genericParamList?
paramListColon pragma? ('=' COMMENT? stmt)? indAndComment
commentStmt = COMMENT
section(RULE) = COMMENT? RULE / (IND{>} (RULE / COMMENT)^+IND{=} DED)
enum = 'enum' optInd (symbol pragma? optInd ('=' optInd expr COMMENT?)? comma?)+
objectWhen = 'when' expr colcom objectPart COMMENT?
('elif' expr colcom objectPart COMMENT?)*
('else' colcom objectPart COMMENT?)?
objectBranch = 'of' exprList colcom objectPart
objectBranches = objectBranch (IND{=} objectBranch)*
(IND{=} 'elif' expr colcom objectPart)*
(IND{=} 'else' colcom objectPart)?
objectCase = 'case' identWithPragma ':' typeDesc ':'? COMMENT?
(IND{>} objectBranches DED
| IND{=} objectBranches)
objectPart = IND{>} objectPart^+IND{=} DED
/ objectWhen / objectCase / 'nil' / 'discard' / declColonEquals
object = 'object' pragma? ('of' typeDesc)? COMMENT? objectPart
typeClassParam = ('var' | 'out')? symbol
typeClass = typeClassParam ^* ',' (pragma)? ('of' typeDesc ^* ',')?
&IND{>} stmt
typeDef = identWithPragmaDot genericParamList? '=' optInd typeDefAux
indAndComment? / identVisDot genericParamList? pragma '=' optInd typeDefAux
indAndComment?
varTuple = '(' optInd identWithPragma ^+ comma optPar ')' '=' optInd expr
colonBody = colcom stmt postExprBlocks?
variable = (varTuple / identColonEquals) colonBody? indAndComment
constant = (varTuple / identWithPragma) (colon typeDesc)? '=' optInd expr indAndComment
bindStmt = 'bind' optInd qualifiedIdent ^+ comma
mixinStmt = 'mixin' optInd qualifiedIdent ^+ comma
pragmaStmt = pragma (':' COMMENT? stmt)?
simpleStmt = ((returnStmt | raiseStmt | yieldStmt | discardStmt | breakStmt
| continueStmt | pragmaStmt | importStmt | exportStmt | fromStmt
| includeStmt | commentStmt) / exprStmt) COMMENT?
complexOrSimpleStmt = (ifStmt | whenStmt | whileStmt
| tryStmt | forStmt
| blockStmt | staticStmt | deferStmt | asmStmt
| 'proc' routine
| 'method' routine
| 'func' routine
| 'iterator' routine
| 'macro' routine
| 'template' routine
| 'converter' routine
| 'type' section(typeDef)
| 'const' section(constant)
| ('let' | 'var' | 'using') section(variable)
| bindStmt | mixinStmt)
/ simpleStmt
stmt = (IND{>} complexOrSimpleStmt^+(IND{=} / ';') DED)
/ simpleStmt ^+ ';' Порядок вычисления
Порядок вычисления строго слева направо, внутри-наружу, как это типично для большинства других императивных языков программирования:
var s = "" proc p(arg: int): int = s.add $arg result = arg discard p(p(1) + p(2)) doAssert s == "123"
Операции присваивания не являются особыми, выражение левой части оценивается до выражения правой части:
var v = 0 proc getI(): int = result = v inc v var a, b: array[0..2, int] proc someCopy(a: var int; b: int) = a = b a[getI()] = getI() doAssert a == [1, 0, 0] v = 0 someCopy(b[getI()], getI()) doAssert b == [1, 0, 0]
Обоснование: Согласованность с перегруженными операциями присваивания или похожими на присваивание операциями, a = b может быть прочитано как performSomeCopy(a, b).
Однако понятие «порядка вычисления» применимо только после нормализации кода: Нормализация включает расширение шаблонов и переупорядочивание аргументов, которые были переданы именованным параметрам:
var s = "" proc p(): int = s.add "p" result = 5 proc q(): int = s.add "q" result = 3 # Evaluation order is 'b' before 'a' due to template # expansion's semantics. template swapArgs(a, b): untyped = b + a doAssert swapArgs(p() + q(), q() - p()) == 6 doAssert s == "qppq" # Evaluation order is not influenced by named parameters: proc construct(first, second: int) = discard # 'p' is evaluated before 'q'! construct(second = q(), first = p()) doAssert s == "qppqpq"
Обоснование: Это намного проще реализовать, чем гипотетические альтернативы.
Константы и константные выражения
Константа — это символ, связанный со значением константного выражения. Константные выражения ограничены зависимостью только от следующих категорий значений и операций, поскольку они либо встроенные в язык, либо объявлены и вычислены до семантического анализа константного выражения:
- литералы
- встроенные операторы
- предыдущие объявленные константы и переменные времени компиляции
- предыдущие объявленные макросы и шаблоны
- предыдущие объявленные процедуры, не имеющие побочных эффектов, кроме возможного изменения переменных времени компиляции
Константное выражение может содержать блоки кода, которые могут использовать все возможности Nim, поддерживаемые во время компиляции (как подробно описано в следующем разделе ниже). Внутри такого блока кода можно объявлять переменные, а затем читать и обновлять их или объявлять переменные и передавать их в процедуры, которые их изменяют. Однако код в таком блоке по-прежнему должен соответствовать перечисленным выше ограничениям для ссылки на значения и операции вне блока.
Возможность доступа и изменения переменных времени компиляции добавляет гибкость константным выражениям, что может быть неожиданным для тех, кто знаком с другими статически типизированными языками. Например, следующий код отражает начало ряда Фибоначчи во время компиляции. (Это демонстрация гибкости в определении констант, а не рекомендуемый стиль для решения этой проблемы!)
import strformat
var fib_n {.compileTime.}: int
var fib_prev {.compileTime.}: int
var fib_prev_prev {.compileTime.}: int
proc next_fib(): int =
result = if fib_n < 2:
fib_n
else:
fib_prev_prev + fib_prev
inc(fib_n)
fib_prev_prev = fib_prev
fib_prev = result
const f0 = next_fib()
const f1 = next_fib()
const display_fib = block:
const f2 = next_fib()
var result = fmt"Fibonacci sequence: {f0}, {f1}, {f2}"
for i in 3..12:
add(result, fmt", {next_fib()}")
result
static:
echo display_fib Ограничения на выполнение во время компиляции
Код Nim, который будет выполняться во время компиляции, не может использовать следующие языковые возможности:
- методы
- итераторы с замыканием
- оператор
cast - ссылки (указатели) типов
- FFI
Использование обёртки, использующей FFI и/или cast также запрещено. Обратите внимание, что это включает обёртки в стандартных библиотеках.
Некоторые или все эти ограничения, вероятно, будут сняты со временем.
Типы
У всех выражений есть тип, известный во время семантического анализа. Nim — статически типизированный язык. Можно объявлять новые типы, что по сути означает определение идентификатора, который можно использовать для обозначения этого пользовательского типа.
Это основные классы типов:
- порядковые типы (состоят из целочисленных, логических, символьных, перечислений (и поддиапазонов этих типов))
- типы с плавающей точкой
- строковый тип
- структурированные типы
- тип ссылок (указателей)
- процедурный тип
- обобщённый тип
Порядковые типы
Порядковые типы обладают следующими характеристиками:
- Порядковые типы являются счётными и упорядоченными. Это свойство позволяет определять операции функций, как
inc,ord,decна порядковых типах. - Порядковые значения имеют наименьшее возможное значение. Попытка посчитать дальше, чем наименьшее значение, вызывает ошибку или статическую ошибку.
- Порядковые значения имеют наибольшее возможное значение. Попытка посчитать дальше, чем наибольшее значение, вызывает ошибку или статическую ошибку.
Целые числа, логические значения, символы и типы перечислений (и поддиапазоны этих типов) относятся к порядковым типам. Из соображений простоты реализации типы uint и uint64 не являются порядковыми типами. (Это будет изменено в будущих версиях языка.)
Отличительный тип является порядковым типом, если его базовый тип является порядковым типом.
Предварительно определённые целочисленные типы
Эти целочисленные типы предварительно определены:
int- обобщённый тип целых чисел со знаком; его размер зависит от платформы и имеет тот же размер, что и указатель. Этот тип следует использовать в общем случае. Целочисленный литерал без суффикса типа является типом, если он находится в диапазоне
low(int32)..high(int32), в противном случае тип литералаint64. - intXX
- дополнительные типы целых чисел со знаком по XX бит используют эту схему именования (например, int16 — целое число шириной 16 бит). Текущая реализация поддерживает
int8,int16,int32,int64. Литералы этих типов имеют суффикс 'iXX. uint- обобщённый тип беззнаковых целых чисел; его размер зависит от платформы и имеет тот же размер, что и указатель. Целочисленный литерал с суффиксом типа
'uимеет этот тип. - uintXX
- дополнительные типы беззнаковых целых чисел по XX бит используют эту схему именования (например, uint16 — беззнаковое целое число шириной 16 бит). Текущая реализация поддерживает
uint8,uint16,uint32,uint64. Литералы этих типов имеют суффикс 'uXX. Все беззнаковые операции циклически повторяются; они не могут привести к переполнению или недополнению.
Помимо обычных арифметических операторов для целых чисел со знаком и без знака (+ - * и т.д.) также существуют операторы, которые формально работают с целыми числами со знаком, но обрабатывают свои аргументы как целые числа без знака: они в основном предоставляются для обратной совместимости со старыми версиями языка, в которых отсутствовали типы целых чисел без знака. Эти операции без знака для целых чисел со знаком используют суффикс % в качестве соглашения:
| операция | значение |
|---|---|
a +% b |
сложение целых чисел без знака |
a -% b |
вычитание целых чисел без знака |
a *% b |
умножение целых чисел без знака |
a /% b |
деление целых чисел без знака |
a %% b |
операция modulo целых чисел без знака |
a <% b |
рассматривать a и b как без знака и сравнивать |
a <=% b |
рассматривать a и b как без знака и сравнивать |
ze(a) |
расширяет биты a нулями до ширины типа int |
toU8(a) |
рассматривает a как без знака и преобразует его в целое число без знака 8 бит (но по-прежнему типа int8) |
toU16(a) |
рассматривает a как без знака и преобразует его в целое число без знака 16 бит (но по-прежнему типа int16) |
toU32(a) |
рассматривает a как без знака и преобразует его в целое число без знака 32 бит (но по-прежнему типа int32) |
Автоматическое преобразование типов выполняется в выражениях, где используются различные типы целых чисел: меньший тип преобразуется в больший.
A Сужение типа преобразует больший тип в меньший (например, int32 -> int16. A Расширение типа преобразует меньший тип в больший (например, int16 -> int32). В Nim только расширения типов являются *явными*:
var myInt16 = 5i16 var myInt: int myInt16 + 34 # of type ``int16`` myInt16 + myInt # of type ``int`` myInt16 + 2i32 # of type ``int32``
Однако, литералы int неявно преобразуются в меньший тип целых чисел, если значение литерала подходит для этого меньшего типа, и такое преобразование менее затратно, чем другие неявные преобразования, поэтому myInt16 + 34 производит результат int16.
Для получения дополнительной информации см. Операцию преобразования типов.
Типы поддиапазонов
Тип поддиапазона — это диапазон значений из порядкового или плавающего типа (базовый тип). Для определения типа поддиапазона необходимо указать его граничные значения — наименьшее и наибольшее значение типа. Например:
type Subrange = range[0..5] PositiveFloat = range[0.0..Inf]
Subrange — это поддиапазон целого числа, которое может хранить только значения от 0 до 5. PositiveFloat определяет поддиапазон всех положительных значений с плавающей запятой. NaN не принадлежит ни одному поддиапазону типов с плавающей запятой. Присвоение любого другого значения переменной типа Subrange — это паника (или статическая ошибка, если это можно определить во время семантического анализа). Допускаются присваивания из базового типа одному из его типов поддиапазона (и наоборот).
Тип поддиапазона имеет тот же размер, что и его базовый тип (int в примере поддиапазона).
Предварительно определенные типы с плавающей запятой
Следующие типы с плавающей запятой определены предварительно:
float- обобщенный тип с плавающей запятой; его размер использовался платформозависимый, но теперь он всегда сопоставляется с
float64. Этот тип следует использовать в общем случае. - floatXX
- реализация может определить дополнительные типы с плавающей запятой XX бит, используя эту схему именования (например, float64 — это 64-битный float). Текущая реализация поддерживает
float32иfloat64. Литералы этих типов имеют суффикс 'fXX.
Автоматическое преобразование типов в выражениях с различными типами с плавающей запятой выполняется: см. Операцию преобразования типов для получения дополнительной информации. Арифметические операции с типами с плавающей запятой следуют стандарту IEEE. Типы целых чисел не преобразуются в типы с плавающей запятой автоматически и наоборот.
Стандарт IEEE определяет пять типов исключений с плавающей запятой:
- Недействительный: операции с математически недействительными операндами, например 0.0/0.0, sqrt(-1.0) и log(-37.8).
- Деление на ноль: делитель равен нулю, а делимое — конечное ненулевое число, например 1.0/0.0.
- Переполнение: операция производит результат, превышающий диапазон показателя, например MAXDOUBLE+0.0000000000001e308.
- Подпотолк: операция производит результат, который слишком мал, чтобы быть представлен как обычное число, например MINDOUBLE * MINDOUBLE.
- Неточный: операция производит результат, который не может быть представлен с бесконечной точностью, например 2.0 / 3.0, log(1.1) и 0.1 на входе.
Исключения IEEE либо игнорируются во время выполнения, либо отображаются на исключения Nim: FloatInvalidOpDefect, FloatDivByZeroDefect, FloatOverflowDefect, FloatUnderflowDefect и FloatInexactDefect. Эти исключения наследуются от базового класса FloatingPointDefect.
Nim предоставляет директивы nanChecks и infChecks для управления тем, игнорируются ли исключения IEEE или генерируется исключение Nim:
{.nanChecks: on, infChecks: on.}
var a = 1.0
var b = 0.0
echo b / b # raises FloatInvalidOpDefect
echo a / b # raises FloatOverflowDefect
В текущей реализации FloatDivByZeroDefect и FloatInexactDefect никогда не поднимаются. FloatOverflowDefect поднимается вместо FloatDivByZeroDefect. Также существует директива floatChecks, которая является сокращенным вариантом сочетания директив nanChecks и infChecks. floatChecks отключены по умолчанию.
Единственные операции, на которые влияет директива floatChecks, это операторы +, -, *, / для типов с плавающей запятой.
Реализация всегда должна использовать максимальную доступную точность для оценки значений с плавающей точкой во время семантического анализа; это означает, что выражения, такие как 0.09'f32 + 0.01'f32 == 0.09'f64 + 0.01'f64, которые вычисляются во время константной сворачивания, верны.
Тип boolean
Тип boolean называется bool в Nim и может иметь одно из двух предварительно определенных значений true и false. Условия в while, if, elif, when-высказываниях должны иметь тип bool.
Это условие выполняется:
ord(false) == 0 and ord(true) == 1
Операторы not, and, or, xor, <, <=, >, >=, !=, == определены для типа bool. Операторы and и or выполняют краткое вычисление. Пример:
while p != nil and p.name != "xyz": # p.name is not evaluated if p == nil p = p.next
Размер типа bool составляет один байт.
Тип character
Тип character называется char в Nim. Его размер составляет один байт. Таким образом, он не может представить символ UTF-8, а только его часть. Причина этого — эффективность: в подавляющем большинстве случаев результирующие программы по-прежнему будут правильно обрабатывать UTF-8, так как UTF-8 был специально разработан для этого. Еще одна причина заключается в том, что Nim может эффективно поддерживать array[char, int] или set[char], так как многие алгоритмы полагаются на эту функцию. Тип Rune используется для символов Unicode, он может представлять любой символ Unicode. Rune объявлен в модуле unicode.
Типы перечислений
Типы перечислений определяют новый тип, значения которого состоят из указанных значений. Значения упорядочены. Пример:
type
Direction = enum
north, east, south, west
Теперь выполняется следующее:
ord(north) == 0 ord(east) == 1 ord(south) == 2 ord(west) == 3 # Also allowed: ord(Direction.west) == 3
Таким образом, north < east < south < west. Операторы сравнения могут использоваться с типами перечислений. Вместо north и т.д., значение перечисления также может быть квалифицировано типом перечисления, в котором оно находится, Direction.north.
Для лучшего взаимодействия с другими языками программирования поля типов перечислений можно назначить явное порядковое значение. Однако порядковые значения должны быть в порядке возрастания. Поле, которому не присвоено явное порядковое значение, получает значение предыдущего поля + 1.
Явно упорядоченное перечисление может иметь *дыры*:
type
TokenType = enum
a = 2, b = 4, c = 89 # holes are valid
Однако в этом случае оно больше не является порядковым, поэтому эти перечисления нельзя использовать как тип индекса для массивов. Процедуры inc, dec, succ и pred также недоступны для них.
Компилятор поддерживает встроенный оператор строкового представления $ для перечислений. Результат строкового представления может контролироваться путем явного задания значений строк для использования:
type
MyEnum = enum
valueA = (0, "my value A"),
valueB = "value B",
valueC = 2,
valueD = (3, "abc")
Как видно из примера, можно указать как порядковое значение поля, так и его строковое значение, используя кортеж. Также можно указать только одно из них.
Перечисление может быть помечено директивой pure таким образом, что его поля добавляются в специальный скрытый модульный область видимости, который запрашивается только в качестве последней попытки. В этот область видимости добавляются только недвусмысленные символы. Но к ним всегда можно получить доступ с помощью квалификации типа, записанной как MyEnum.value:
type
MyEnum {.pure.} = enum
valueA, valueB, valueC, valueD, amb
OtherEnum {.pure.} = enum
valueX, valueY, valueZ, amb
echo valueA # MyEnum.valueA
echo amb # Error: Unclear whether it's MyEnum.amb or OtherEnum.amb
echo MyEnum.amb # OK.
Для реализации битовых полей с перечислениями см. Битовые поля
Тип string
Все строковые литералы имеют тип string. Строка в Nim очень похожа на последовательность символов. Однако строки в Nim имеют нулевое завершение и поле длины. Длину можно получить с помощью встроенной процедуры len; длина никогда не учитывает завершающий ноль.
Завершающий ноль нельзя получить, если строка не преобразована в тип cstring в первую очередь. Завершающий ноль гарантирует, что это преобразование можно выполнить за O(1) и без выделения памяти.
Оператор присваивания для строк всегда копирует строку. Оператор & конкатенирует строки.
Большинство собственных типов Nim поддерживают преобразование в строки с помощью специальной процедуры $. При вызове процедуры echo, например, вызывается встроенная операция строкового представления для параметра:
echo 3 # calls `$` for `int`
Всякий раз, когда пользователь создает специализированный объект, реализация этой процедуры обеспечивает представление string.
type
Person = object
name: string
age: int
proc `$`(p: Person): string = # `$` always returns a string
result = p.name & " is " &
$p.age & # we *need* the `$` in front of p.age which
# is natively an integer to convert it to
# a string
" years old."
Хотя $p.name также можно использовать, операция $ над строкой ничего не делает. Обратите внимание, что мы не можем полагаться на автоматическое преобразование из int в string, как это возможно для процедуры echo.
Строки сравниваются по лексикографическому порядку. Все операторы сравнения доступны. Строки могут быть индексированы как массивы (нижняя граница — 0). В отличие от массивов, они могут использоваться в операторах выбора:
case paramStr(i) of "-v": incl(options, optVerbose) of "-h", "-?": incl(options, optHelp) else: write(stdout, "invalid command line option!\n")
По соглашению, все строки являются строками UTF-8, но это не навязывается. Например, при чтении строк из двоичных файлов они представляют собой просто последовательность байтов. Операция индексирования s[i] означает i-й символ строки s, а не i-й символ Юникода. Итератор runes из модуля unicode может использоваться для итерации по всем символам Юникода.
Тип cstring
Тип cstring, означающий compatible string, является родным представлением строки для бэкенда компиляции. Для бэкенда C тип cstring представляет собой указатель на массив символов с нулевым завершением, совместимый с типом char* в Ansi C. Его основная цель заключается в простом взаимодействии с C. Операция индексирования s[i] означает i-й символ строки s; однако проверка границ для cstring не выполняется, делая операцию индексирования небезопасной.
Nim-строка string неявно преобразуется в cstring для удобства. Если Nim-строка передаётся в процедуру с переменным числом аргументов C-стиля, она также неявно преобразуется в cstring.
proc printf(formatstr: cstring) {.importc: "printf", varargs,
header: "<stdio.h>".}
printf("This works %s", "as expected")
Несмотря на то, что преобразование неявное, оно не является безопасным: сборщик мусора не рассматривает cstring как корень и может собрать базовую память. Однако на практике это происходит крайне редко, так как сборщик мусора консервативно рассматривает корни стека. Для редких случаев, когда это не работает, можно использовать встроенные процедуры GC_ref и GC_unref для сохранения данных строки.
Для cstring определена процедура $, возвращающая строку. Таким образом, чтобы получить Nim-строку из cstring:
var str: string = "Hello!" var cstr: cstring = str var newstr: string = $cstr
Структурированные типы
Переменная структурированного типа может хранить несколько значений одновременно. Структурированные типы могут быть вложены на неограниченные уровни. К структурированным типам относятся массивы, последовательности, кортежи, объекты и множества.
Типы массивов и последовательностей
Массивы — это однородный тип, то есть каждый элемент в массиве имеет один и тот же тип. Массивы всегда имеют фиксированную длину, заданную как константное выражение (за исключением открытых массивов). Их можно индексировать любым порядковым типом. Параметр A может быть открытым массивом, в этом случае он индексируется целыми числами от 0 до len(A)-1. Выражение массива может быть сконструировано с помощью конструктора массива []. Тип элемента этого массива определяется по типу первого элемента. Все остальные элементы должны быть неявно преобразуемы к этому типу.
Тип массива можно определить, используя синтаксис array[size, T], или array[lo..hi, T] для массивов, начинающихся с индекса, отличного от нуля.
Последовательности похожи на массивы, но имеют динамическую длину, которая может изменяться во время выполнения (как строки). Последовательности реализованы как расширяемые массивы, выделяя блоки памяти по мере добавления элементов. Последовательность S всегда индексируется целыми числами от 0 до len(S)-1, и ее границы проверяются. Последовательности могут быть созданы с помощью конструктора массива [] в сочетании с оператором преобразования массива в последовательность @. Другой способ выделить память для последовательности — вызвать встроенную процедуру newSeq.
Последовательность можно передать в параметр типа открытый массив.
Пример:
type IntArray = array[0..5, int] # an array that is indexed with 0..5 IntSeq = seq[int] # a sequence of integers var x: IntArray y: IntSeq x = [1, 2, 3, 4, 5, 6] # [] is the array constructor y = @[1, 2, 3, 4, 5, 6] # the @ turns the array into a sequence let z = [1.0, 2, 3, 4] # the type of z is array[0..3, float]
Нижняя граница массива или последовательности может быть получена с помощью встроенной процедуры low(), верхняя граница — с помощью high(). Длина может быть получена с помощью len(). low() для последовательности или открытого массива всегда возвращает 0, так как это первый допустимый индекс. Элементы можно добавлять в последовательность с помощью процедуры add() или оператора &, а последний элемент последовательности можно удалить (и получить) с помощью процедуры pop().
Запись x[i] используется для доступа к i-му элементу последовательности x.
Массивы всегда проверяют границы (статически или во время выполнения). Эти проверки можно отключить с помощью директив или вызвав компилятор со флагом --boundChecks:off.
Конструктор массива может иметь явные индексы для читабельности:
type
Values = enum
valA, valB, valC
const
lookupTable = [
valA: "A",
valB: "B",
valC: "C"
]
Если индекс опущен, succ(lastIndex) используется в качестве значения индекса:
type
Values = enum
valA, valB, valC, valD, valE
const
lookupTable = [
valA: "A",
"B",
valC: "C",
"D", "e"
] Открытые массивы
Часто массивы с фиксированным размером оказываются слишком жёсткими; процедуры должны уметь обрабатывать массивы разных размеров. Тип openarray позволяет это; он может использоваться только для параметров. Открытые массивы всегда индексируются целым числом int, начиная с позиции 0. Для открытых массивов также доступны операции len, low и high. Любой массив с совместимым базовым типом может быть передан в параметр openarray, тип индекса не имеет значения. Кроме массивов, в параметр openarray также могут быть переданы последовательности.
Тип openarray не может быть вложен: многомерные openarray не поддерживаются, так как это редко необходимо и не может быть сделано эффективно.
proc testOpenArray(x: openArray[int]) = echo repr(x) testOpenArray([1,2,3]) # array[] testOpenArray(@[1,2,3]) # seq[]
Varargs
Параметр varargs — это параметр openarray, который дополнительно позволяет передавать переменное число аргументов в процедуру. Компилятор неявно преобразует список аргументов в массив:
proc myWriteln(f: File, a: varargs[string]) =
for s in items(a):
write(f, s)
write(f, "\n")
myWriteln(stdout, "abc", "def", "xyz")
# is transformed to:
myWriteln(stdout, ["abc", "def", "xyz"])
Это преобразование выполняется только в том случае, если параметр varargs является последним параметром в заголовке процедуры. Также возможно выполнение преобразований типов в этом контексте:
proc myWriteln(f: File, a: varargs[string, `$`]) =
for s in items(a):
write(f, s)
write(f, "\n")
myWriteln(stdout, 123, "abc", 4.0)
# is transformed to:
myWriteln(stdout, [$123, $"def", $4.0])
В этом примере $ применяется к любому аргументу, переданному параметру a. (Обратите внимание, что $ применённый к строкам, является nop.)
Обратите внимание, что явный конструктор массива, переданный параметру varargs, не заключён в другой неявный конструктор массива:
proc takeV[T](a: varargs[T]) = discard takeV([123, 2, 1]) # takeV's T is "int", not "array of int"
varargs[typed] обрабатывается особым образом: он соответствует переменному списку аргументов произвольного типа, но всегда строит неявный массив. Это необходимо для того, чтобы встроенная процедура echo работала как ожидается:
proc echo*(x: varargs[typed, `$`]) {...}
echo @[1, 2, 3]
# prints "@[1, 2, 3]" and not "123" Непроверенные массивы
Тип UncheckedArray[T] — это особый вид array, в котором не проверяются границы. Это часто бывает полезно для реализации настраиваемых массивов с гибким размером. Кроме того, неиспользуемый массив преобразуется в массив C неопределённого размера:
type
MySeq = object
len, cap: int
data: UncheckedArray[int]
Производит примерно такой код C:
typedef struct {
NI len;
NI cap;
NI data[];
} MySeq;
Базовый тип неиспользуемого массива не может содержать памяти, управляемой сборщиком мусора, но это в настоящее время не проверяется.
Будущие направления: в неиспользуемых массивах должна быть разрешена память, управляемая сборщиком мусора, и должно быть явное аннотирование, как сборщик мусора должен определить размер массива во время выполнения.
Кортежи и типы объектов
Переменная типа кортежа или объекта — это контейнер разнородных данных. Кортеж или объект определяет различные именованные поля типа. Кортеж также определяет лексикографический порядок полей. Кортежи предназначены для хранения разнородных данных с небольшим количеством абстракций. Синтаксис () может использоваться для построения кортежей. Порядок полей в конструкторе должен соответствовать порядку в определении кортежа. Разные типы кортежей эквивалентны, если они указывают одни и те же поля одного и того же типа в том же порядке. Имена полей также должны быть идентичны.
Оператор присваивания для кортежей копирует каждый компонент. Стандартный оператор присваивания для объектов копирует каждый компонент. Перегрузка оператора присваивания описана здесь.
type
Person = tuple[name: string, age: int] # type representing a person:
# a person consists of a name
# and an age
var
person: Person
person = (name: "Peter", age: 30)
echo person.name
# the same, but less readable:
person = ("Peter", 30)
echo person[0]
Кортеж с одним безымянным полем можно создать с помощью скобок и запятой:
proc echoUnaryTuple(a: (int,)) = echo a[0] echoUnaryTuple (1,)
На самом деле, запятая разрешена для любого построения кортежа.
Реализация выравнивает поля для наилучшей производительности доступа. Выравнивание совместимо с тем, как это делает компилятор C.
Для согласованности с объявлениями object, кортежи в разделе type также могут быть определены с отступами вместо []:
type
Person = tuple # type representing a person
name: string # a person consists of a name
age: Natural # and an age
Объекты предоставляют множество функций, которых нет у кортежей. Объекты обеспечивают наследование и возможность скрытия полей от других модулей. Объекты с включённым наследованием имеют информацию о своём типе во время выполнения, так что оператор of можно использовать для определения типа объекта. Оператор of похож на оператор instanceof в Java.
type
Person = object of RootObj
name*: string # the * means that `name` is accessible from other modules
age: int # no * means that the field is hidden
Student = ref object of Person # a student is a person
id: int # with an id field
var
student: Student
person: Person
assert(student of Student) # is true
assert(student of Person) # also true
Поля объекта, которые должны быть видимы извне определяющего модуля, должны быть помечены с помощью * . В отличие от кортежей, разные типы объектов никогда не эквивалентны, они являются именованными типами, тогда как кортежи — структурными. Объекты, у которых нет предка, неявно final и, следовательно, не имеют скрытой информации о типе. С помощью директивы inheritable можно вводить новые корни объекта, помимо system.RootObj.
type
Person = object # example of a final object
name*: string
age: int
Student = ref object of Person # Error: inheritance only works with non-final objects
id: int Создание объектов
Объекты также можно создавать с помощью выражения создания объекта, имеющего синтаксис T(fieldA: valueA, fieldB: valueB, ...), где T — тип object или тип ref object:
var student = Student(name: "Anton", age: 5, id: 3)
Обратите внимание, что, в отличие от кортежей, объекты требуют указания имён полей вместе со значениями. Для типа ref object функция system.new вызывается неявно.
Варианты объектов
Часто иерархия объектов является излишней в определенных ситуациях, когда нужны простые типы вариантов. Объектные варианты — это помеченные объединения, различаемые с помощью перечисления, используемого для гибкости типа во время выполнения, отражающие концепции типов суммы и алгебраических типов данных (ADT), как и в других языках.
Пример:
# This is an example how an abstract syntax tree could be modelled in Nim
type
NodeKind = enum # the different node types
nkInt, # a leaf with an integer value
nkFloat, # a leaf with a float value
nkString, # a leaf with a string value
nkAdd, # an addition
nkSub, # a subtraction
nkIf # an if statement
Node = ref NodeObj
NodeObj = object
case kind: NodeKind # the ``kind`` field is the discriminator
of nkInt: intVal: int
of nkFloat: floatVal: float
of nkString: strVal: string
of nkAdd, nkSub:
leftOp, rightOp: Node
of nkIf:
condition, thenPart, elsePart: Node
# create a new case object:
var n = Node(kind: nkIf, condition: nil)
# accessing n.thenPart is valid because the ``nkIf`` branch is active:
n.thenPart = Node(kind: nkFloat, floatVal: 2.0)
# the following statement raises an `FieldDefect` exception, because
# n.kind's value does not fit and the ``nkString`` branch is not active:
n.strVal = ""
# invalid: would change the active object branch:
n.kind = nkInt
var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
rightOp: Node(kind: nkInt, intVal: 2))
# valid: does not change the active object branch:
x.kind = nkSub
Как видно из примера, преимущество иерархии объектов состоит в том, что не нужно производить преобразование между различными типами объектов. Тем не менее, доступ к недопустимым полям объекта приводит к исключению.
Синтаксис case в объявлении объекта тесно следует за синтаксисом инструкции case: ветви в разделе case также могут быть отступом.
В примере поле kind называется дискриминатором: для безопасности его адрес нельзя получить, а присваивания ему ограничены: новое значение не должно приводить к изменению ветви активного объекта. Также, когда поля конкретной ветви задаются во время создания объекта, соответствующее значение дискриминатора должно быть задано как константное выражение.
Вместо изменения активной ветви объекта замените старый объект в памяти новым целиком:
var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
rightOp: Node(kind: nkInt, intVal: 2))
# change the node's contents:
x[] = NodeObj(kind: nkString, strVal: "abc")
Начиная с версии 0.20 system.reset больше нельзя использовать для поддержки изменений ветвей объектов, так как это никогда не было полностью безопасным в отношении памяти.
Как специальное правило, тип дискриминатора также может быть ограничен с помощью инструкции case. Если возможные значения переменной дискриминатора в инструкции case являются подмножеством значений дискриминатора для выбранной ветви объекта, инициализация считается допустимой. Этот анализ работает только для неизменяемых дискриминаторов порядкового типа и игнорирует elif ветви. Для значений дискриминатора с типом range компилятор проверяет, соответствует ли весь диапазон возможных значений для значения дискриминатора выбранной ветви объекта.
Небольшой пример:
let unknownKind = nkSub # invalid: unsafe initialization because the kind field is not statically known: var y = Node(kind: unknownKind, strVal: "y") var z = Node() case unknownKind of nkAdd, nkSub: # valid: possible values of this branch are a subset of nkAdd/nkSub object branch: z = Node(kind: unknownKind, leftOp: Node(), rightOp: Node()) else: echo "ignoring: ", unknownKind # also valid, since unknownKindBounded can only contain the values nkAdd or nkSub let unknownKindBounded = range[nkAdd..nkSub](unknownKind) z = Node(kind: unknownKindBounded, leftOp: Node(), rightOp: Node())
Тип множества
Тип множества моделирует математическое понятие множества. Базовый тип множества может быть только порядковым типом определенного размера, а именно:-
int8-int16 -
uint8/byte-uint16 charenum
или эквивалентный. Для знакомых целых чисел базовый тип множества определяется в диапазоне 0 .. MaxSetElements-1, где MaxSetElements в настоящее время всегда равно 2^16.
Причина в том, что множества реализованы как высокопроизводительные битовые векторы. Попытка объявить множество с типом большего размера приведет к ошибке:
var s: set[int64] # Error: set is too large
Множества можно создать с помощью конструктора множества: {} — это пустое множество. Пустое множество совместимо по типу с любым конкретным типом множества. Конструктор также можно использовать для включения элементов (и диапазонов элементов):
type
CharSet = set[char]
var
x: CharSet
x = {'a'..'z', '0'..'9'} # This constructs a set that contains the
# letters from 'a' to 'z' and the digits
# from '0' to '9'
Эти операции поддерживаются множествами:
| операция | значение |
|---|---|
A + B |
объединение двух множеств |
A * B |
пересечение двух множеств |
A - B |
разность двух множеств (A без элементов B) |
A == B |
равенство множеств |
A <= B |
отношение подмножества (A является подмножеством B или равно B) |
A < B |
строгое отношение подмножества (A является собственным подмножеством B) |
e in A |
принадлежность к множеству (A содержит элемент e) |
e notin A |
A не содержит элемент e |
contains(A, e) |
A содержит элемент e |
card(A) |
мощность A (количество элементов в A) |
incl(A, elem) |
то же, что и A = A + {elem}
|
excl(A, elem) |
то же, что и A = A - {elem}
|
Битовые поля
Множества часто используются для определения типа флагов процедуры. Это более чистое (и безопасное по типу) решение, чем определение целочисленных констант, которые должны быть or объединены.
Перечисление, множества и приведение типов могут использоваться вместе, как в:
type
MyFlag* {.size: sizeof(cint).} = enum
A
B
C
D
MyFlags = set[MyFlag]
proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)
assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}
Обратите внимание, как множество преобразует значения перечисления в степени 2.
Если использовать перечисления и множества с C, используйте distinct cint.
Для взаимодействия с C см. также битовую директиву bitsize.
Ссылки и указатели
Ссылки (похожие на указатели в других языках программирования) — это способ введения многих-ко-одному отношений. Это означает, что разные ссылки могут указывать на одно и то же место в памяти и изменять его (также называемое алиасингом).
Nim различает отслеживаемые и неотслеживаемые ссылки. Неотслеживаемые ссылки также называются указателями. Отслеживаемые ссылки указывают на объекты на управляемой куче, неотслеживаемые ссылки указывают на объекты, выделенные вручную, или объекты где-то еще в памяти. Таким образом, неотслеживаемые ссылки являются небезопасными. Однако для некоторых операций низкого уровня (доступ к оборудованию) неотслеживаемые ссылки неизбежны.
Отслеживаемые ссылки объявляются с ключевым словом ref, неотслеживаемые ссылки объявляются с ключевым словом ptr. В общем случае ptr T неявно преобразуется в тип pointer.
Пустой индекс [] используется для разыменования ссылки, процедура addr возвращает адрес элемента. Адрес всегда является неотслеживаемой ссылкой. Таким образом, использование addr является небезопасной функцией.
Операторы . (доступ к полю кортежа/объекта) и [] (индекс массива/строки/последовательности) выполняют неявное разыменование для типов ссылок:
type
Node = ref NodeObj
NodeObj = object
le, ri: Node
data: int
var
n: Node
new(n)
n.data = 9
# no need to write n[].data; in fact n[].data is highly discouraged!
Автоматическое разыменование может быть выполнено для первого аргумента вызова процедуры, но это экспериментальная функция, и она описана здесь.
Для упрощения проверки типов рекурсивные кортежи недействительны:
# invalid recursion type MyTuple = tuple[a: ref MyTuple]
Аналогично, T = ref T — это недопустимый тип.
В качестве синтаксической расширения типы object могут быть анонимными, если объявлены в разделе типов с помощью обозначений ref object или ptr object. Эта функция полезна, если объект должен получить только семантику ссылки:
type
Node = ref object
le, ri: Node
data: int
Для выделения нового отслеживаемого объекта должна использоваться встроенная процедура new. Для работы с неотслеживаемой памятью можно использовать процедуры alloc, dealloc и realloc. Дополнительную информацию можно найти в документации модуля системы.
Nil ---
Если ссылка указывает на ничто, она имеет значение nil. nil — это значение по умолчанию для всех типов ref и ptr. Значение nil также можно использовать как любое другое литеральное значение. Например, его можно использовать в присваивании, как myRef = nil.
Разыменование nil — это необратимая фатальная ошибка во время выполнения (а не паника).
Успешная операция разыменования p[] подразумевает, что p не равно nil. Это может быть использовано реализацией для оптимизации кода, такого как:
p[].field = 3 if p != nil: # if p were nil, ``p[]`` would have caused a crash already, # so we know ``p`` is always not nil here. action()
В:
p[].field = 3 action()
Примечание: Это не сравнимо с "неопределенным поведением" C при разыменовании указателей NULL.
Смешивание памяти с управляемым сборщиком мусора с ptr
Необходимо проявлять особую осторожность, если неотслеживаемый объект содержит отслеживаемые объекты, такие как отслеживаемые ссылки, строки или последовательности: для правильного освобождения всего необходимо вызвать встроенную процедуру reset перед ручным освобождением неотслеживаемой памяти:
type Data = tuple[x, y: int, s: string] # allocate memory for Data on the heap: var d = cast[ptr Data](alloc0(sizeof(Data))) # create a new string on the garbage collected heap: d.s = "abc" # tell the GC that the string is not needed anymore: reset(d.s) # free the memory: dealloc(d)
Без вызова reset память, выделенная для строки d.s, никогда не будет освобождена. Пример также демонстрирует две важные особенности для программирования на низком уровне: процедура sizeof возвращает размер типа или значения в байтах. Оператор cast может обойти систему типов: компилятор вынужден рассматривать результат вызова alloc0 (который возвращает нетипизированный указатель) как если бы у него был тип ptr Data. Приведение типов следует применять только в случае крайней необходимости: оно нарушает безопасность типов, и ошибки могут привести к таинственным сбоям.
Примечание: Пример работает только потому, что память инициализируется нулями (alloc0 вместо alloc делает это): d.s таким образом инициализируется двоичным нулём, что строковое присваивание может обработать. Необходимо знать подробности низкого уровня, подобные этому, при смешивании данных, управляемых сборщиком мусора, с неуправляемой памятью.
Процедурный тип
Процедурный тип — это указатель на процедуру. nil является допустимым значением для переменных процедурного типа. Nim использует процедурные типы для достижения функционального программирования.
Примеры:
proc printItem(x: int) = ...
proc forEach(c: proc (x: int) {.cdecl.}) =
...
forEach(printItem) # this will NOT compile because calling conventions differ
type
OnMouseMove = proc (x, y: int) {.closure.}
proc onMouseMove(mouseX, mouseY: int) =
# has default calling convention
echo "x: ", mouseX, " y: ", mouseY
proc setOnMouseMove(mouseMoveEvent: OnMouseMove) = discard
# ok, 'onMouseMove' has the default calling convention, which is compatible
# to 'closure':
setOnMouseMove(onMouseMove)
Тонкая проблема с процедурными типами заключается в том, что соглашение о вызове процедуры влияет на совместимость типов: процедурные типы совместимы только в том случае, если они имеют одинаковое соглашение о вызове. В качестве специального расширения процедура со соглашением о вызове nimcall может быть передана в параметр, который ожидает процедуру с соглашением о вызове closure.
Nim поддерживает эти соглашения о вызове:
- nimcall
- является стандартной конвенцией, используемой для Nim proc. Она эквивалентна
fastcall, но только для компиляторов C, поддерживающихfastcall. - closure
- является стандартной конвенцией вызова для процедурного типа, который не имеет никаких аннотаций pragma. Это указывает на то, что процедура имеет скрытый неявный параметр (среду). Переменные Proc, имеющие конвенцию вызова
closure, занимают два машинных слова: одно для указателя proc и другое для указателя на неявную переданную среду. - stdcall
- Это конвенция stdcall, как определена Microsoft. Сгенерированная C-процедура объявляется с ключевым словом
__stdcall. - cdecl
- Конвенция cdecl означает, что процедура должна использовать ту же конвенцию, что и компилятор C. В Windows сгенерированная C-процедура объявляется с ключевым словом
__cdecl. - safecall
- Это конвенция safecall, как определена Microsoft. Сгенерированная C-процедура объявляется с ключевым словом
__safecall. Слово safe относится к тому, что все регистры процессора должны быть помещены в стеk. - inline
- Конвенция inline означает, что вызывающая сторона не должна вызывать процедуру, а должна встроить её код напрямую. Обратите внимание, что Nim не встраивает, а оставляет это компилятору C; он генерирует процедуры
__inline. Это лишь подсказка для компилятора: он может её полностью игнорировать и может встраивать процедуры, которые не помечены какinline. - fastcall
- Fastcall означает разные вещи для разных компиляторов C. Получается то, что означает C
__fastcall. - thiscall
- Это конвенция вызова thiscall, как определена Microsoft, используемая для функций-членов класса C++ на архитектуре x86
- syscall
- Конвенция syscall такая же, как
__syscallв C. Она используется для прерываний. - noconv
- Сгенерированный C-код не будет иметь явной конвенции вызова и, следовательно, будет использовать стандартную конвенцию вызова компилятора C. Это необходимо, поскольку стандартная конвенция вызова процедур Nim —
fastcallдля повышения скорости.
Большинство конвенций вызова существуют только для 32-разрядной платформы Windows.
Стандартная конвенция вызова — nimcall, если это не внутренняя процедура (процедура внутри процедуры). Для внутренней процедуры выполняется анализ, обращается ли она к своей среде. Если это так, то у неё конвенция вызова closure, в противном случае — nimcall.
Тип distinct
Тип distinct — это новый тип, полученный из основного типа, который несовместим со своим основным типом. В частности, существенным свойством типа distinct является то, что он не подразумевает отношение подтипа между ним и его основным типом. Явные преобразования типов от типа distinct к его основному типу и наоборот разрешены. Смотрите также distinctBase для получения обратной операции.
Тип distinct является порядковым типом, если его основной тип является порядковым типом.
Моделирование валют
Тип distinct можно использовать для моделирования различных физических единиц с численным основным типом, например. В следующем примере показано моделирование валют.
Разные валюты не должны смешиваться в денежных расчётах. Типы distinct — идеальный инструмент для моделирования различных валют:
type Dollar = distinct int Euro = distinct int var d: Dollar e: Euro echo d + 12 # Error: cannot add a number with no unit and a ``Dollar``
К сожалению, d + 12.Dollar также не разрешено, потому что + определено для int (и др.), а не для Dollar. Поэтому для долларов нужно определить +.
proc `+` (x, y: Dollar): Dollar = result = Dollar(int(x) + int(y))
Не имеет смысла умножать доллар на доллар, а не на число без единицы; и то же самое касается деления:
proc `*` (x: Dollar, y: int): Dollar = result = Dollar(int(x) * y) proc `*` (x: int, y: Dollar): Dollar = result = Dollar(x * int(y)) proc `div` ...
Это быстро надоедает. Реализации тривиальны, и компилятор не должен генерировать весь этот код только для того, чтобы оптимизировать его позже — в конце концов, + для долларов должен генерировать тот же двоичный код, что и + для целых чисел. Пragma borrow был разработан для решения этой проблемы; в принципе, он генерирует вышеперечисленные тривиальные реализации:
proc `*` (x: Dollar, y: int): Dollar {.borrow.}
proc `*` (x: int, y: Dollar): Dollar {.borrow.}
proc `div` (x: Dollar, y: int): Dollar {.borrow.}
Пragma borrow заставляет компилятор использовать ту же реализацию, что и процедура, которая работает с базовым типом типа distinct, поэтому код не генерируется.
Но кажется, что весь этот шаблонный код нужно повторять для Euro валюты. Это можно решить с помощью шаблонов.
template additive(typ: typedesc) =
proc `+` *(x, y: typ): typ {.borrow.}
proc `-` *(x, y: typ): typ {.borrow.}
# unary operators:
proc `+` *(x: typ): typ {.borrow.}
proc `-` *(x: typ): typ {.borrow.}
template multiplicative(typ, base: typedesc) =
proc `*` *(x: typ, y: base): typ {.borrow.}
proc `*` *(x: base, y: typ): typ {.borrow.}
proc `div` *(x: typ, y: base): typ {.borrow.}
proc `mod` *(x: typ, y: base): typ {.borrow.}
template comparable(typ: typedesc) =
proc `<` * (x, y: typ): bool {.borrow.}
proc `<=` * (x, y: typ): bool {.borrow.}
proc `==` * (x, y: typ): bool {.borrow.}
template defineCurrency(typ, base: untyped) =
type
typ* = distinct base
additive(typ)
multiplicative(typ, base)
comparable(typ)
defineCurrency(Dollar, int)
defineCurrency(Euro, int)
Пragma borrow также может использоваться для аннотации типа distinct, чтобы разрешить подъём определённых встроенных операций:
type
Foo = object
a, b: int
s: string
Bar {.borrow: `.`.} = distinct Foo
var bb: ref Bar
new bb
# field access now valid
bb.a = 90
bb.s = "abc"
В настоящее время только доступ к членам через точку может быть заимствован таким образом.
Предотвращение атак SQL-инъекции
SQL-запрос, который передаётся от Nim в базу данных SQL, может быть смоделирован как строка. Однако использование шаблонов строк и заполнение значений уязвимо для известной атаки SQL-инъекции:
import strutils
proc query(db: DbHandle, statement: string) = ...
var
username: string
db.query("SELECT FROM users WHERE name = '$1'" % username)
# Horrible security hole, but the compiler does not mind!
Этого можно избежать, различая строки, содержащие SQL, и строки, которые его не содержат. Типы distinct предоставляют способ введения нового типа строки SQL, который несовместим с string:
type
SQL = distinct string
proc query(db: DbHandle, statement: SQL) = ...
var
username: string
db.query("SELECT FROM users WHERE name = '$1'" % username)
# Static error: `query` expects an SQL string!
Это существенное свойство абстрактных типов, что они не подразумевают отношение подтипа между абстрактным типом и его базовым типом. Явные преобразования типа из string в SQL разрешены:
import strutils, sequtils
proc properQuote(s: string): SQL =
# quotes a string properly for an SQL statement
return SQL(s)
proc `%` (frmt: SQL, values: openarray[string]): SQL =
# quote each argument:
let v = values.mapIt(SQL, properQuote(it))
# we need a temporary type for the type conversion :-(
type StrSeq = seq[string]
# call strutils.`%`:
result = SQL(string(frmt) % StrSeq(v))
db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])
Теперь у нас есть проверка на этапе компиляции против атак SQL-инъекции. Поскольку "".SQL преобразуется в SQL("") не требуется новый синтаксис для красивых SQL строковых литералов. Гипотетический тип SQL фактически существует в библиотеке в качестве типа SqlQuery модулей, таких как db_sqlite.
Автоматический тип
Тип auto может использоваться только для типов возвращаемых значений и параметров. Для типов возвращаемых значений он заставляет компилятор выводить тип из тела процедуры:
proc returnsInt(): auto = 1984
Для параметров в настоящее время он неявно создаёт процедуры с параметрами дженериками:
proc foo(a, b: auto) = discard
То же самое, что и:
proc foo[T1, T2](a: T1, b: T2) = discard
Однако в последующих версиях языка это может измениться, чтобы означать «вывод типов параметров из тела». Тогда вышеупомянутый foo будет отвергнут, так как типы параметров не могут быть выведены из пустого discard оператора.
Отношения между типами
В следующем разделе определены несколько отношений между типами, которые необходимы для описания проверки типов, выполняемой компилятором.
Равенство типов
Nim использует структурное равенство типов для большинства типов. Только для объектов, перечислений и типов distinct используется равенство имён. Следующий алгоритм (в псевдокоде) определяет равенство типов:
proc typeEqualsAux(a, b: PType,
s: var HashSet[(PType, PType)]): bool =
if (a,b) in s: return true
incl(s, (a,b))
if a.kind == b.kind:
case a.kind
of int, intXX, float, floatXX, char, string, cstring, pointer,
bool, nil, void:
# leaf type: kinds identical; nothing more to check
result = true
of ref, ptr, var, set, seq, openarray:
result = typeEqualsAux(a.baseType, b.baseType, s)
of range:
result = typeEqualsAux(a.baseType, b.baseType, s) and
(a.rangeA == b.rangeA) and (a.rangeB == b.rangeB)
of array:
result = typeEqualsAux(a.baseType, b.baseType, s) and
typeEqualsAux(a.indexType, b.indexType, s)
of tuple:
if a.tupleLen == b.tupleLen:
for i in 0..a.tupleLen-1:
if not typeEqualsAux(a[i], b[i], s): return false
result = true
of object, enum, distinct:
result = a == b
of proc:
result = typeEqualsAux(a.parameterTuple, b.parameterTuple, s) and
typeEqualsAux(a.resultType, b.resultType, s) and
a.callingConvention == b.callingConvention
proc typeEquals(a, b: PType): bool =
var s: HashSet[(PType, PType)] = {}
result = typeEqualsAux(a, b, s)
Поскольку типы являются графами, которые могут иметь циклы, вышеуказанный алгоритм нуждается в вспомогательном наборе s для обнаружения этого случая.
Равенство типов с учётом различия типов
Следующий алгоритм (в псевдокоде) определяет, равны ли два типа без учёта типов distinct. Для краткости проверка циклов с помощью вспомогательного набора s опущена:
proc typeEqualsOrDistinct(a, b: PType): bool =
if a.kind == b.kind:
case a.kind
of int, intXX, float, floatXX, char, string, cstring, pointer,
bool, nil, void:
# leaf type: kinds identical; nothing more to check
result = true
of ref, ptr, var, set, seq, openarray:
result = typeEqualsOrDistinct(a.baseType, b.baseType)
of range:
result = typeEqualsOrDistinct(a.baseType, b.baseType) and
(a.rangeA == b.rangeA) and (a.rangeB == b.rangeB)
of array:
result = typeEqualsOrDistinct(a.baseType, b.baseType) and
typeEqualsOrDistinct(a.indexType, b.indexType)
of tuple:
if a.tupleLen == b.tupleLen:
for i in 0..a.tupleLen-1:
if not typeEqualsOrDistinct(a[i], b[i]): return false
result = true
of distinct:
result = typeEqualsOrDistinct(a.baseType, b.baseType)
of object, enum:
result = a == b
of proc:
result = typeEqualsOrDistinct(a.parameterTuple, b.parameterTuple) and
typeEqualsOrDistinct(a.resultType, b.resultType) and
a.callingConvention == b.callingConvention
elif a.kind == distinct:
result = typeEqualsOrDistinct(a.baseType, b)
elif b.kind == distinct:
result = typeEqualsOrDistinct(a, b.baseType) Отношение подтипа
Если объект a наследуется от b, то a является подтипом b. Это отношение подтипа распространяется на типы var, ref, ptr:
proc isSubtype(a, b: PType): bool =
if a.kind == b.kind:
case a.kind
of object:
var aa = a.baseType
while aa != nil and aa != b: aa = aa.baseType
result = aa == b
of var, ref, ptr:
result = isSubtype(a.baseType, b.baseType) Отношение преобразуемости
Тип a неявно преобразуется в тип b тогда и только тогда, когда следующий алгоритм возвращает true:
proc isImplicitlyConvertible(a, b: PType): bool =
if isSubtype(a, b) or isCovariant(a, b):
return true
if isIntLiteral(a):
return b in {int8, int16, int32, int64, int, uint, uint8, uint16,
uint32, uint64, float32, float64}
case a.kind
of int: result = b in {int32, int64}
of int8: result = b in {int16, int32, int64, int}
of int16: result = b in {int32, int64, int}
of int32: result = b in {int64, int}
of uint: result = b in {uint32, uint64}
of uint8: result = b in {uint16, uint32, uint64}
of uint16: result = b in {uint32, uint64}
of uint32: result = b in {uint64}
of float32: result = b in {float64}
of float64: result = b in {float32}
of seq:
result = b == openArray and typeEquals(a.baseType, b.baseType)
of array:
result = b == openArray and typeEquals(a.baseType, b.baseType)
if a.baseType == char and a.indexType.rangeA == 0:
result = b == cstring
of cstring, ptr:
result = b == pointer
of string:
result = b == cstring
Неявные преобразования также выполняются для конструктора типа range Nim.
Пусть a0, b0 имеют тип T.
Пусть A = range[a0..b0] тип аргумента, F тип формального параметра. Тогда неявное преобразование из A в F существует, если a0 >= low(F) and b0 <= high(F) и оба T и F являются целыми числами со знаком или оба беззнаковыми целыми числами.
Тип a явное преобразуется в тип b тогда и только тогда, когда следующий алгоритм возвращает true:
proc isIntegralType(t: PType): bool =
result = isOrdinal(t) or t.kind in {float, float32, float64}
proc isExplicitlyConvertible(a, b: PType): bool =
result = false
if isImplicitlyConvertible(a, b): return true
if typeEqualsOrDistinct(a, b): return true
if isIntegralType(a) and isIntegralType(b): return true
if isSubtype(a, b) or isSubtype(b, a): return true
Отношение преобразуемости можно ослабить с помощью определённого пользователем типа converter.
converter toInt(x: char): int = result = ord(x) var x: int chr: char = 'a' # implicit conversion magic happens here x = chr echo x # => 97 # one can use the explicit form too x = chr.toInt echo x # => 97
Преобразование типа T(a) является L-значением, если a является L-значением и typeEqualsOrDistinct(T, typeof(a)) выполняется.
Совместимость при присваивании
Выражение b может быть присвоено выражению a тогда и только тогда, когда a является l-value и isImplicitlyConvertible(b.typ, a.typ) выполняется.
Разрешение перегрузки
В вызове p(args) выбирается наиболее подходящая процедура p. Если несколько процедур подходят одинаково хорошо, неоднозначность сообщается на этапе семантического анализа.
Каждый аргумент в args должен соответствовать. Существует несколько категорий соответствия аргумента. Пусть f тип формального параметра, а a тип аргумента.
- Точное совпадение:
aиfимеют одинаковый тип. - Буквальное совпадение:
a— это целочисленная литерал со значениемv, аf— это целого типа со знаком или без знака, иvнаходится в диапазонеf. Или:a— это литерал с плавающей точкой со значениемv, аf— это тип с плавающей точкой, иvнаходится в диапазонеf. - Общее совпадение:
f— это общий тип, иaсоответствует, например,aэтоint, иf— это общий (ограниченный) параметр типа (как в[T]или[T: int|char]. - Совпадение поддиапазона или подтипа:
aэтоrange[T], иTточно соответствуетf. Или:aявляется подтипомf. - Совпадение целочисленного преобразования:
aпреобразуется вf, иfиa— это какой-то целочисленный или тип с плавающей точкой. - Совпадение преобразования:
aпреобразуется вf, возможно, через определенный пользователемconverter.
Эти категории соответствия имеют приоритет: точное совпадение лучше, чем буквальное совпадение, а оно лучше, чем общее совпадение и т. д. В следующем count(p, m) считает количество совпадений категории соответствия m для процедуры p.
Процедура p лучше соответствует процедуре q, если следующий алгоритм возвращает true:
for each matching category m in ["exact match", "literal match",
"generic match", "subtype match",
"integral match", "conversion match"]:
if count(p, m) > count(q, m): return true
elif count(p, m) == count(q, m):
discard "continue with next category m"
else:
return false
return "ambiguous" Некоторые примеры:
proc takesInt(x: int) = echo "int" proc takesInt[T](x: T) = echo "T" proc takesInt(x: int16) = echo "int16" takesInt(4) # "int" var x: int32 takesInt(x) # "T" var y: int16 takesInt(y) # "int16" var z: range[0..4] = 0 takesInt(z) # "T"
Если этот алгоритм возвращает «неопределённый», выполняется дальнейшее уточнение: если аргумент a соответствует типу параметра f процедуры p и g процедуры q через отношение подтипирования, учитывается глубина наследования:
type A = object of RootObj B = object of A C = object of B proc p(obj: A) = echo "A" proc p(obj: B) = echo "B" var c = C() # not ambiguous, calls 'B', not 'A' since B is a subtype of A # but not vice versa: p(c) proc pp(obj: A, obj2: B) = echo "A B" proc pp(obj: B, obj2: A) = echo "B A" # but this is ambiguous: pp(c, c)
Аналогично, для общих совпадений предпочтительнее наиболее специализированный общий тип (который все еще соответствует):
proc gen[T](x: ref ref T) = echo "ref ref T" proc gen[T](x: ref T) = echo "ref T" proc gen[T](x: T) = echo "T" var ri: ref int gen(ri) # "ref T"
Перегрузка на основе 'var T'
Если формальный параметр f имеет тип var T, помимо обычной проверки типов, аргумент проверяется на то, является ли он l-значением. var T лучше соответствует, чем просто T.
proc sayHi(x: int): string =
# matches a non-var int
result = $x
proc sayHi(x: var int): string =
# matches a var int
result = $(x + 10)
proc sayHello(x: int) =
var m = x # a mutable version of x
echo sayHi(x) # matches the non-var version of sayHi
echo sayHi(m) # matches the var version of sayHi
sayHello(3) # 3
# 13 Ленивое разрешение типов для нетипизированных
Примечание: неразрешённое выражение — это выражение, для которого не были выполнены поиск символов и проверка типов.
Поскольку шаблоны и макросы, не объявленные как immediate, участвуют в разрешении перегрузки, необходимо иметь способ передачи неразрешённых выражений шаблону или макросу. Это достигается с помощью метатипа untyped:
template rem(x: untyped) = discard rem unresolvedExpression(undeclaredIdentifier)
Параметр типа untyped всегда соответствует любому аргументу (при условии, что какой-либо аргумент ему передан).
Но нужно быть внимательным, потому что другие перегрузки могут вызвать разрешение аргумента:
template rem(x: untyped) = discard proc rem[T](x: T) = discard # undeclared identifier: 'unresolvedExpression' rem unresolvedExpression(undeclaredIdentifier)
untyped и varargs[untyped] — единственные метатипы, которые ленивы в этом смысле; остальные метатипы typed и typedesc не ленивы.
Соответствие varargs
См. Varargs.
Операторы и выражения
Nim использует общую парадигму операторов/выражений: операторы не производят значения в отличие от выражений. Однако некоторые выражения являются операторами.
Операторы делятся на простые операторы и сложные операторы. Простые операторы — это операторы, которые не могут содержать другие операторы, такие как присваивания, вызовы или оператор return; сложные операторы могут содержать другие операторы. Чтобы избежать проблемы «висящего else», сложные операторы всегда должны иметь отступы. Подробности можно найти в грамматике.
Выражение списка операторов
Операторы также могут встречаться в контексте выражения, который выглядит как (stmt1; stmt2; ...; ex). Это называется выражением списка операторов или (;). Тип (stmt1; stmt2; ...; ex) — это тип ex. Все остальные операторы должны иметь тип void. (Можно использовать discard для получения типа void.) (;) не вводит новую область видимости.
Оператор discard
Пример:
proc p(x, y: int): int = result = x + y discard p(3, 4) # discard the return value of `p`
Оператор discard вычисляет своё выражение для побочных эффектов и отбрасывает возвращаемое значение выражения, и его следует использовать только в тех случаях, когда известно, что игнорирование этого значения не приведёт к проблемам.
Игнорирование возвращаемого значения процедуры без использования оператора discard — это статическая ошибка.
Возвращаемое значение можно неявно игнорировать, если вызываемый proc/итератор был объявлен с псевдонимом discardable:
proc p(x, y: int): int {.discardable.} =
result = x + y
p(3, 4) # now valid
однако псевдоним discardable не работает с шаблонами, так как шаблоны подставляют AST на место. Например:
{.push discardable .}
template example(): string = "https://nim-lang.org"
{.pop.}
example()
Этот шаблон будет разрешён в "https://nim-lang.org", что является строковой литерал, и поскольку {.discardable.} не применяется к литералам, компилятор выдаст ошибку.
Пустой discard оператор часто используется как пустой оператор:
proc classify(s: string) = case s[0] of SymChars, '_': echo "an identifier" of '0'..'9': echo "a number" else: discard
Контекст void
В списке операторов каждое выражение, кроме последнего, должно иметь тип void. Кроме этого правила, присвоение встроенному символу result также активирует обязательный контекст void для последующих выражений:
proc invalid*(): string = result = "foo" "invalid" # Error: value of type 'string' has to be discarded
proc valid*(): string = let x = 317 "valid"
Оператор var
Оператор var объявляет новые локальные и глобальные переменные и инициализирует их. Можно использовать список переменных через запятую, чтобы указать переменные одного типа:
var a: int = 0 x, y, z: int
Если задана инициализация, тип можно опустить: переменная будет иметь тот же тип, что и инициализирующее выражение. Переменные всегда инициализируются значением по умолчанию, если нет выражения инициализации. Значение по умолчанию зависит от типа и всегда равно нулю в двоичном представлении.
| Тип | значение по умолчанию |
|---|---|
| любой целочисленный тип | 0 |
| любой тип с плавающей точкой | 0.0 |
| char | '\0' |
| bool | false |
| тип ref или указатель | nil |
| тип процедуры | nil |
| последовательность | @[] |
| строка | "" |
| кортеж[x: A, y: B, ...] | (default(A), default(B), ...) (аналогично для объектов) |
| массив[0..., T] | [default(T), ...] |
| диапазон[T] | default(T); может быть вне допустимого диапазона |
| T = enum | cast[T](0); может быть недопустимым значением |
Неявную инициализацию можно избежать для оптимизации с помощью псевдонима noinit:
var
a {.noInit.}: array[0..1023, char]
Если процедура помечена псевдонимом noinit, это относится к её неявной переменной result:
proc returnUndefinedValue: int {.noinit.} = discard
Неявную инициализацию также можно предотвратить с помощью псевдонима requiresInit типа. Компилятор требует явной инициализации объекта и всех его полей. Однако он выполняет анализ потока управления, чтобы доказать, что переменная была инициализирована, и не полагается на синтаксические свойства:
type
MyObject = object {.requiresInit.}
proc p() =
# the following is valid:
var x: MyObject
if someCondition():
x = a()
else:
x = a()
# use x Оператор let
Оператор let объявляет новые локальные и глобальные переменные с единственным присвоением и связывает их со значением. Синтаксис аналогичен оператору var, за исключением того, что ключевое слово var заменено ключевым словом let. Переменные let не являются l-значениями и поэтому не могут передаваться в параметры var ни взятие их адреса. Их невозможно присвоить новые значения.
Для переменных let доступны те же псевдонимы, что и для обычных переменных.
Так как операторы let являются неизменяемыми после создания, они должны определять значение при объявлении. Единственное исключение — если применён псевдоним {.importc.} (или любой другой importX псевдоним), в этом случае ожидается, что значение будет получено из кода нативном языке, обычно из C/C++ const.
Распаковка кортежей
В операторе var или let может быть выполнена распаковка кортежей. Специальный идентификатор _ может использоваться для игнорирования некоторых частей кортежа:
proc returnsTuple(): (int, int, int) = (4, 2, 3) let (x, _, z) = returnsTuple()
Раздел const
Раздел const объявляет константы, значения которых являются константными выражениями:
import strutils
const
roundPi = 3.1415
constEval = contains("abc", 'b') # computed at compile time!
После объявления символ константы может использоваться как константное выражение.
См. Константы и константные выражения для подробностей.
Статический оператор/выражение
Статический оператор/выражение явно требует выполнения во время компиляции. Даже некоторый код с побочными эффектами разрешается в блоке static:
static: echo "echo at compile time"
Существуют ограничения на то, какой код Nim может быть выполнен во время компиляции; см. Ограничения на выполнение во время компиляции для подробностей. Если компилятор не может выполнить блок во время компиляции, это статическая ошибка.
Оператор if
Пример:
var name = readLine(stdin) if name == "Andreas": echo "What a nice name!" elif name == "": echo "Don't you have a name?" else: echo "Boring name..."
Оператор if — это простой способ создания ветвления в потоке управления: выражение после ключевого слова if вычисляется; если оно истинно, выполняются соответствующие операторы после :. В противном случае вычисляется выражение после elif (если есть ветвь elif); если оно истинно, выполняются соответствующие операторы после :. Так продолжается до последней elif. Если все условия ложны, выполняется часть else. Если части else нет, выполнение продолжается со следующего оператора.
В операторах if новые области видимости начинаются сразу после ключевых слов if/elif/else и заканчиваются после соответствующего блока then. Для визуализации области видимости в следующем примере заключены в {| |}.
if {| (let m = input =~ re"(\w+)=\w+"; m.isMatch):
echo "key ", m[0], " value ", m[1] |}
elif {| (let m = input =~ re""; m.isMatch):
echo "new m in this scope" |}
else: {|
echo "m not declared here" |} Оператор case
Пример:
case readline(stdin)
of "delete-everything", "restart-computer":
echo "permission denied"
of "go-for-a-walk": echo "please yourself"
else: echo "unknown command"
# indentation of the branches is also allowed; and so is an optional colon
# after the selecting expression:
case readline(stdin):
of "delete-everything", "restart-computer":
echo "permission denied"
of "go-for-a-walk": echo "please yourself"
else: echo "unknown command"
Оператор case похож на оператор if, но представляет собой многоветвевой выбор. Выражение после ключевого слова case вычисляется, и если его значение находится в списке срезов, выполняются соответствующие операторы (после ключевого слова of). Если значение не находится ни в одном заданном списке срезов, выполняется часть else. Если части else нет, и не все возможные значения, которые может принимать expr, не встречаются в slicelist, возникает статическая ошибка. Это справедливо только для выражений порядковых типов. «Все возможные значения» expr определяются типом expr. Для подавления статической ошибки следует использовать часть else с пустым оператором discard.
Для типов, не являющихся порядковыми, невозможно перечислить каждое возможное значение, поэтому им всегда требуется часть else.
Поскольку операторы case проверяются на полноту во время семантического анализа, значение в каждой ветви of должно быть константным выражением. Это ограничение также позволяет компилятору генерировать более производительный код.
В качестве специального семантического расширения, выражение в ветви of оператора case может вычисляться в конструктор множества или массива; множество или массив затем расширяются в список его элементов:
const
SymChars: set[char] = {'a'..'z', 'A'..'Z', '\x80'..'\xFF'}
proc classify(s: string) =
case s[0]
of SymChars, '_': echo "an identifier"
of '0'..'9': echo "a number"
else: echo "other"
# is equivalent to:
proc classify(s: string) =
case s[0]
of 'a'..'z', 'A'..'Z', '\x80'..'\xFF', '_': echo "an identifier"
of '0'..'9': echo "a number"
else: echo "other"
Оператор case не производит l-значение, поэтому следующий пример не сработает:
type
Foo = ref object
x: seq[string]
proc get_x(x: Foo): var seq[string] =
# doesn't work
case true
of true:
x.x
else:
x.x
var foo = Foo(x: @[])
foo.get_x().add("asd")
Это можно исправить, явно используя return:
proc get_x(x: Foo): var seq[string] =
case true
of true:
return x.x
else:
return x.x Оператор when
Пример:
when sizeof(int) == 2: echo "running on a 16 bit system!" elif sizeof(int) == 4: echo "running on a 32 bit system!" elif sizeof(int) == 8: echo "running on a 64 bit system!" else: echo "cannot happen!"
Оператор when почти идентичен оператору if с некоторыми исключениями:
- Каждое условие (
expr) должно быть константным выражением (типаbool). - Операторы не открывают новую область видимости.
- Операторы, относящиеся к выражению, которое вычислилось как истинное, переводятся компилятором; другие операторы не проверяются на семантику! Однако каждое условие проверяется на семантику.
Оператор when позволяет использовать методы условной компиляции. В качестве специального синтаксического расширения конструкция when также доступна внутри определений object.
Оператор when nimvm
nimvm — это специальный символ, который может использоваться в качестве выражения оператора when nimvm для различения путей выполнения между временем компиляции и исполняемым файлом.
Пример:
proc someProcThatMayRunInCompileTime(): bool =
when nimvm:
# This branch is taken at compile time.
result = true
else:
# This branch is taken in the executable.
result = false
const ctValue = someProcThatMayRunInCompileTime()
let rtValue = someProcThatMayRunInCompileTime()
assert(ctValue == true)
assert(rtValue == false)
Оператор when nimvm должен удовлетворять следующим требованиям:
- Его выражение должно всегда быть
nimvm. Более сложные выражения не допускаются. - Он не должен содержать ветвей
elif. - Он должен содержать ветвь
else. - Код во ветвях не должен влиять на семантику кода, следующего за оператором
when nimvm. Например, он не должен определять символы, используемые в последующем коде.
Оператор return
Пример:
return 40+2
Оператор return завершает выполнение текущей процедуры. Он разрешён только в процедурах. Если есть expr, это синтаксический сахар для:
result = expr return result
return без выражения — это короткая запись для return result , если у процедуры есть тип возвращаемого значения. Переменная result всегда является значением возврата процедуры. Она автоматически объявляется компилятором. Как и все переменные, result инициализируется (двоичным) нулём:
proc returnZero(): int = # implicitly returns 0
Оператор yield
Пример:
yield (1, 2, 3)
Оператор yield используется вместо оператора return в итераторах. Он допустим только в итераторах. Выполнение возвращается в тело цикла for, который вызвал итератор. Yield не завершает процесс итерации, но выполнение передаётся обратно итератору, если начинается следующая итерация. См. раздел об итераторах (Итераторы и оператор for) для получения дополнительной информации.
Оператор блока
Пример:
var found = false
block myblock:
for i in 0..3:
for j in 0..3:
if a[j][i] == 7:
found = true
break myblock # leave the block, in this case both for-loops
echo found
Оператор блока — это способ группирования операторов в (именованный) block. Внутри блока разрешён оператор break для немедленного выхода из блока. Оператор break может содержать имя окружающего блока, чтобы указать, из какого блока требуется выйти.
Оператор break
Пример:
break
Оператор break используется для немедленного выхода из блока. Если задано symbol, это имя вложенного блока, из которого требуется выйти. Если оно отсутствует, покидается самый внутренний блок.
Оператор while
Пример:
echo "Please tell me your password:" var pw = readLine(stdin) while pw != "12345": echo "Wrong password! Next try:" pw = readLine(stdin)
Оператор while выполняется до тех пор, пока expr не вычислится как ложное. Бесконечные циклы не являются ошибкой. Операторы while открывают implicit block, так что их можно покинуть с помощью оператора break.
Оператор continue
Оператор continue приводит к немедленной следующей итерации окружающего цикла. Он допустим только внутри цикла. Оператор continue — это синтаксический сахар для вложенного блока:
while expr1: stmt1 continue stmt2
Эквивалентно:
while expr1:
block myBlockName:
stmt1
break myBlockName
stmt2 Оператор Assembler
Прямое встраивание ассемблерного кода в код Nim поддерживается оператором unsafe asm . Идентификаторы в ассемблерном коде, которые ссылаются на Nim-идентификаторы, должны быть заключены в специальный символ, который можно указать в предикатах оператора. По умолчанию специальным символом является '`':
{.push stackTrace:off.}
proc addInt(a, b: int): int =
# a in eax, and b in edx
asm """
mov eax, `a`
add eax, `b`
jno theEnd
call `raiseOverflow`
theEnd:
"""
{.pop.}
Если используется ассемблер GNU, кавычки и символы новой строки вставляются автоматически:
proc addInt(a, b: int): int =
asm """
addl %%ecx, %%eax
jno 1
call `raiseOverflow`
1:
:"=a"(`result`)
:"a"(`a`), "c"(`b`)
"""
Вместо:
proc addInt(a, b: int): int =
asm """
"addl %%ecx, %%eax\n"
"jno 1\n"
"call `raiseOverflow`\n"
"1: \n"
:"=a"(`result`)
:"a"(`a`), "c"(`b`)
""" Оператор using
Оператор using предоставляет синтаксическую удобство в модулях, где одни и те же имена параметров и типы используются многократно. Вместо:
proc foo(c: Context; n: Node) = ... proc bar(c: Context; n: Node, counter: int) = ... proc baz(c: Context; n: Node) = ...
Можно сообщить компилятору о соглашении, что параметр с именем c должен иметь тип по умолчанию Context, n должен иметь тип по умолчанию Node и т. д.:
using c: Context n: Node counter: int proc foo(c, n) = ... proc bar(c, n, counter) = ... proc baz(c, n) = ... proc mixedMode(c, n; x, y: int) = # 'c' is inferred to be of the type 'Context' # 'n' is inferred to be of the type 'Node' # But 'x' and 'y' are of type 'int'.
Раздел using использует тот же синтаксис группирования на основе отступов, что и раздел var или let.
Обратите внимание, что using не применяется к template, поскольку параметры шаблона без типа по умолчанию имеют тип system.untyped.
Смешение параметров, которые должны использовать объявление using, с параметрами, которые явно типизированы, возможно и требует точки с запятой между ними.
Выражение if
Выражение if expression почти идентично оператору if, но это выражение. Эта функция аналогична оператору ternary operators в других языках. Пример:
var y = if x > 8: 9 else: 10
Выражение if всегда возвращает значение, поэтому часть else требуется. Также разрешены части Elif.
Выражение when
Точно так же, как и выражение if expression, но соответствующее оператору when.
Выражение case
Выражение case expression опять же очень похоже на оператор case:
var favoriteFood = case animal
of "dog": "bones"
of "cat": "mice"
elif animal.endsWith"whale": "plankton"
else:
echo "I'm not sure what to serve, but everybody loves ice cream"
"ice cream"
Как видно из приведенного выше примера, выражение case также может иметь побочные эффекты. Когда для ветви задано несколько операторов, Nim использует последнее выражение в качестве значения результата.
Выражение блока
Выражение block expression почти идентично оператору блока, но это выражение, которое использует последнее выражение в блоке в качестве значения. Оно похоже на выражение списка операторов, но выражение списка операторов не открывает новую область видимости блока.
let a = block:
var fib = @[0, 1]
for i in 0..10:
fib.add fib[^1] + fib[^2]
fib Конструктор таблицы
Конструктор таблицы — это синтаксический сахар для конструктора массива:
{"key1": "value1", "key2", "key3": "value2"}
# is the same as:
[("key1", "value1"), ("key2", "value2"), ("key3", "value2")]
Пустая таблица может быть записана как {:} (в отличие от пустого множества, которое является {}), что является другим способом записи конструктора пустого массива []. Этот немного необычный способ поддержки таблиц имеет много преимуществ:
- Порядок пар (ключ, значение) сохраняется, поэтому легко поддерживать упорядоченные словари, например, с помощью
{key: val}.newOrderedTable. - Литерал таблицы можно поместить в раздел
constи компилятор может легко поместить его в раздел данных исполняемого файла, как он это делает для массивов, и сгенерированный раздел данных требует минимального объема памяти. - Каждая реализация таблиц рассматривается одинаково с синтаксической точки зрения.
- Помимо минимального синтаксического сахара, ядру языка не нужно знать о таблицах.
Преобразования типов
Синтаксически преобразование типа похоже на вызов процедуры, но вместо имени процедуры используется имя типа. Преобразование типа всегда безопасно в том смысле, что неудачное преобразование типа в другой тип приводит к исключению (если это невозможно определить статически).
Обычные процедуры часто предпочтительнее преобразований типов в Nim: Например, $ по соглашению является оператором toString и toFloat и toInt могут использоваться для преобразования из чисел с плавающей точкой в целые числа или наоборот.
Преобразование типа также может использоваться для устранения неоднозначности перегруженных процедур:
proc p(x: int) = echo "int"
proc p(x: string) = echo "string"
let procVar = (proc(x: string))(p)
procVar("a")
Поскольку операции с беззнаковыми числами циклически переполняются и не проверяются, преобразования типов в беззнаковые целые числа и между беззнаковыми целыми числами также не проверяются. Основное обоснование заключается в лучшей взаимозаменяемости с языком программирования C при переносе алгоритмов из C в Nim.
Исключение: значения, преобразованные в беззнаковый тип во время компиляции, проверяются, чтобы код, подобный byte(-1) не компилировался.
Примечание: Исторически операции не проверялись, а преобразования иногда проверялись, но начиная с редакции 1.0.4 этого документа и реализации языка преобразования теперь всегда не проверяются.
Преобразования типов
Преобразования типов — это грубый механизм для интерпретации битового представления выражения так, как будто оно относится к другому типу. Преобразования типов необходимы только для низкоуровневого программирования и по своей сути небезопасны.
cast[int](x)
Целевой тип преобразования должен быть конкретным типом, например, целевой тип, который является типом класса (который неконкретен), будет недопустимым:
type Foo = int or float var x = cast[Foo](1) # Error: cannot cast to a non concrete type: 'Foo'
Преобразования типов не следует путать с преобразованиями типов, как упоминалось в предыдущем разделе. В отличие от преобразований типов, преобразование типов не может изменить подлежащее битовое представление данных, преобразуемых (кроме того, размер целевого типа может отличаться от размера исходного типа). Преобразование напоминает приведение типов в других языках или функции reinterpret_cast и bit_cast в C++.
Оператор addr
Оператор addr возвращает адрес l-значения. Если тип расположения — T, результат оператора addr имеет тип ptr T. Адрес всегда является неотслеживаемой ссылкой. Получение адреса объекта, находящегося в стеке, является небезопасным, поскольку указатель может существовать дольше, чем объект в стеке, и, таким образом, может ссылаться на несуществующий объект. Можно получить адрес переменных, но нельзя использовать его для переменных, объявленных с помощью let:
let t1 = "Hello" var t2 = t1 t3 : pointer = addr(t2) echo repr(addr(t2)) # --> ref 0x7fff6b71b670 --> 0x10bb81050"Hello" echo cast[ptr string](t3)[] # --> Hello # The following line doesn't compile: echo repr(addr(t1)) # Error: expression has no address
Оператор unsafeAddr
Для лучшей взаимозаменяемости с другими компилируемыми языками, такими как C, для получения адреса переменной let, параметра или переменной цикла for, можно использовать операцию unsafeAddr.
let myArray = [1, 2, 3] foreignProcThatTakesAnAddr(unsafeAddr myArray)
Процедуры
То, что большинство языков программирования называют методами или функциями, в Nim называется процедурами. Объявление процедуры состоит из идентификатора, нуля или более формальных параметров, типа возвращаемого значения и блока кода. Формальные параметры объявляются как список идентификаторов, разделенных запятыми или точкой с запятой. Параметру присваивается тип с помощью : typename. Тип применяется ко всем параметрам непосредственно перед ним, до начала списка параметров, разделителя точкой с запятой или уже типизированного параметра. Точка с запятой может использоваться для более четкого разделения типов и последующих идентификаторов.
# Using only commas proc foo(a, b: int, c, d: bool): int # Using semicolon for visual distinction proc foo(a, b: int; c, d: bool): int # Will fail: a is untyped since ';' stops type propagation. proc foo(a; b: int; c, d: bool): int
Параметр может быть объявлен со значением по умолчанию, которое используется, если вызывающий не предоставляет значения для аргумента.
# b is optional with 47 as its default value proc foo(a: int, b: int = 47): int
Параметры могут быть объявлены изменяемыми, позволяя процедуре изменять эти аргументы, используя модификатор типа var.
# "returning" a value to the caller through the 2nd argument # Notice that the function uses no actual return value at all (ie void) proc foo(inp: int, outp: var int) = outp = inp + 47
Если в объявлении процедуры нет тела, это — объявление forward. Если процедура возвращает значение, тело процедуры может получить доступ к неявно объявленной переменной с именем result, которая представляет возвращаемое значение. Процедуры могут быть перегружены. Алгоритм разрешения перегрузки определяет, какая процедура является лучшим соответствием для аргументов. Пример:
proc toLower(c: char): char = # toLower for characters
if c in {'A'..'Z'}:
result = chr(ord(c) + (ord('a') - ord('A')))
else:
result = c
proc toLower(s: string): string = # toLower for strings
result = newString(len(s))
for i in 0..len(s) - 1:
result[i] = toLower(s[i]) # calls toLower for characters; no recursion!
Вызов процедуры может осуществляться различными способами:
proc callme(x, y: int, s: string = "", c: char, b: bool = false) = ... # call with positional arguments # parameter bindings: callme(0, 1, "abc", '\t', true) # (x=0, y=1, s="abc", c='\t', b=true) # call with named and positional arguments: callme(y=1, x=0, "abd", '\t') # (x=0, y=1, s="abd", c='\t', b=false) # call with named arguments (order is not relevant): callme(c='\t', y=1, x=0) # (x=0, y=1, s="", c='\t', b=false) # call as a command statement: no () needed: callme 0, 1, "abc", '\t' # (x=0, y=1, s="abc", c='\t', b=false)
Процедура может вызывать себя рекурсивно.
Операторы — это процедуры со специальным операторным символом в качестве идентификатора:
proc `$` (x: int): string = # converts an integer to a string; this is a prefix operator. result = intToStr(x)
Операторы с одним параметром — это префиксные операторы, операторы с двумя параметрами — инфиксные операторы. (Однако, анализатор различает их по положению оператора в выражении.) Нет способа объявить постфиксные операторы: все постфиксные операторы являются встроенными и обрабатываются грамматикой явно.
Любой оператор может быть вызван как обычная процедура с помощью обозначения 'opr'. (Таким образом, оператор может иметь более двух параметров):
proc `*+` (a, b, c: int): int = # Multiply and add result = a * b + c assert `*+`(3, 4, 6) == `+`(`*`(a, b), c)
Маркер экспорта
Если объявленный символ помечен звездочкой, он экспортируется из текущего модуля:
proc exportedEcho*(s: string) = echo s
proc `*`*(a: string; b: int): string =
result = newStringOfCap(a.len * b)
for i in 1..b: result.add a
var exportedVar*: int
const exportedConst* = 78
type
ExportedType* = object
exportedField*: int Синтаксис вызова метода
Для объектно-ориентированного программирования может использоваться синтаксис obj.method(args) вместо method(obj, args). Скобки можно опустить, если больше нет аргументов: obj.len (вместо len(obj)).
Этот синтаксис вызова метода не ограничивается объектами, он может использоваться для предоставления любого типа первого аргумента для процедур:
echo "abc".len # is the same as echo len "abc"
echo "abc".toUpper()
echo {'a', 'b', 'c'}.card
stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo")
Другой способ взглянуть на синтаксис вызова метода заключается в том, что он предоставляет недостающую постфиксную запись.
Синтаксис вызова метода конфликтует с явными обобщенными инстанцированиями: p[T](x) не может быть записано как x.p[T], потому что x.p[T] всегда анализируется как (x.p)[T].
См. также: Ограничения синтаксиса вызова метода.
Обозначение [: ] разработано для решения этой проблемы: x.p[:T] переписывается анализатором в p[T](x), x.p[:T](y) переписывается в p[T](x, y). Обратите внимание, что [: ] не имеет представления AST, переписывание выполняется непосредственно на этапе анализа.
Свойства
Nim не нуждается в свойствах-получателях: обычные процедуры-получатели, которые вызываются с синтаксисом вызова метода, достигают того же. Но установка значения отличается; для этого нужен специальный синтаксис установщика:
# Module asocket
type
Socket* = ref object of RootObj
host: int # cannot be accessed from the outside of the module
proc `host=`*(s: var Socket, value: int) {.inline.} =
## setter of hostAddr.
## This accesses the 'host' field and is not a recursive call to
## ``host=`` because the builtin dot access is preferred if it is
## available:
s.host = value
proc host*(s: Socket): int {.inline.} =
## getter of hostAddr
## This accesses the 'host' field and is not a recursive call to
## ``host`` because the builtin dot access is preferred if it is
## available:
s.host
# module B import asocket var s: Socket new s s.host = 34 # same as `host=`(s, 34)
Процедура, определенная как f= (с заключительным =) называется установщиком. Установщик можно вызвать явно с помощью обычного обозначения в обратных кавычках:
proc `f=`(x: MyObject; value: string) = discard `f=`(myObject, "value")
f= может вызываться неявно в шаблоне x.f = value, если и только если тип x не имеет поля с именем f или если f не виден в текущем модуле. Эти правила гарантируют, что поля объектов и их аксессоры могут иметь одинаковое имя. Внутри модуля x.f всегда интерпретируется как доступ к полю, а вне модуля — как вызов процедуры аксессора.
Синтаксис вызова команды
Процедуры могут вызываться без (), если вызов синтаксически является оператором. Этот синтаксис вызова команды также работает для выражений, но за ним может следовать только один аргумент. Это ограничение означает, что echo f 1, f 2 анализируется как echo(f(1), f(2)), а не как echo(f(1, f(2))). Синтаксис вызова метода может использоваться для предоставления одного дополнительного аргумента в этом случае:
proc optarg(x: int, y: int = 0): int = x + y proc singlearg(x: int): int = 20*x echo optarg 1, " ", singlearg 2 # prints "1 40" let fail = optarg 1, optarg 8 # Wrong. Too many arguments for a command call let x = optarg(1, optarg 8) # traditional procedure call with 2 arguments let y = 1.optarg optarg 8 # same thing as above, w/o the parenthesis assert x == y
Синтаксис вызова команды также не может иметь сложных выражений в качестве аргументов. Например: (анонимные процедуры), if, case или try. Вызовы функций без аргументов все равно нуждаются в () для различения вызова от самой функции как значения первого класса.
Замыкания
Процедуры могут появляться на верхнем уровне в модуле, а также внутри других областей видимости, в этом случае они называются вложенными процедурами. Вложенная процедура может получать доступ к локальным переменным из своего внешнего окружения, и если это происходит, она становится замыканием. Любые захваченные переменные хранятся в скрытом дополнительном аргументе замыкания (его окружении), и к ним осуществляется доступ по ссылке как для замыкания, так и для его внешнего окружения (т.е. любые изменения, внесенные в них, видны в обоих местах). Окружение замыкания может быть выделено в куче или стеке, если компилятор определит, что это безопасно.
Создание замыканий в циклах
Поскольку замыкания захватывают локальные переменные по ссылке, это часто нежелательное поведение внутри циклов. См. closureScope и capture для получения подробной информации о том, как изменить это поведение.
Анонимные процедуры
Можно использовать безымянные процедуры в качестве лямбда-выражений для передачи в другие процедуры:
var cities = @["Frankfurt", "Tokyo", "New York", "Kyiv"]
cities.sort(proc (x,y: string): int =
cmp(x.len, y.len))
Процедуры как выражения могут появляться как вложенные процедуры, так и внутри кода верхнего уровня. Модуль sugar содержит макрос =>, который позволяет использовать более лаконичный синтаксис для анонимных процедур, напоминающий лямбды, как они используются в таких языках, как JavaScript, C# и т.д.
Func
Ключевое слово func вводит сокращение для процедуры noSideEffect.
func binarySearch[T](a: openArray[T]; elem: T): int
Является сокращением для:
proc binarySearch[T](a: openArray[T]; elem: T): int {.noSideEffect.} Неперегружаемые встроенные функции
Следующие встроенные процедуры не могут быть перегружены по причинам простоты реализации (они требуют специализированной семантической проверки):
declared, defined, definedInScope, compiles, sizeof, is, shallowCopy, getAst, astToStr, spawn, procCall
Таким образом, они действуют больше как ключевые слова, чем как обычные идентификаторы; в отличие от ключевого слова, переопределение может затенять определение в модуле system. Из этого списка следующее не следует писать с помощью точечной нотации x.f, так как x не может быть проверено на типы до передачи в f:
declared, defined, definedInScope, compiles, getAst, astToStr
Параметры var
Тип параметра может быть префиксным ключевым словом var:
proc divmod(a, b: int; res, remainder: var int) = res = a div b remainder = a mod b var x, y: int divmod(8, 5, x, y) # modifies x and y assert x == 1 assert y == 3
В примере res и remainder являются var parameters. Параметры var могут изменяться процедурой, и изменения видны вызывающей стороне. Аргумент, переданный параметру var, должен быть l-значением. Параметры var реализуются как скрытые указатели. Приведенный выше пример эквивалентен:
proc divmod(a, b: int; res, remainder: ptr int) = res[] = a div b remainder[] = a mod b var x, y: int divmod(8, 5, addr(x), addr(y)) assert x == 1 assert y == 3
В примерах для получения двух возвращаемых значений используются параметры var или указатели. Это можно сделать более чистым способом, вернув кортеж:
proc divmod(a, b: int): tuple[res, remainder: int] = (a div b, a mod b) var t = divmod(8, 5) assert t.res == 1 assert t.remainder == 3
Для доступа к полям кортежа можно использовать распаковку кортежа:
var (x, y) = divmod(8, 5) # tuple unpacking assert x == 1 assert y == 3
Примечание: var параметры никогда не нужны для эффективной передачи параметров. Поскольку параметры без модификации не могут быть изменены, компилятор всегда свободен передавать аргументы по ссылке, если считает, что это ускорит выполнение.
Тип возвращаемого значения var
Процедура, преобразователь или итератор могут вернуть var тип, что означает, что возвращаемое значение является l-значением и может быть изменено вызывающей стороной:
var g = 0 proc writeAccessToG(): var int = result = g writeAccessToG() = 6 assert g == 6
Это статическая ошибка, если неявный указатель может быть использован для доступа к месту за пределами его жизненного цикла:
proc writeAccessToG(): var int = var g = 0 result = g # Error!
Для итераторов компонент типа возвращаемого кортежа также может иметь var тип:
iterator mpairs(a: var seq[string]): tuple[key: int, val: var string] =
for i in 0..a.high:
yield (i, a[i])
В стандартной библиотеке каждое имя процедуры, возвращающей var тип, по соглашению начинается с префикса m.
var T обеспечивается простым правилом заимствования: если result не ссылается на место, указывающее на кучу (то есть в result = X случае X включает доступ по ptr или ref ссылке), то он должен быть получен от первого параметра процедуры:proc forward[T](x: var T): var T =
result = x # ok, derived from the first parameter.
proc p(param: var int): var int =
var x: int
# we know 'forward' provides a view into the location derived from
# its first argument 'x'.
result = forward(x) # Error: location is derived from ``x``
# which is not p's first parameter and lives
# on the stack.
Другими словами, жизненный цикл того, на что указывает result, привязан к жизненному циклу первого параметра, и этого достаточно, чтобы проверить безопасность памяти в месте вызова.
Будущие направления
Более поздние версии Nim могут быть более точными в отношении правила заимствования с синтаксисом, подобным:
proc foo(other: Y; container: var X): var T from container
Здесь var T from container явно показывает, что местоположение получено от второго параметра (в данном случае называемого «контейнером»). Синтаксис var T from p определяет тип varTy[T, 2], который несовместим с varTy[T, 1].
NRVO
Примечание: Этот раздел описывает текущую реализацию. Эта часть спецификации языка будет изменена. См. https://github.com/nim-lang/RFCs/issues/230 для получения дополнительной информации.
Значение возврата представлено внутри тела процедуры как специальная переменная result. Это позволяет механизм, похожий на оптимизацию «именованного возвращаемого значения» C++ (NRVO). NRVO означает, что записи в result внутри p напрямую влияют на место назначения dest в let/var dest = p(args) (определение dest) и также в dest = p(args) (присваивание dest). Это достигается путем переписывания dest = p(args) в p'(args, dest) где p' является вариантом p, который возвращает void и получает скрытый изменяемый параметр, представляющий result.
Неформально:
proc p(): BigT = ... var x = p() x = p() # is roughly turned into: proc p(result: var BigT) = ... var x; p(x) p(x)
Пусть T будет типом возвращаемого значения p. NRVO применяется к T если sizeof(T) >= N (где N зависит от реализации), другими словами, это применяется к «большим» структурам.
Если p может вызвать исключение, NRVO применяется независимо. Это может привести к наблюдаемым различиям в поведении:
type
BigT = array[16, int]
proc p(raiseAt: int): BigT =
for i in 0..high(result):
if i == raiseAt: raise newException(ValueError, "interception")
result[i] = i
proc main =
var x: BigT
try:
x = p(8)
except ValueError:
doAssert x == [0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0]
main()
Однако, текущая реализация выдает предупреждение в этих случаях. Существует несколько способов справиться с этим предупреждением:
- Отключить предупреждение с помощью
{.push warning[ObservableStores]: off.}...{.pop.}. Тогда может потребоваться убедиться, чтоpподнимает исключение *до* выполнения любых записей вresult. - Можно использовать временную вспомогательную переменную, например, вместо
x = p(8)использоватьlet tmp = p(8); x = tmp.
Перегрузка оператора подстроки
Оператор подстроки [] для массивов/openarrays/последовательностей может быть перегружен.
Методы
Процедуры всегда используют статический диспетчер. Методы используют динамический диспетчер. Для работы динамического диспетчера с объектом он должен быть типом ссылки.
type
Expression = ref object of RootObj ## abstract base class for an expression
Literal = ref object of Expression
x: int
PlusExpr = ref object of Expression
a, b: Expression
method eval(e: Expression): int {.base.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method eval(e: Literal): int = return e.x
method eval(e: PlusExpr): int =
# watch out: relies on dynamic binding
result = eval(e.a) + eval(e.b)
proc newLit(x: int): Literal =
new(result)
result.x = x
proc newPlus(a, b: Expression): PlusExpr =
new(result)
result.a = a
result.b = b
echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))
В примере конструкторы newLit и newPlus являются процедурами, потому что они должны использовать статическую привязку, но eval является методом, потому что он требует динамической привязки.
Как видно из примера, базовые методы должны быть помечены атрибутом base. Атрибут base также служит напоминанием программисту о том, что базовый метод m используется в качестве основы для определения всех эффектов, которые может вызвать вызов m.
Примечание: Выполнение во время компиляции не поддерживается (еще) для методов.
Примечание: Начиная с Nim 0.20, универсальные методы устарели.
Многометоды
Примечание: Начиная с Nim 0.20, для использования многометодов необходимо явно передать --multimethods:on при компиляции.
В многометоде все параметры, имеющие тип объекта, используются для диспетчеризации:
type
Thing = ref object of RootObj
Unit = ref object of Thing
x: int
method collide(a, b: Thing) {.inline.} =
quit "to override!"
method collide(a: Thing, b: Unit) {.inline.} =
echo "1"
method collide(a: Unit, b: Thing) {.inline.} =
echo "2"
var a, b: Unit
new a
new b
collide(a, b) # output: 2 Запрещение динамического разрешения методов с помощью procCall
Динамическое разрешение методов может быть запрещено с помощью встроенного system.procCall. Это в какой-то степени сравнимо с ключевым словом super, которое предлагают традиционные языки ООП.
type
Thing = ref object of RootObj
Unit = ref object of Thing
x: int
method m(a: Thing) {.base.} =
echo "base"
method m(a: Unit) =
# Call the base method:
procCall m(Thing(a))
echo "1" Итераторы и оператор for
Оператор for — это абстрактный механизм для итерации по элементам контейнера. Для этого он полагается на итератор. Как и while инструкции, for инструкции открывают неявный блок, чтобы они могли завершаться инструкцией break.
Цикл for объявляет переменные итерации — их область действия распространяется до конца тела цикла. Типы переменных итерации выводятся по типу возврата итератора.
Итератор похож на процедуру, за исключением того, что его можно вызывать в контексте цикла for . Итераторы предоставляют способ указать итерацию по абстрактному типу. Ключевую роль в выполнении цикла for играет инструкция yield в вызываемом итераторе. Всякий раз, когда достигается инструкция yield, данные привязываются к переменным цикла for, и управление продолжается в теле цикла for. Локальные переменные и состояние выполнения итератора автоматически сохраняются между вызовами. Пример:
# this definition exists in the system module
iterator items*(a: string): char {.inline.} =
var i = 0
while i < len(a):
yield a[i]
inc(i)
for ch in items("hello world"): # `ch` is an iteration variable
echo ch
Компилятор генерирует код так, как будто программист написал это:
var i = 0 while i < len(a): var ch = a[i] echo ch inc(i)
Если итератор возвращает кортеж, может быть столько же переменных итерации, сколько компонентов в кортеже. Тип i-й переменной итерации — это тип i-го компонента. Другими словами, неявная распаковка кортежа в контексте цикла for поддерживается.
Неявные вызовы items/pairs
Если выражение цикла for e не обозначает итератор, а в цикле for ровно одна переменная, выражение цикла for переписывается в items(e); то есть вызывается итератор items:
for x in [1,2,3]: echo x
Если в цикле for ровно две переменные, вызывается итератор pairs.
Поиск символов идентификаторов items/pairs выполняется после этапа переписывания, чтобы учесть все перегрузки items/pairs.
Итераторы первого класса
В Nim существует два типа итераторов: встроенные и замыкания. Встроенный итератор — это итератор, который всегда встраивается компилятором, что приводит к нулевым накладным расходам на абстракцию, но может привести к значительному увеличению размера кода.
Внимание: тело цикла for над встроенным итератором встраивается в каждую инструкцию yield, появляющуюся в коде итератора, поэтому в идеале код следует переработать, чтобы содержать единственное yield, чтобы избежать раздувания кода.
Встроенные итераторы — это итераторы второго класса; Они могут передаваться в качестве параметров только другим механизмам инлайнинга, таким как шаблоны, макросы и другие встроенные итераторы.
В отличие от этого, итератор замыкания может передаваться более свободно:
iterator count0(): int {.closure.} =
yield 0
iterator count2(): int {.closure.} =
var x = 1
yield x
inc x
yield x
proc invoke(iter: iterator(): int {.closure.}) =
for x in iter(): echo x
invoke(count0)
invoke(count2)
Итераторы замыкания и встроенные итераторы имеют некоторые ограничения:
- Пока итератор замыкания не может выполняться во время компиляции.
-
returnразрешен в итераторе замыкания, но не в встроенном итераторе (но редко полезен) и завершает итерацию. - Ни встроенные, ни итераторы замыкания не могут быть (непосредственно)* рекурсивными.
- Ни встроенные, ни итераторы замыкания не имеют специальной переменной
result. - Итераторы замыкания не поддерживаются js-бэкендом.
(*) Итераторы замыкания могут быть корекурсивными с процедурой-фабрикой, что приводит к аналогичному синтаксису рекурсивному итератору. Более подробная информация следует.
Итераторы, которые не помечены явно как {.closure.} или {.inline.} по умолчанию являются встроенными, но это может измениться в будущих версиях реализации.
Тип iterator всегда имеет вызов closure неявно; следующий пример показывает, как использовать итераторы для реализации системы совместного выполнения задач:
# simple tasking:
type
Task = iterator (ticker: int)
iterator a1(ticker: int) {.closure.} =
echo "a1: A"
yield
echo "a1: B"
yield
echo "a1: C"
yield
echo "a1: D"
iterator a2(ticker: int) {.closure.} =
echo "a2: A"
yield
echo "a2: B"
yield
echo "a2: C"
proc runTasks(t: varargs[Task]) =
var ticker = 0
while true:
let x = t[ticker mod t.len]
if finished(x): break
x(ticker)
inc ticker
runTasks(a1, a2)
Встроенный system.finished может быть использован для определения того, завершил ли итератор свою работу; исключение не генерируется при попытке вызвать итератор, который уже завершил свою работу.
Обратите внимание, что использование system.finished небезопасно, так как оно возвращает только true одну итерацию после завершения итератора:
iterator mycount(a, b: int): int {.closure.} =
var x = a
while x <= b:
yield x
inc x
var c = mycount # instantiate the iterator
while not finished(c):
echo c(1, 3)
# Produces
1
2
3
0
Вместо этого необходимо использовать следующий код:
var c = mycount # instantiate the iterator while true: let value = c(1, 3) if finished(c): break # and discard 'value'! echo value
Полезно думать о том, что итератор фактически возвращает пару (value, done) и finished используется для доступа к скрытому полю done.
Итераторы замыкания являются возобновляемыми функциями, поэтому необходимо предоставлять аргументы каждому вызову. Чтобы обойти это ограничение, можно захватить параметры внешней процедуры-фабрики:
proc mycount(a, b: int): iterator (): int =
result = iterator (): int =
var x = a
while x <= b:
yield x
inc x
let foo = mycount(1, 4)
for f in foo():
echo f
Вызов можно сделать более похожим на встроенный итератор с помощью макроса for:
import macros
macro toItr(x: ForLoopStmt): untyped =
let expr = x[0]
let call = x[1][1] # Get foo out of toItr(foo)
let body = x[2]
result = quote do:
block:
let itr = `call`
for `expr` in itr():
`body`
for f in toItr(mycount(1, 4)): # using early `proc mycount`
echo f
Из-за полного участия механизма вызова функций бэкенда вызов итератора замыкания обычно имеет большую стоимость, чем вызов встроенного итератора. Добавление макроса-обёртки в месте вызова, как в данном случае, может служить полезным напоминанием.
Фабрика proc, как обычная процедура, может быть рекурсивной. Вышеприведённая макрос позволяет такой рекурсии выглядеть очень похожей на рекурсивный итератор. Например:
proc recCountDown(n: int): iterator(): int =
result = iterator(): int =
if n > 0:
yield n
for e in toItr(recCountDown(n - 1)):
yield e
for i in toItr(recCountDown(6)): # Emits: 6 5 4 3 2 1
echo i Конвертеры
Конвертер похож на обычную процедуру, за исключением того, что он улучшает отношение типа «неявное преобразование» (см. Отношение преобразования):
# bad style ahead: Nim is not C. converter toBool(x: int): bool = x != 0 if 4: echo "compiles"
Конвертер также может быть явно вызван для улучшения читаемости. Обратите внимание, что не поддерживается цепочная неявная конвертация: если существует конвертер из типа A в тип B и из типа B в тип C, не предоставляется неявное преобразование из A в C.
Разделы типов
Пример:
type # example demonstrating mutually recursive types
Node = ref object # an object managed by the garbage collector (ref)
le, ri: Node # left and right subtrees
sym: ref Sym # leaves contain a reference to a Sym
Sym = object # a symbol
name: string # the symbol's name
line: int # the line the symbol was declared in
code: Node # the symbol's abstract syntax tree
Раздел типа начинается со ключевого слова type. Он содержит несколько определений типов. Определение типа связывает тип с именем. Определения типов могут быть рекурсивными или даже взаимно рекурсивными. Взаимно рекурсивные типы возможны только в пределах одного раздела type. Номинальные типы, такие как objects или enums, могут быть определены только в разделе type.
Обработка исключений
Оператор try
Пример:
# read the first two lines of a text file that should contain numbers
# and tries to add them
var
f: File
if open(f, "numbers.txt"):
try:
var a = readLine(f)
var b = readLine(f)
echo "sum: " & $(parseInt(a) + parseInt(b))
except OverflowDefect:
echo "overflow!"
except ValueError:
echo "could not convert string to integer"
except IOError:
echo "IO error!"
except:
echo "Unknown exception!"
finally:
close(f)
Указанные после try операторы выполняются последовательно, пока не возникнет исключение e. Если тип исключения e совпадает с одним из перечисленных в блоке except, выполняются соответствующие операторы. Операторы, следующие за блоками except, называются обработчиками исключений.
Блок except без указания типа исключения выполняется, если возникло исключение, не перечисленное в остальных блоках. Он похож на блок else в операторах if.
Если есть блок finally, он всегда выполняется после обработчиков исключений.
Исключение поглощается в обработчике исключений. Однако обработчик исключений может породить другое исключение. Если исключение не обработано, оно распространяется по стеку вызовов. Это означает, что часто остальная часть процедуры — которая не находится внутри блока finally — не выполняется (если возникает исключение).
Выражение try
Try также может использоваться как выражение; тип ветви try тогда должен соответствовать типам ветвей except, но тип ветви finally всегда должен быть void;
from strutils import parseInt
let x = try: parseInt("133a")
except: -1
finally: echo "hi"
Для предотвращения путаницы в коде существует ограничение синтаксического разбора; если блок try следует за (, он должен быть записан в одну строку:
let x = (try: parseInt("133a") except: -1) Блоки except
Внутри блока except можно получить доступ к текущему исключению с помощью следующего синтаксиса:
try: # ... except IOError as e: # Now use "e" echo "I/O error: " & e.msg
В качестве альтернативы можно использовать getCurrentException для получения исключения, которое было вызвано:
try: # ... except IOError: let e = getCurrentException() # Now use "e"
Обратите внимание, что getCurrentException всегда возвращает тип ref Exception. Если требуется переменная соответствующего типа (в примере выше, IOError), необходимо явно преобразовать её:
try: # ... except IOError: let e = (ref IOError)(getCurrentException()) # "e" is now of the proper type
Однако это редко нужно. Наиболее распространённый случай — извлечение сообщения об ошибке из e, и для таких ситуаций достаточно использовать getCurrentExceptionMsg:
try: # ... except: echo getCurrentExceptionMsg()
Пользовательские исключения
Можно создавать пользовательские исключения. Пользовательское исключение — это пользовательский тип:
type LoadError* = object of Exception
Рекомендуется заканчивать имя пользовательского исключения Error.
Пользовательские исключения можно вызывать как любые другие, например:
raise newException(LoadError, "Failed to load data")
Оператор defer
Вместо оператора try finally можно использовать оператор defer, который избегает лексического вложения и предлагает большую гибкость в плане области видимости, как показано ниже.
Любые операторы, следующие за defer в текущем блоке, будут рассматриваться как находящиеся в неявном блоке try:
proc main =
var f = open("numbers.txt", fmWrite)
defer: close(f)
f.write "abc"
f.write "def"
Переписывается как:
proc main =
var f = open("numbers.txt")
try:
f.write "abc"
f.write "def"
finally:
close(f)
Когда defer находится в самой внешней области видимости шаблона/макроса, его область видимости распространяется на блок, из которого вызывается шаблон:
template safeOpenDefer(f, path) =
var f = open(path, fmWrite)
defer: close(f)
template safeOpenFinally(f, path, body) =
var f = open(path, fmWrite)
try: body # without `defer`, `body` must be specified as parameter
finally: close(f)
block:
safeOpenDefer(f, "/tmp/z01.txt")
f.write "abc"
block:
safeOpenFinally(f, "/tmp/z01.txt"):
f.write "abc" # adds a lexical scope
block:
var f = open("/tmp/z01.txt", fmWrite)
try:
f.write "abc" # adds a lexical scope
finally: close(f)
Операторы defer верхнего уровня не поддерживаются, поскольку неясно, к чему должен относиться такой оператор.
Оператор raise
Пример:
raise newException(IOError, "IO failed")
Помимо встроенных операций, таких как индексирование массива, выделение памяти и т. д., оператор raise — единственный способ вызвать исключение.
Если имя исключения не указано, текущее исключение перевызывается. Исключение ReraiseDefect вызывается, если нет исключения для перевызова. Следовательно, оператор raise всегда вызывает исключение.
Иерархия исключений
Дерево исключений определено в модуле system. Каждое исключение наследуется от system.Exception. Исключения, указывающие на ошибки программирования, наследуются от system.Defect (которое является подтипом Exception) и строго говоря не могут быть перехвачены, поскольку они также могут быть сопоставлены с операцией, которая завершает весь процесс. Если аварийные остановки преобразуются в исключения, эти исключения наследуются от Defect.
Исключения, указывающие на любые другие ошибки во время выполнения, которые могут быть перехвачены, наследуются от system.CatchableError (которое является подтипом Exception).
Импортированные исключения
Можно вызывать/перехватывать импортированные исключения C++. Типы, импортированные с помощью importcpp могут быть вызваны или перехвачены. Исключения вызываются по значению и перехватываются по ссылке. Пример:
type
CStdException {.importcpp: "std::exception", header: "<exception>", inheritable.} = object
## does not inherit from `RootObj`, so we use `inheritable` instead
CRuntimeError {.requiresInit, importcpp: "std::runtime_error", header: "<stdexcept>".} = object of CStdException
## `CRuntimeError` has no default constructor => `requiresInit`
proc what(s: CStdException): cstring {.importcpp: "((char *)#.what())".}
proc initRuntimeError(a: cstring): CRuntimeError {.importcpp: "std::runtime_error(@)", constructor.}
proc initStdException(): CStdException {.importcpp: "std::exception()", constructor.}
proc fn() =
let a = initRuntimeError("foo")
doAssert $a.what == "foo"
var b: cstring
try: raise initRuntimeError("foo2")
except CStdException as e:
doAssert e is CStdException
b = e.what()
doAssert $b == "foo2"
try: raise initStdException()
except CStdException: discard
try: raise initRuntimeError("foo3")
except CRuntimeError as e:
b = e.what()
except CStdException:
doAssert false
doAssert $b == "foo3"
fn()
Примечание: getCurrentException() и getCurrentExceptionMsg() недоступны для импортированных исключений из C++. Необходимо использовать синтаксис except ImportedException as x: и полагаться на функциональность объекта x для получения подробностей об исключении.
Система эффектов
Отслеживание исключений
Nim поддерживает отслеживание исключений. Предикат raises может быть использован для явного определения, какие исключения может вызвать процедура/итератор/метод/конвертер. Компилятор это проверяет:
proc p(what: bool) {.raises: [IOError, OSError].} =
if what: raise newException(IOError, "IO")
else: raise newException(OSError, "OS")
Пустой список raises (raises: []) означает, что исключения вызывать нельзя:
proc p(): bool {.raises: [].} =
try:
unsafeCall()
result = true
except:
result = false
Список raises также может быть присоединён к типу процедуры. Это влияет на совместимость типов:
type
Callback = proc (s: string) {.raises: [IOError].}
var
c: Callback
proc p(x: string) =
raise newException(OSError, "OS")
c = p # type error
Для процедуры p компилятор использует правила вывода для определения набора потенциально вызываемых исключений; алгоритм работает с графом вызовов p:
- Каждый косвенный вызов через некоторый тип процедуры
Tпредполагается, что вызываетsystem.Exception(базовый тип иерархии исключений) и, следовательно, любое исключение, если уTнет явного спискаraises. Однако, если вызов имеет видf(...), гдеf— параметр анализируемой процедуры, он игнорируется. Вызов оптимистически предполагается без эффектов. Правило 2 компенсирует этот случай. - Каждое выражение некоторого типа процедуры внутри вызова, которое само не является вызовом (и не nil), предполагается вызываться косвенно, и поэтому его список raises добавляется в список raises
p. - Каждый вызов процедуры
qс неизвестным телом (из-за объявления вперёд или директивыimportc) предполагается вызывающимsystem.Exception, если уqнет явного спискаraises. - Каждый вызов метода
mпредполагается вызывающимsystem.Exception, если уmнет явного спискаraises. - Для всех остальных вызовов анализ может определить точный список
raises. - Для определения списка
raises, операторыraiseиtryпроцедурыpучитываются.
Правила 1-2 гарантируют, что следующее работает:
proc noRaise(x: proc()) {.raises: [].} =
# unknown call that might raise anything, but valid:
x()
proc doRaise() {.raises: [IOError].} =
raise newException(IOError, "IO")
proc use() {.raises: [].} =
# doesn't compile! Can raise IOError!
noRaise(doRaise)
Таким образом, во многих случаях обратный вызов не заставляет компилятор быть чрезмерно консервативным в анализе эффектов.
Исключения, наследующие от system.Defect не отслеживаются механизмом отслеживания исключений .raises: []. Это более соответствует встроенным операциям. Следующий код допустим:
proc mydiv(a, b): int {.raises: [].} =
a div b # can raise an DivByZeroDefect
И так же:
proc mydiv(a, b): int {.raises: [].} =
if b == 0: raise newException(DivByZeroDefect, "division by zero")
else: result = a div b
Причина в том, что DivByZeroDefect наследуется от Defect и с --panics:on ошибки становятся невосстанавливаемыми ошибками. (Начиная с версии 1.4 языка).
Отслеживание тегов
Отслеживание исключений является частью системы эффектов Nim. Вызов исключения — это эффект. Также могут быть определены другие эффекты. Пользовательский эффект — это средство для метки процедуры и выполнения проверок по этой метке:
type IO = object ## input/output effect
proc readLine(): string {.tags: [IO].} = discard
proc no_IO_please() {.tags: [].} =
# the compiler prevents this:
let x = readLine()
Метка должна быть именем типа. Список tags — как и список raises — также может быть присоединен к типу процедуры. Это влияет на совместимость типов.
Вывод для отслеживания тегов аналогичен выводу для отслеживания исключений.
Директива effects
Директива effects разработана для помощи программисту в анализе эффектов. Это оператор, который заставляет компилятор выводить все выведенные эффекты до позиции effects;
proc p(what: bool) =
if what:
raise newException(IOError, "IO")
{.effects.}
else:
raise newException(OSError, "OS")
Компилятор выводит сообщение-подсказку о том, что может быть вызвано IOError. OSError не указано, так как оно не может быть вызвано в ветви, в которой появляется директива effects.
Обобщения
Обобщения — это способ Nim параметризовать процедуры, итераторы или типы с параметрами типов. В зависимости от контекста, скобки используются либо для введения параметров типа, либо для инстанцирования обобщенной процедуры, итератора или типа.
Следующий пример показывает, как можно смоделировать обобщенное двоичное дерево:
type
BinaryTree*[T] = ref object # BinaryTree is a generic type with
# generic param ``T``
le, ri: BinaryTree[T] # left and right subtrees; may be nil
data: T # the data stored in a node
proc newNode*[T](data: T): BinaryTree[T] =
# constructor for a node
result = BinaryTree[T](le: nil, ri: nil, data: data)
proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
# insert a node into the tree
if root == nil:
root = n
else:
var it = root
while it != nil:
# compare the data items; uses the generic ``cmp`` proc
# that works for any type that has a ``==`` and ``<`` operator
var c = cmp(it.data, n.data)
if c < 0:
if it.le == nil:
it.le = n
return
it = it.le
else:
if it.ri == nil:
it.ri = n
return
it = it.ri
proc add*[T](root: var BinaryTree[T], data: T) =
# convenience proc:
add(root, newNode(data))
iterator preorder*[T](root: BinaryTree[T]): T =
# Preorder traversal of a binary tree.
# This uses an explicit stack (which is more efficient than
# a recursive iterator factory).
var stack: seq[BinaryTree[T]] = @[root]
while stack.len > 0:
var n = stack.pop()
while n != nil:
yield n.data
add(stack, n.ri) # push right subtree onto the stack
n = n.le # and follow the left pointer
var
root: BinaryTree[string] # instantiate a BinaryTree with ``string``
add(root, newNode("hello")) # instantiates ``newNode`` and ``add``
add(root, "world") # instantiates the second ``add`` proc
for str in preorder(root):
stdout.writeLine(str)
T называется параметром обобщенного типа или переменной типа.
Оператор is
Оператор is оценивается во время семантического анализа для проверки эквивалентности типов. Поэтому он очень полезен для специализации типов в генерическом коде:
type
Table[Key, Value] = object
keys: seq[Key]
values: seq[Value]
when not (Key is string): # empty value for strings used for optimization
deletedKeys: seq[bool] Типы классов
Тип класса — это специальный псевдотип, который может использоваться для сопоставления с типами в контексте разрешения перегрузки или оператора is. Nim поддерживает следующие встроенные типы классов:
| тип класса | сопоставляется с |
|---|---|
object |
любой тип объекта |
tuple |
любой тип кортежа |
enum |
любая перечисления |
proc |
любой тип proc |
ref |
любой тип ref |
ptr |
любой тип ptr |
var |
любой тип var |
distinct |
любой отдельный тип |
array |
любой тип массива |
set |
любой тип множества |
seq |
любой тип seq |
auto |
любой тип |
any |
отдельный auto (см. ниже) |
Кроме того, каждый генерический тип автоматически создает тип класса с тем же именем, который будет соответствовать любому экземпляру генерического типа.
Типы классов можно комбинировать, используя стандартные булевы операторы, чтобы сформировать более сложные типы классов:
# create a type class that will match all tuple and object types
type RecordType = tuple or object
proc printFields[T: RecordType](rec: T) =
for key, value in fieldPairs(rec):
echo key, " = ", value
Хотя синтаксис типов классов похож на синтаксис ADTs/алгебраических типов данных в языках типа ML, следует понимать, что типы классов представляют собой статические ограничения, которые должны быть применены при создании экземпляров типов. Типы классов — это не типы сами по себе, а система предоставления общих «проверок», которые в конечном итоге разрешаются в некотором единственном типе. В отличие от вариантов объектов или методов, типы классов не позволяют динамически менять типы во время выполнения.
Например, следующее не будет компилироваться:
type TypeClass = int | string var foo: TypeClass = 2 # foo's type is resolved to an int here foo = "this will fail" # error here, because foo is an int
Nim позволяет указывать типы классов и обычные типы как ограничения типов параметра генерического типа:
proc onlyIntOrString[T: int|string](x, y: T) = discard
onlyIntOrString(450, 616) # valid
onlyIntOrString(5.0, 0.0) # type mismatch
onlyIntOrString("xy", 50) # invalid as 'T' cannot be both at the same time Неявные генерики
Тип класса можно использовать непосредственно в качестве типа параметра.
# create a type class that will match all tuple and object types
type RecordType = tuple or object
proc printFields(rec: RecordType) =
for key, value in fieldPairs(rec):
echo key, " = ", value
Процедуры, использующие типы классов таким образом, считаются неявными генериками. Они будут создаваться один раз для каждой уникальной комбинации типов параметров, используемых в программе.
По умолчанию при разрешении перегрузки каждый именованный тип класса будет привязываться ровно к одному конкретному типу. Мы называем такие типы классов типами, привязываемыми один раз. Вот пример, взятый непосредственно из модуля системы, чтобы проиллюстрировать это:
proc `==`*(x, y: tuple): bool =
## requires `x` and `y` to be of the same tuple type
## generic ``==`` operator for tuples that is lifted from the components
## of `x` and `y`.
result = true
for a, b in fields(x, y):
if a != b: result = false
В качестве альтернативы, модификатор типа distinct может быть применен к типу класса, чтобы позволить каждому параметру, соответствующему типу класса, привязываться к другому типу. Такие типы классов называются типами, привязываемыми многократно.
Процедуры, написанные в неявном генерическом стиле, часто должны ссылаться на параметры типа соответствующего генерического типа. Их можно легко получить с помощью синтаксиса точки:
type Matrix[T, Rows, Columns] = object ... proc `[]`(m: Matrix, row, col: int): Matrix.T = m.data[col * high(Matrix.Columns) + row]
Вот больше примеров, которые иллюстрируют неявные генерики:
proc p(t: Table; k: Table.Key): Table.Value # is roughly the same as: proc p[Key, Value](t: Table[Key, Value]; k: Key): Value
proc p(a: Table, b: Table) # is roughly the same as: proc p[Key, Value](a, b: Table[Key, Value])
proc p(a: Table, b: distinct Table) # is roughly the same as: proc p[Key, Value, KeyB, ValueB](a: Table[Key, Value], b: Table[KeyB, ValueB])
typedesc в качестве типа параметра также вводит неявный генерик. typedesc имеет свой набор правил:
proc p(a: typedesc) # is roughly the same as: proc p[T](a: typedesc[T])
typedesc — это "многократный" тип класса:
proc p(a, b: typedesc) # is roughly the same as: proc p[T, T2](a: typedesc[T], b: typedesc[T2])
Параметр типа typedesc сам по себе может использоваться в качестве типа. Если он используется в качестве типа, это базовый тип. (Другими словами, один уровень «typedesc»-ности удаляется):
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.
Ограничения на вывод генерических типов
Типы var T и typedesc[T] нельзя вывести в генерическом создании экземпляра. Следующее не допускается:
proc g[T](f: proc(x: T); x: T) = f(x) proc c(y: int) = echo y proc v(y: var int) = y += 100 var i: int # allowed: infers 'T' to be of type 'int' g(c, 42) # not valid: 'T' is not inferred to be of type 'var int' g(v, i) # also not allowed: explicit instantiation via 'var int' g[var int](v, i)
Поиск символов в генериках
Открытые и закрытые символы
Правила привязки символов в генериках немного сложные: существуют «открытые» и «закрытые» символы. «Закрытый» символ не может быть повторно привязан в контексте создания экземпляра, а «открытый» — может. По умолчанию перегруженные символы открыты, а все остальные — закрыты.
Открытые символы ищутся в двух разных контекстах: рассматриваются как контекст определения, так и контекст создания экземпляра:
type
Index = distinct int
proc `==` (a, b: Index): bool {.borrow.}
var a = (0, 0.Index)
var b = (0, 0.Index)
echo a == b # works!
В примере генерик == для кортежей (как определено в модуле системы) использует операторы == компонентов кортежа. Однако == для типа Index определено после == для кортежей; тем не менее, пример компилируется, поскольку создание экземпляра также учитывает текущие определенные символы.
Декларация mixin
Символ можно принудительно открыть с помощью декларации mixin:
proc create*[T](): ref T = # there is no overloaded 'init' here, so we need to state that it's an # open symbol explicitly: mixin init new result init result
Декларации mixin имеют смысл только в шаблонах и генериках.
Декларация bind
Декларация bind является обратной стороной декларации mixin. Она может использоваться для явного объявления идентификаторов, которые должны быть привязаны на ранней стадии (т.е. идентификаторы должны быть найдены в области видимости определения шаблона/генерика):
# Module A var lastId = 0 template genId*: untyped = bind lastId inc(lastId) lastId
# Module B import A echo genId()
Но декларация bind редко бывает полезной, потому что привязка символов из области видимости определения — это значение по умолчанию.
Декларации bind имеют смысл только в шаблонах и генериках.
Делегирующие декларации bind
Следующий пример иллюстрирует проблему, которая может возникнуть, когда генерические экземпляры пересекают несколько разных модулей:
# module A proc genericA*[T](x: T) = mixin init init(x)
import C # module B proc genericB*[T](x: T) = # Without the `bind init` statement C's init proc is # not available when `genericB` is instantiated: bind init genericA(x)
# module C type O = object proc init*(x: var O) = discard
# module main import B, C genericB O()
В модуле B есть процедура init из модуля C в его области видимости, которая не учитывается при создании экземпляра genericB, что приводит к созданию экземпляра genericA. Решением является forward этих символов с помощью декларации bind внутри genericB.
Шаблоны
Шаблон — это простая форма макроса: это механизм простой подстановки, который работает с абстрактными синтаксическими деревьями Nim. Он обрабатывается на семантической стадии компилятора.
Синтаксис вызова шаблона такой же, как вызов процедуры.
Пример:
template `!=` (a, b: untyped): untyped = # this definition exists in the System module not (a == b) assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))
Операторы !=, >, >=, in, notin, isnot на самом деле являются шаблонами:
a > b преобразуется в b < a.a in b преобразуется в contains(b, a).notin и isnot имеют очевидные значения.
«Типы» шаблонов могут быть символами untyped, typed или typedesc. Это «метатипы», они могут использоваться только в определенных контекстах. Также могут использоваться обычные типы; это подразумевает, что ожидаются выражения typed.
Типизированные и нетипизированные параметры
Параметр untyped означает, что поиск символов и разрешение типов не выполняются до передачи выражения в шаблон. Это означает, что, например, необъявленные идентификаторы могут быть переданы в шаблон:
template declareInt(x: untyped) = var x: int declareInt(x) # valid x = 3
template declareInt(x: typed) = var x: int declareInt(x) # invalid, because x has not been declared and so it has no type
Шаблон, где каждый параметр untyped называется непосредственным шаблоном. По историческим причинам шаблоны могут быть явно аннотированы с помощью псевдонима immediate , а затем эти шаблоны не участвуют в разрешении перегрузки, и типы параметров игнорируются компилятором. Явные шаблоны непосредственного использования теперь устарели.
Примечание: По историческим причинам stmt был алиасом для typed, а expr был алиасом для untyped, но они удалены.
Передача блока кода шаблону
Можно передать блок операторов как последний аргумент шаблону, используя специальный синтаксис ::
template withFile(f, fn, mode, actions: untyped): untyped =
var f: File
if open(f, fn, mode):
try:
actions
finally:
close(f)
else:
quit("cannot open: " & fn)
withFile(txt, "ttempl3.txt", fmWrite): # special colon
txt.writeLine("line 1")
txt.writeLine("line 2")
В примере два оператора writeLine привязаны к параметру actions.
Обычно для передачи блока кода шаблону параметр, принимающий блок, должен иметь тип untyped. Поскольку поиск символов отложен до момента создания экземпляра шаблона:
template t(body: typed) =
proc p = echo "hey"
block:
body
t:
p() # fails with 'undeclared identifier: p'
Вышеприведенный код завершается с сообщением об ошибке, что p не объявлено. Причина в том, что тело p() проверяется на типы до передачи в параметр body, а проверка типа в Nim подразумевает поиск символов. Тот же код работает с untyped, так как передаваемое тело не должно проверяться на типы:
template t(body: untyped) =
proc p = echo "hey"
block:
body
t:
p() # compiles Аргументы с переменным числом нетипизированных параметров
В дополнение к метатипу untyped, который предотвращает проверку типов, также существует varargs[untyped], чтобы даже число параметров не было фиксированным:
template hideIdentifiers(x: varargs[untyped]) = discard hideIdentifiers(undeclared1, undeclared2)
Однако, поскольку шаблон не может перебирать аргументы с переменным числом, эта функция в целом намного полезнее для макросов.
Привязка символов в шаблонах
Шаблон является гигиеническим макросом и поэтому открывает новую область. Большинство символов привязаны из области определения шаблона:
# Module A var lastId = 0 template genId*: untyped = inc(lastId) lastId
# Module B import A echo genId() # Works as 'lastId' has been bound in 'genId's defining scope
Как и в генериках, привязку символов можно контролировать с помощью деклараций mixin или bind.
Конструирование идентификаторов
В шаблонах идентификаторы можно создавать с помощью обозначения обратных кавычек:
template typedef(name: untyped, typ: typedesc) =
type
`T name`* {.inject.} = typ
`P name`* {.inject.} = ref `T name`
typedef(myint, int)
var x: PMyInt
В примере name создается экземпляр с myint, поэтому `T имя` становится Tmyint.
Правила поиска для параметров шаблона
Параметр p в шаблоне даже подставляется в выражение x.p. Таким образом, аргументы шаблона могут использоваться как имена полей, и глобальный символ может быть скрыт тем же именем аргумента даже при полном квалифицировании:
# module 'm'
type
Lev = enum
levA, levB
var abclev = levB
template tstLev(abclev: Lev) =
echo abclev, " ", m.abclev
tstLev(levA)
# produces: 'levA levA'
Но глобальную переменную можно корректно получить с помощью оператора bind:
# module 'm'
type
Lev = enum
levA, levB
var abclev = levB
template tstLev(abclev: Lev) =
bind m.abclev
echo abclev, " ", m.abclev
tstLev(levA)
# produces: 'levA levB' Гигиена в шаблонах
По умолчанию шаблоны являются гигиеничными: локальные идентификаторы, объявленные в шаблоне, недоступны в контексте инстанциации:
template newException*(exceptn: typedesc, message: string): untyped =
var
e: ref exceptn # e is implicitly gensym'ed here
new(e)
e.msg = message
e
# so this works:
let e = "message"
raise newException(IoError, e)
Доступность символа, объявленного в шаблоне, в контексте инстанциации управляется директивами inject и gensym: символы, помеченные gensym, не доступны, а символы, помеченные inject, доступны.
Значение по умолчанию для символов сущностей type, var, let и const — gensym, а для proc, iterator, converter, template, macro — inject. Однако, если имя сущности передаётся в качестве параметра шаблона, оно становится доступным символом (inject):
template withFile(f, fn, mode: untyped, actions: untyped): untyped =
block:
var f: File # since 'f' is a template param, it's injected implicitly
...
withFile(txt, "ttempl3.txt", fmWrite):
txt.writeLine("line 1")
txt.writeLine("line 2")
Директивы inject и gensym являются аннотациями второго класса; они не имеют семантики вне определения шаблона и не могут быть обобщены:
{.pragma myInject: inject.}
template t() =
var x {.myInject.}: int # does NOT work
Чтобы отключить гигиену в шаблоне, можно использовать директиву dirty для шаблона. inject и gensym не имеют эффекта в dirty шаблонах.
gensym символы не могут быть использованы как field в синтаксисе x.field. Также они не могут быть использованы в синтаксических конструкциях ObjectConstruction(field: value) и namedParameterCall(field = value).
Причина в том, что код вида
type
T = object
f: int
template tmp(x: T) =
let f = 34
echo x.f, T(f: 4)
должен работать как ожидается.
Однако это означает, что синтаксис вызова метода недоступен для gensym символов:
template tmp(x) =
type
T {.gensym.} = int
echo x.T # invalid: instead use: 'echo T(x)'.
tmp(12)
Примечание: Компилятор Nim до версии 1 был более снисходителен к этому требованию. Используйте переключатель --useVersion:0.19 для переходного периода.
Ограничения синтаксиса вызова метода
Выражение x в x.f нужно семантически проверить (то есть выполнить поиск символа и проверку типа), прежде чем можно будет решить, что его нужно переписать на f(x). Поэтому синтаксис с точкой имеет некоторые ограничения при вызове шаблонов/макросов:
template declareVar(name: untyped) =
const name {.inject.} = 45
# Doesn't compile:
unknownIdentifier.declareVar
Другой распространённый пример:
from sequtils import toSeq iterator something: string = yield "Hello" yield "World" var info = something().toSeq
Проблема здесь в том, что компилятор уже решил, что something() как итератор не вызываем в данном контексте, прежде чем toSeq получит возможность преобразовать его в последовательность.
Также невозможно использовать полностью квалифицированные идентификаторы с символом модуля в синтаксисе вызова метода. Порядок связывания оператора точки с символами это запрещает.
import sequtils var myItems = @[1,3,3,7] let N1 = count(myItems, 3) # OK let N2 = sequtils.count(myItems, 3) # fully qualified, OK let N3 = myItems.count(3) # OK let N4 = myItems.sequtils.count(3) # illegal, `myItems.sequtils` can't be resolved
Это означает, что если по какой-то причине процедуре необходимо разыменование через имя модуля, вызов необходимо записать в синтаксисе вызова функции.
Макросы
Макрос — это специальная функция, которая выполняется во время компиляции. Обычно входными данными для макроса является абстрактное синтаксическое дерево (AST) кода, который ему передаётся. Макрос может затем выполнить преобразования над ним и вернуть преобразованное AST. Это можно использовать для добавления пользовательских функций языка и реализации языков предметной области.
Вызов макроса — случай, когда семантический анализ не происходит полностью сверху вниз и слева направо. Вместо этого семантический анализ происходит как минимум дважды:
- Семантический анализ распознаёт и разрешает вызов макроса.
- Компилятор выполняет тело макроса (которое может вызывать другие процедуры).
- Он заменяет AST вызова макроса на AST, возвращённое макросом.
- Он повторяет семантический анализ этого фрагмента кода.
- Если AST, возвращённый макросом, содержит другие вызовы макросов, этот процесс повторяется.
Хотя макросы позволяют выполнять сложные преобразования кода во время компиляции, они не могут изменить синтаксис Nim.
Пример отладки
Следующий пример реализует мощную команду debug , принимающую переменное количество аргументов:
# to work with Nim syntax trees, we need an API that is defined in the
# ``macros`` module:
import macros
macro debug(args: varargs[untyped]): untyped =
# `args` is a collection of `NimNode` values that each contain the
# AST for an argument of the macro. A macro always has to
# return a `NimNode`. A node of kind `nnkStmtList` is suitable for
# this use case.
result = nnkStmtList.newTree()
# iterate over any argument that is passed to this macro:
for n in args:
# add a call to the statement list that writes the expression;
# `toStrLit` converts an AST to its string representation:
result.add newCall("write", newIdentNode("stdout"), newLit(n.repr))
# add a call to the statement list that writes ": "
result.add newCall("write", newIdentNode("stdout"), newLit(": "))
# add a call to the statement list that writes the expressions value:
result.add newCall("writeLine", newIdentNode("stdout"), n)
var
a: array[0..10, int]
x = "some string"
a[0] = 42
a[1] = 45
debug(a[0], a[1], x)
Вызов макроса расширяется до:
write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") write(stdout, ": ") writeLine(stdout, x)
Аргументы, передаваемые параметру varargs, заключаются в выражении конструктора массива. Вот почему debug перебирает всех потомков n.
BindSym
Вышеупомянутый макрос debug опирается на тот факт, что write, writeLine и stdout объявлены в модуле системы и, таким образом, видны в контексте инстанциации. Существует способ использования связанных идентификаторов (также известных как символы) вместо использования несвязанных идентификаторов. Для этого можно использовать встроенную функцию bindSym:
import macros
macro debug(n: varargs[typed]): untyped =
result = newNimNode(nnkStmtList, n)
for x in n:
# we can bind symbols in scope via 'bindSym':
add(result, newCall(bindSym"write", bindSym"stdout", toStrLit(x)))
add(result, newCall(bindSym"write", bindSym"stdout", newStrLitNode(": ")))
add(result, newCall(bindSym"writeLine", bindSym"stdout", x))
var
a: array[0..10, int]
x = "some string"
a[0] = 42
a[1] = 45
debug(a[0], a[1], x)
Вызов макроса расширяется до:
write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") write(stdout, ": ") writeLine(stdout, x)
Однако символы write, writeLine и stdout уже связаны и не ищутся заново. Как показывает пример, bindSym работает со перегруженными символами неявно.
Макрос Case-Of
В Nim можно иметь макрос с синтаксисом выражения case-of, за исключением того, что все ветви передаются в реализацию макроса и обрабатываются им. Далее реализация макроса преобразует ветви of в допустимое утверждение Nim. Следующий пример демонстрирует, как эта функция может использоваться для лексического анализатора.
import macros macro case_token(args: varargs[untyped]): untyped = echo args.treeRepr # creates a lexical analyzer from regular expressions # ... (implementation is an exercise for the reader ;-) discard case_token: # this colon tells the parser it is a macro statement of r"[A-Za-z_]+[A-Za-z_0-9]*": return tkIdentifier of r"0-9+": return tkInteger of r"[\+\-\*\?]+": return tkOperator else: return tkUnknown
Рекомендация по стилю: Для повышения читаемости кода лучше всего использовать наименее мощную конструкцию языка, которая всё ещё достаточна. Таким образом, «чек-лист» выглядит так:
- Использовать обычную функцию/итератор, если возможно.
- В противном случае: использовать обобщённую функцию/итератор, если возможно.
- В противном случае: использовать шаблон, если возможно.
- В противном случае: использовать макрос.
Макрос цикла For
Макрос, принимающий в качестве единственного параметра выражение специального типа system.ForLoopStmt, может переписать весь цикл for:
import macros
macro enumerate(x: ForLoopStmt): untyped =
expectKind x, nnkForStmt
# check if the starting count is specified:
var countStart = if x[^2].len == 2: newLit(0) else: x[^2][1]
result = newStmtList()
# we strip off the first for loop variable and use it as an integer counter:
result.add newVarStmt(x[0], countStart)
var body = x[^1]
if body.kind != nnkStmtList:
body = newTree(nnkStmtList, body)
body.add newCall(bindSym"inc", x[0])
var newFor = newTree(nnkForStmt)
for i in 1..x.len-3:
newFor.add x[i]
# transform enumerate(X) to 'X'
newFor.add x[^2][^1]
newFor.add body
result.add newFor
# now wrap the whole macro in a block to create a new scope
result = quote do:
block: `result`
for a, b in enumerate(items([1, 2, 3])):
echo a, " ", b
# without wrapping the macro in a block, we'd need to choose different
# names for `a` and `b` here to avoid redefinition errors
for a, b in enumerate(10, [1, 2, 3, 5]):
echo a, " ", b Специальные типы
static[T]
Как следует из их названия, статические параметры должны быть константными выражениями:
proc precompiledRegex(pattern: static string): RegEx =
var res {.global.} = re(pattern)
return res
precompiledRegex("/d+") # Replaces the call with a precompiled
# regex, stored in a global variable
precompiledRegex(paramStr(1)) # Error, command-line options
# are not constant expressions
Для целей генерации кода все статические параметры обрабатываются как обобщённые параметры — процедура будет компилироваться отдельно для каждого уникального предоставленного значения (или комбинации значений).
Статические параметры также могут появляться в сигнатурах обобщённых типов:
type
Matrix[M,N: static int; T: Number] = array[0..(M*N - 1), T]
# Note how `Number` is just a type constraint here, while
# `static int` requires us to supply an int value
AffineTransform2D[T] = Matrix[3, 3, T]
AffineTransform3D[T] = Matrix[4, 4, T]
var m1: AffineTransform3D[float] # OK
var m2: AffineTransform2D[string] # Error, `string` is not a `Number`
Обратите внимание, что static T — это просто синтаксический удобный способ для базового обобщённого типа static[T]. Параметр типа можно опустить, чтобы получить класс типов всех константных выражений. Более специфический класс типов можно создать, инстанцируя static с другим классом типов.
Можно принудительно вычислить выражение во время компиляции как константное выражение, применив к нему соответствующий тип static:
import math echo static(fac(5)), " ", static[bool](16.isPowerOfTwo)
Компилятор сообщит об ошибках в случае невозможности вычисления выражения или возможной несовместимости типов.
typedesc[T]
Во многих контекстах Nim обрабатывает имена типов как обычные значения. Эти значения существуют только во время компиляции, но поскольку все значения должны иметь тип, typedesc считается их специальным типом.
typedesc действует как обобщённый тип. Например, тип символа int — typedesc[int]. Как и с обычными обобщёнными типами, когда обобщённый параметр опущен, typedesc обозначает класс типов всех типов. В качестве синтаксического удобства можно также использовать typedesc в качестве модификатора.
Процедуры со параметрами typedesc считаются неявно обобщёнными. Они будут инстанцироваться для каждой уникальной комбинации предоставленных типов, и внутри тела процедуры имя каждого параметра будет ссылаться на связанный конкретный тип:
proc new(T: typedesc): ref T = echo "allocating ", T.name new(result) var n = Node.new var tree = new(BinaryTree[int])
При наличии нескольких параметров типа они будут свободно связываться с различными типами. Чтобы принудительно осуществить однократное связывание, можно использовать явный обобщённый параметр:
proc acceptOnlyTypePairs[T, U](A, B: typedesc[T]; C, D: typedesc[U])
После связывания параметры типа могут появляться в остальной части сигнатуры процедуры:
template declareVariableWithType(T: typedesc, value: T) = var x: T = value declareVariableWithType int, 42
Разрешение перегрузки можно дополнительно контролировать, ограничивая набор типов, которые будут соответствовать параметру типа. Это работает на практике, привязывая атрибуты к типам с помощью шаблонов. Ограничение может быть конкретным типом или классом типов.
template maxval(T: typedesc[int]): int = high(int) template maxval(T: typedesc[float]): float = Inf var i = int.maxval var f = float.maxval when false: var s = string.maxval # error, maxval is not implemented for string template isNumber(t: typedesc[object]): string = "Don't think so." template isNumber(t: typedesc[SomeInteger]): string = "Yes!" template isNumber(t: typedesc[SomeFloat]): string = "Maybe, could be NaN." echo "is int a number? ", isNumber(int) echo "is float a number? ", isNumber(float) echo "is RootObj a number? ", isNumber(RootObj)
Передача typedesc почти идентична, за исключением того, что макрос не инстанцируется обобщённо. Выражение типа просто передаётся как NimNode макросу, как и всё остальное.
import macros macro forwardType(arg: typedesc): typedesc = # ``arg`` is of type ``NimNode`` let tmp: NimNode = arg result = tmp var tmp: forwardType(int)
Оператор typeof
Примечание: typeof(x) по историческим причинам также может быть записано как type(x), но type(x) не рекомендуется.
Тип данного выражения можно получить, создав значение типа typeof из него (во многих других языках это известно как оператор typeof):
var x = 0 var y: typeof(x) # y has type int
Если typeof используется для определения типа результата вызова функции/итератора/конвертера c(X) (где X представляет собой возможно пустой список аргументов), интерпретация, где c — итератор, предпочтительнее, но это поведение можно изменить, передав typeOfProc как второй аргумент функции typeof:
iterator split(s: string): string = discard
proc split(s: string): seq[string] = discard
# since an iterator is the preferred interpretation, `y` has the type ``string``:
assert typeof("a b c".split) is string
assert typeof("a b c".split, typeOfProc) is seq[string] Модули
Nim поддерживает разделение программы на части с помощью концепции модулей. Каждый модуль должен находиться в своём файле и имеет свой пространство имён. Модули обеспечивают сокрытие информации и раздельную компиляцию. Модуль может получить доступ к символам другого модуля с помощью оператора import. Рекурсивные зависимости модулей разрешены, но немного тонкие. Только символы верхнего уровня, помеченные звёздочкой (* ), экспортируются. Валидное имя модуля может быть только допустимым идентификатором Nim (и, следовательно, его имя файла — identifier.nim).
Алгоритм компиляции модулей:
- компилировать весь модуль как обычно, рекурсивно следуя операторам import
- если есть цикл, импортировать только уже обработанные символы (которые экспортируются); если встречается неизвестный идентификатор, прервать
Это лучше всего проиллюстрировано примером:
# Module A type T1* = int # Module A exports the type ``T1`` import B # the compiler starts parsing B proc main() = var i = p(3) # works because B has been parsed completely here main()
# Module B
import A # A is not parsed here! Only the already known symbols
# of A are imported.
proc p*(x: A.T1): A.T1 =
# this works because the compiler has already
# added T1 to A's interface symbol table
result = x + 1 Оператор import
После оператора import может следовать список имён модулей или одно имя модуля, за которым следует список except для предотвращения импорта некоторых символов:
import strutils except `%`, toUpperAscii # doesn't work then: echo "$1" % "abc".toUpperAscii
Не проверяется, что список except действительно экспортируется из модуля. Эта функция позволяет нам компилировать против более старой версии модуля, не экспортирующей эти идентификаторы.
Оператор import разрешён только на верхнем уровне.
Оператор include
Оператор include выполняет существенно иную операцию, чем импорт модуля: он просто включает содержимое файла. Оператор include полезен для разделения большого модуля на несколько файлов:
include fileA, fileB, fileC
Оператор include может использоваться вне верхнего уровня, например:
# Module A echo "Hello World!"
# Module B proc main() = include A main() # => Hello World!
Имена модулей в импортах
Псевдоним модуля можно ввести с помощью ключевого слова as:
import strutils as su, sequtils as qu
echo su.format("$1", "lalelu")
Исходное имя модуля тогда недоступно. Обозначения path/to/module или "path/to/module" могут использоваться для ссылки на модуль в подкаталогах:
import lib/pure/os, "lib/pure/times"
Обратите внимание, что имя модуля всё ещё strutils и не lib/pure/strutils, поэтому сделать нельзя:
import lib/pure/strutils
echo lib/pure/strutils.toUpperAscii("abc")
Аналогично, следующее не имеет смысла, так как имя уже strutils:
import lib/pure/strutils as strutils
Коллективный импорт из каталога
Синтаксис import dir / [moduleA, moduleB] может использоваться для импорта нескольких модулей из одного каталога.
Имена путей синтаксически являются либо идентификаторами Nim, либо строковыми литералами. Если имя пути не является допустимым идентификатором Nim, оно должно быть строковым литералом:
import "gfx/3d/somemodule" # in quotes because '3d' is not a valid Nim identifier
Псевдо пути import/include
Каталог также может быть так называемым «псевдокаталогом». Они могут использоваться для избежания неоднозначности, когда существует несколько модулей с одинаковым путём.
Существует два псевдокаталога:
-
std: Псевдокаталогstd— это абстрактное расположение стандартной библиотеки Nim. Например, синтаксисimport std / strutilsиспользуется для однозначной ссылки на модульstrutilsстандартной библиотеки. -
pkg: Псевдокаталогpkgиспользуется для однозначной ссылки на пакет Nimble. Однако, для технических деталей, лежащих за пределами этой документации, его семантика заключается в следующем: Используйте путь поиска для поиска имени модуля, но игнорируйте расположения стандартной библиотеки. Другими словами, это противоположностьstd.
Оператор from import
После оператора from следует имя модуля, за которым следует import для перечисления символов, которые нужно использовать без явного полного квалифицирования:
from strutils import `%`
echo "$1" % "abc"
# always possible: full qualification:
echo strutils.replace("abc", "a", "z")
Также можно использовать from module import nil, если нужно импортировать модуль, но нужно принудительно использовать полное квалифицирование для каждого символа в module.
Оператор export
Оператор export можно использовать для передачи символов, чтобы модули-клиенты не нуждались в импорте зависимостей модуля:
# module B type MyObject* = object
# module A import B export B.MyObject proc `$`*(x: MyObject): string = "my object"
# module C import A # B.MyObject has been imported implicitly here: var x: MyObject echo $x
Если экспортируемый символ — это другой модуль, все его определения будут переданы. Можно использовать список except для исключения некоторых символов.
Обратите внимание, что при экспорте нужно указать только имя модуля:
import foo/bar/baz export baz
Правила области видимости
Идентификаторы действительны с момента их объявления до конца блока, в котором произошло объявление. Диапазон, в котором известен идентификатор, является областью видимости идентификатора. Точная область видимости идентификатора зависит от способа его объявления.
Область видимости блока
Область видимости переменной, объявленной в части объявления блока, действительна с момента объявления до конца блока. Если блок содержит второй блок, в котором идентификатор объявляется повторно, то внутри этого блока второе объявление будет действительным. При выходе из внутреннего блока первое объявление снова становится действительным. Идентификатор нельзя переопределять в одном и том же блоке, если это не оправдано для целей перегрузки процедур или итераторов.
Область видимости кортежа или объекта
Идентификаторы полей внутри определения кортежа или объекта действительны в следующих местах:
- До конца определения кортежа/объекта.
- Обозначения полей переменной данного типа кортежа/объекта.
- Во всех дочерних типах типа объекта.
Область видимости модуля
Все идентификаторы модуля действительны с момента объявления до конца модуля. Идентификаторы из косвенно зависимых модулей не доступны. Модуль system автоматически импортируется в каждый модуль.
Если модуль импортирует идентификатор двумя разными модулями, каждое вхождение идентификатора должно быть квалифицировано, если это не перегруженная процедура или итератор, в этом случае происходит разрешение перегрузки:
# Module A var x*: string
# Module B var x*: int
# Module C import A, B write(stdout, x) # error: x is ambiguous write(stdout, A.x) # no error: qualifier used var x = 4 write(stdout, x) # not ambiguous: uses the module C's x
Сообщения компилятора
Компилятор Nim выводит различные типы сообщений: сообщения подсказки, предупреждения и ошибки. Сообщение об ошибке выводится, если компилятор обнаруживает любую статическую ошибку.
Директивы
Директивы — это способ Nim предоставить компилятору дополнительную информацию/команды без введения большого количества новых ключевых слов. Директивы обрабатываются динамически во время семантической проверки. Директивы заключаются в специальные фигурные скобки {. и .}. Директивы также часто используются в качестве первой реализации для работы с функцией языка до появления более удобного синтаксиса для доступа к функции.
Директива deprecated
Директива deprecated используется для пометки символа как устаревшего:
proc p() {.deprecated.}
var x {.deprecated.}: char
Эта директива также может принимать необязательную строку предупреждения, чтобы передать её разработчикам.
proc thing(x: bool) {.deprecated: "use thong instead".} Директива noSideEffect
Директива noSideEffect используется для пометки proc/итератора как не имеющего побочных эффектов. Это означает, что proc/итератор изменяет только расположения, доступные из его параметров, а возвращаемое значение зависит только от аргументов. Если ни один из его параметров не имеет тип var T или ref T или ptr T, это означает, что никакие расположения не изменяются. Статическая ошибка — отметить proc/итератор как не имеющий побочных эффектов, если компилятор не может это проверить.
В качестве специального правила, встроенная функция debugEcho притворяется свободной от побочных эффектов, чтобы её можно было использовать для отладки процедур, помеченных как noSideEffect.
func — это синтаксический сахар для proc без побочных эффектов:
func `+` (x, y: int): int
Для переопределения анализа побочных эффектов компилятором можно использовать блок директивы {.noSideEffect.} cast:
func f() =
{.cast(noSideEffect).}:
echo "test" Директива compileTime
Директива compileTime используется для пометки proc или переменной, которые используются только во время выполнения компиляции. Для них не будет сгенерирован никакой код. Процедуры компиляции полезны в качестве вспомогательных средств для макросов. С версии 0.12.0 языка proc, использующая system.NimNode в своих типах параметров, неявно объявляется compileTime:
proc astHelper(n: NimNode): NimNode = result = n
То же самое, что и:
proc astHelper(n: NimNode): NimNode {.compileTime.} =
result = n
Переменные compileTime доступны и во время выполнения. Это упрощает определённые идиомы, где переменные заполняются во время компиляции (например, таблицы поиска), но используются во время выполнения:
import macros
var nameToProc {.compileTime.}: seq[(string, proc (): string {.nimcall.})]
macro registerProc(p: untyped): untyped =
result = newTree(nnkStmtList, p)
let procName = p[0]
let procNameAsStr = $p[0]
result.add quote do:
nameToProc.add((`procNameAsStr`, `procName`))
proc foo: string {.registerProc.} = "foo"
proc bar: string {.registerProc.} = "bar"
proc baz: string {.registerProc.} = "baz"
doAssert nameToProc[2][1]() == "baz" Директива noReturn
Директива noreturn используется для пометки proc, который никогда не возвращает значение.
Директива acyclic
Директива acyclic может использоваться для типов объектов, чтобы пометить их как ациклические, даже если они кажутся циклическими. Это оптимизация для сборщика мусора, чтобы он не рассматривал объекты этого типа как часть цикла:
type
Node = ref NodeObj
NodeObj {.acyclic.} = object
left, right: Node
data: string
Или если мы непосредственно используем объект ref:
type
Node {.acyclic.} = ref object
left, right: Node
data: string
В примере структура дерева объявлена с типом Node. Обратите внимание, что определение типа рекурсивно, и сборщик мусора должен предположить, что объекты этого типа могут образовывать циклический граф. Директива acyclic передаёт информацию о том, что это невозможно, сборщику мусора. Если программист использует директиву acyclic для типов данных, которые на самом деле циклические, утечки памяти могут быть результатом, но безопасность памяти сохраняется.
Директива final
Директива final может использоваться для типа объекта, чтобы указать, что от него нельзя наследовать. Обратите внимание, что наследование доступно только для объектов, которые наследуют от существующего объекта (через синтаксис object of SuperType), или которые были помечены как inheritable.
Директива shallow
Директива shallow влияет на семантику типа: компилятор разрешено выполнять поверхностную копию. Это может привести к серьёзным семантическим проблемам и нарушить безопасность памяти! Однако это может значительно ускорить присваивания, поскольку семантика Nim требует глубокой копии последовательностей и строк. Это может быть дорогостоящим, особенно если последовательности используются для построения древовидной структуры:
type
NodeKind = enum nkLeaf, nkInner
Node {.shallow.} = object
case kind: NodeKind
of nkLeaf:
strVal: string
of nkInner:
children: seq[Node] Директива pure
Тип объекта может быть помечен директивой pure таким образом, чтобы его поле типа, используемое для определения типа во время выполнения, было опущено. Это было необходимо для бинарной совместимости с другими компилируемыми языками.
Тип перечисления может быть помечен как pure. Тогда доступ к его полям всегда требует полного квалифицирования.
Директива asmNoStackFrame
Процедуру можно пометить директивой asmNoStackFrame чтобы сообщить компилятору, что он не должен генерировать стековый кадр для процедуры. Также не генерируются операторы выхода, такие как return result;, и сгенерированная функция C объявляется как __declspec(naked) или __attribute__((naked)) (в зависимости от используемого компилятора C).
Примечание: Эта директива должна использоваться только для процедур, которые состоят только из машинных инструкций.
Директива error
Директива error используется для того, чтобы компилятор выводил сообщение об ошибке с указанным содержимым. Однако компиляция не обязательно прерывается после появления ошибки.
Предикат error также можно использовать для аннотации символа (например, итератора или proc). Использование символа затем вызывает статическую ошибку. Это особенно полезно для исключения возможности того, что какая-то операция допустима из-за перегрузки и преобразований типов:
## check that underlying int values are compared and not the pointers:
proc `==`(x, y: ptr int): bool {.error.} Предикат fatal
Предикат fatal используется для того, чтобы заставить компилятор вывести сообщение об ошибке с заданным содержимым. В отличие от предиката error, компиляция гарантированно будет прервана этим предикатом. Пример:
when not defined(objc):
{.fatal: "Compile this program with the objc command!".} Предикат warning
Предикат warning используется для того, чтобы заставить компилятор вывести сообщение об ошибке с заданным содержимым. Компиляция продолжается после предупреждения.
Предикат hint
Предикат hint используется для того, чтобы заставить компилятор вывести сообщение с подсказкой с заданным содержимым. Компиляция продолжается после подсказки.
Предикат line
Предикат line может использоваться для влияния на информацию о строках аннотированного оператора, как это видно в отступах стека:
template myassert*(cond: untyped, msg = "") =
if not cond:
# change run-time line information of the 'raise' statement:
{.line: instantiationInfo().}:
raise newException(EAssertionFailed, msg)
Если предикат line используется с параметром, параметр должен быть tuple[filename: string, line: int]. Если он используется без параметра, используется system.InstantiationInfo().
Предикат linearScanEnd
Предикат linearScanEnd можно использовать, чтобы сообщить компилятору, как компилировать оператор case Nim. Синтаксически он должен использоваться как оператор:
case myInt
of 0:
echo "most common case"
of 1:
{.linearScanEnd.}
echo "second most common case"
of 2: echo "unlikely: use branch table"
else: echo "unlikely too: use branch table for ", myInt
В примере ветви case 0 и 1 встречаются гораздо чаще, чем другие случаи. Поэтому сгенерированный код ассемблера должен сначала проверить эти значения, чтобы предсказатель ветвей процессора имел хорошие шансы на успех (избегая дорогостоящей остановки конвейера процессора). Другие случаи могут быть помещены в таблицу переходов для O(1) накладных расходов, но ценой (вероятно) остановки конвейера.
Предикат linearScanEnd должен быть помещен в последнюю ветвь, которая должна проверяться с помощью линейного сканирования. Если он помещен в последнюю ветвь всего оператора case case, весь оператор case case использует линейное сканирование.
Предикат computedGoto
Предикат computedGoto можно использовать, чтобы сообщить компилятору, как компилировать оператор case Nim в операторе while true. Синтаксически он должен использоваться как оператор внутри цикла:
type
MyEnum = enum
enumA, enumB, enumC, enumD, enumE
proc vm() =
var instructions: array[0..100, MyEnum]
instructions[2] = enumC
instructions[3] = enumD
instructions[4] = enumA
instructions[5] = enumD
instructions[6] = enumC
instructions[7] = enumA
instructions[8] = enumB
instructions[12] = enumE
var pc = 0
while true:
{.computedGoto.}
let instr = instructions[pc]
case instr
of enumA:
echo "yeah A"
of enumC, enumD:
echo "yeah CD"
of enumB:
echo "yeah B"
of enumE:
break
inc(pc)
vm()
Как показывает пример, computedGoto в основном полезен для интерпретаторов. Если базовый бэкэнд (компилятор C) не поддерживает расширение computed goto, предикат просто игнорируется.
Предикат immediate
Предикат immediate устарел. См. Типизированные и нетипизированные параметры.
Предикаты опций компиляции
Перечисленные здесь предикаты могут быть использованы для переопределения опций генерации кода для proc/method/converter.
Текущая реализация предоставляет следующие возможные опции (в будущем могут быть добавлены другие).
| Предикат | Разрешенные значения | Описание |
|---|---|---|
| checks | on|off | Включает или отключает генерацию кода для всех проверок во время выполнения. |
| boundChecks | on|off | Включает или отключает генерацию кода для проверок границ массивов. |
| overflowChecks | on|off | Включает или отключает генерацию кода для проверок переполнения или недополнения. |
| nilChecks | on|off | Включает или отключает генерацию кода для проверок указателей nil. |
| assertions | on|off | Включает или отключает генерацию кода для утверждений. |
| warnings | on|off | Включает или отключает сообщения об ошибках компилятора. |
| hints | on|off | Включает или отключает сообщения с подсказками компилятора. |
| optimization | none|speed|size | Оптимизировать код для скорости или размера или отключить оптимизацию. |
| patterns | on|off | Включает или отключает шаблоны/макросы переписывания терминов. |
| callconv | cdecl|... | Устанавливает стандартную конвенцию вызова для всех процедур (и типов процедур), которые следуют за ней. |
Пример:
{.checks: off, optimization: speed.}
# compile without runtime checks and optimize for speed Предикаты push и pop
Предикаты push/pop очень похожи на директиву опций, но используются для временного переопределения настроек. Пример:
{.push checks: off.}
# compile this section without runtime checks as it is
# speed critical
# ... some code ...
{.pop.} # restore old settings
push/pop может включать/отключать некоторые предикаты стандартной библиотеки, например:
{.push inline.}
proc thisIsInlined(): int = 42
func willBeInlined(): float = 42.0
{.pop.}
proc notInlined(): int = 9
{.push discardable, boundChecks: off, compileTime, noSideEffect, experimental.}
template example(): string = "https://nim-lang.org"
{.pop.}
{.push deprecated, hint[LineTooLong]: off, used, stackTrace: off.}
proc sample(): bool = true
{.pop.}
Для предикат сторонних библиотек, это зависит от его реализации, но используется тот же синтаксис.
Предикат register
Предикат register предназначен только для переменных. Он объявляет переменную как register, давая компилятору подсказку, что переменную следует разместить в регистре процессора для более быстрого доступа. Однако компиляторы C обычно игнорируют это по понятным причинам: Часто они работают лучше без этого.
В очень специфических случаях (например, цикл обработки инструкций интерпретатора байт-кода) это может дать преимущества.
Предикат global
Предикат global может быть применен к переменной в рамках процедуры, чтобы указать компилятору сохранить ее в глобальной области и инициализировать ее один раз при запуске программы.
proc isHexNumber(s: string): bool =
var pattern {.global.} = re"[0-9a-fA-F]+"
result = s.match(pattern)
При использовании внутри генерической процедуры для каждой инстанциации процедуры будет создана отдельная уникальная глобальная переменная. Порядок инициализации созданных глобальных переменных в модуле не определен, но все они будут инициализированы после всех переменных верхнего уровня в своем исходном модуле и до любой переменной в модуле, который его импортирует.
Отключение определенных сообщений
Nim генерирует некоторые предупреждения и подсказки ("строка слишком длинная"), которые могут раздражать пользователя. Предоставлен механизм для отключения определенных сообщений: каждое сообщение об ошибке и подсказка содержит символ в квадратных скобках. Это идентификатор сообщения, который можно использовать для включения или отключения:
{.hint[LineTooLong]: off.} # turn off the hint about too long lines
Это часто лучше, чем отключение всех предупреждений сразу.
Предикат used
Nim выдает предупреждение для символов, которые не экспортируются и не используются. Предикат used может быть применен к символу, чтобы подавить это предупреждение. Это особенно полезно, когда символ был сгенерирован макросом:
template implementArithOps(T) =
proc echoAdd(a, b: T) {.used.} =
echo a + b
proc echoSub(a, b: T) {.used.} =
echo a - b
# no warning produced for the unused 'echoSub'
implementArithOps(int)
echoAdd 3, 5
used также может использоваться как оператор верхнего уровня для маркировки модуля как "использованного". Это предотвращает предупреждение "Неиспользуемый импорт":
# module: debughelper.nim
when defined(nimHasUsed):
# 'import debughelper' is so useful for debugging
# that Nim shouldn't produce a warning for that import,
# even if currently unused:
{.used.} Предикат experimental
Предикат experimental включает экспериментальные функции языка. В зависимости от конкретной функции это означает, что функция либо считается слишком нестабильной для стабильной версии, либо будущее функции неопределенно (она может быть удалена в любое время).
Пример:
import threadpool
{.experimental: "parallel".}
proc threadedEcho(s: string, i: int) =
echo(s, " ", $i)
proc useParallel() =
parallel:
for i in 0..4:
spawn threadedEcho("echo in parallel", i)
useParallel()
В качестве оператора верхнего уровня предикат experimental включает функцию для остальной части модуля, в котором он включен. Это проблематично для макросов и генерических инстанциаций, которые пересекают границы модулей. В настоящее время эти использования должны быть помещены в .push/pop среду:
# client.nim
proc useParallel*[T](unused: T) =
# use a generic T here to show the problem.
{.push experimental: "parallel".}
parallel:
for i in 0..4:
echo "echo in parallel"
{.pop.}
import client useParallel(1)
Предикаты, специфичные для реализации
В этом разделе описываются дополнительные предикаты, которые поддерживает текущая реализация Nim, но которые не должны рассматриваться как часть спецификации языка.
Предикат Bitsize
Предикат bitsize предназначен для членов полей объектов. Он объявляет поле как битовое поле в C/C++.
type
mybitfield = object
flag {.bitsize:1.}: cuint
генерирует:
struct mybitfield {
unsigned int flag:1;
}; Предикат Align
Предикат align предназначен для переменных и членов полей объектов. Он изменяет требование выравнивания объявляемого сущности. Аргумент должен быть константой, являющейся степенью двойки. Действительные отличные от нуля выравнивания, которые слабее, чем другие предикаты align в одном и том же объявлении, игнорируются. Выравнивания, которые слабее, чем требование выравнивания типа, игнорируются.
type
sseType = object
sseData {.align(16).}: array[4, float32]
# every object will be aligned to 128-byte boundary
Data = object
x: char
cacheline {.align(128).}: array[128, char] # over-aligned array of char,
proc main() =
echo "sizeof(Data) = ", sizeof(Data), " (1 byte + 127 bytes padding + 128-byte array)"
# output: sizeof(Data) = 256 (1 byte + 127 bytes padding + 128-byte array)
echo "alignment of sseType is ", alignof(sseType)
# output: alignment of sseType is 16
var d {.align(2048).}: Data # this instance of data is aligned even stricter
main()
Этот предикат не оказывает влияния на JS бэкэнд.
Предикат Volatile
Предикат volatile предназначен только для переменных. Он объявляет переменную как volatile, что бы это ни значило в C/C++ (его семантика не определена в C/C++).
Примечание: Этот предикат не будет существовать для LLVM бэкэнда.
Предикат NoDecl
Предикат noDecl может быть применен практически к любому символу (переменной, процедуре, типу и т. д.) и иногда полезен для взаимодействия с C: он сообщает Nim, что он не должен генерировать объявление для символа в коде C. Например:
var
EACCES {.importc, noDecl.}: cint # pretend EACCES was a variable, as
# Nim does not know its value
Однако предикат header часто является лучшей альтернативой.
Примечание: Это не будет работать для LLVM бэкэнда.
Предикат Header
Предикат header очень похож на предикат noDecl: Он может быть применен практически к любому символу и указывает, что он не должен объявляться, а вместо этого сгенерированный код должен содержать #include:
type
PFile {.importc: "FILE*", header: "<stdio.h>".} = distinct pointer
# import C's FILE* type; Nim will treat it as a new pointer type
Предикат header всегда ожидает строковую константу. Строковая константа содержит заголовочный файл: как обычно для C, системный заголовочный файл заключен в угловые скобки: <>. Если угловые скобки не указаны, Nim заключает заголовочный файл в "" в сгенерированном коде C.
Примечание: Это не будет работать для LLVM бэкэнда.
Предикат IncompleteStruct
Предикат incompleteStruct сообщает компилятору не использовать базовый C struct в выражении sizeof:
type
DIR* {.importc: "DIR", header: "<dirent.h>",
pure, incompleteStruct.} = object Предикат Compile
Предикат compile может использоваться для компиляции и компоновки файла исходного кода C/C++ с проектом:
{.compile: "myfile.cpp".}
Примечание: Nim вычисляет контрольную сумму SHA1 и перекомпилирует файл только в том случае, если он изменился. Можно использовать параметр командной строки -f для принудительной перекомпиляции файла.
Начиная с версии 1.4, предикат compile также доступен с этим синтаксисом:
{.compile("myfile.cpp", "--custom flags here").}
Как видно из примера, эта новая форма позволяет настраивать флаги, которые передаются компилятору C при перекомпиляции файла.
Предикат Link
Предикат link может использоваться для добавления файла в компоновку проекта:
{.link: "myfile.o".} Директива PassC
Директива passc может использоваться для передачи дополнительных параметров компилятору C, как если бы они были переданы через командную строку, например, --passc:
{.passc: "-Wall -Werror".}
Обратите внимание, что можно использовать gorge из модуля system для вставки параметров из внешней команды, которая будет выполнена во время семантического анализа:
{.passc: gorge("pkg-config --cflags sdl").} Директива LocalPassc
Директива localPassc может использоваться для передачи дополнительных параметров компилятору C, но только для файла C/C++, сгенерированного из модуля Nim, в котором находится директива:
# Module A.nim
# Produces: A.nim.cpp
{.localPassc: "-Wall -Werror".} # Passed when compiling A.nim.cpp Директива PassL
Директива passL может использоваться для передачи дополнительных параметров линковщику, как если бы они были переданы через командную строку, например, --passL:
{.passL: "-lSDLmain -lSDL".}
Обратите внимание, что можно использовать gorge из модуля system для вставки параметров из внешней команды, которая будет выполнена во время семантического анализа:
{.passL: gorge("pkg-config --libs sdl").} Директива Emit
Директива emit может использоваться для прямого влияния на вывод генератора кода компилятора. Полученный код будет непортирован для других генераторов кода/бекендов. Использование этой директивы крайне не рекомендуется! Однако, она может быть очень полезной для взаимодействия с кодом C++ или Objective C.
Пример:
{.emit: """
static int cvariable = 420;
""".}
{.push stackTrace:off.}
proc embedsC() =
var nimVar = 89
# access Nim symbols within an emit section outside of string literals:
{.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""", nimVar, ");"].}
{.pop.}
embedsC()
nimbase.h определяет NIM_EXTERNC макрос C, который может использоваться для того, чтобы extern "C" код работал как с nim c , так и с nim cpp, например:
proc foobar() {.importc:"$1".}
{.emit: """
#include <stdio.h>
NIM_EXTERNC
void fun(){}
""".}
Для обратной совместимости, если аргументом директивы emit является единственная строковая константа, символы Nim могут быть указаны в обратных кавычках. Однако, это использование устарело.
Для директивы emit на верхнем уровне, раздел в сгенерированном файле C/C++, куда должен быть вставлен код, может быть задан с помощью префиксов /*TYPESECTION*/ или /*VARSECTION*/ или /*INCLUDESECTION*/:
{.emit: """/*TYPESECTION*/
struct Vector3 {
public:
Vector3(): x(5) {}
Vector3(float x_): x(x_) {}
float x;
};
""".}
type Vector3 {.importcpp: "Vector3", nodecl} = object
x: cfloat
proc constructVector3(a: cfloat): Vector3 {.importcpp: "Vector3(@)", nodecl} Директива ImportCpp
Примечание: c2nim может анализировать большой подмножество C++ и знает о шаблоне директивы importcpp . Не обязательно знать все детали, описанные здесь.
Аналогично директиве importc для C, директива importcpp может использоваться для импорта методов C++ или C++ символов в целом. Сгенерированный код затем использует синтаксис вызова метода C++: obj->method(arg). В сочетании с директивами header и emit это позволяет нестрогому взаимодействию с библиотеками, написанными на C++:
# Horrible example of how to interface with a C++ engine ... ;-)
{.link: "/usr/lib/libIrrlicht.so".}
{.emit: """
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
""".}
const
irr = "<irrlicht/irrlicht.h>"
type
IrrlichtDeviceObj {.header: irr,
importcpp: "IrrlichtDevice".} = object
IrrlichtDevice = ptr IrrlichtDeviceObj
proc createDevice(): IrrlichtDevice {.
header: irr, importcpp: "createDevice(@)".}
proc run(device: IrrlichtDevice): bool {.
header: irr, importcpp: "#.run(@)".}
Для работы компилятор должен быть настроен на генерацию C++ (команда cpp). Условный символ cpp определён, когда компилятор генерирует C++ код.
Пространства имён
Пример нестрогого взаимодействия использует .emit для создания using namespace объявлений. Обычно гораздо лучше использовать вместо этого ссылку на импортированное имя через обозначение namespace::identifier:
type
IrrlichtDeviceObj {.header: irr,
importcpp: "irr::IrrlichtDevice".} = object Директива Importcpp для перечислений
Когда importcpp применяется к типу перечисления, числовые значения перечисления аннотируются с типом C++ перечисления, как в этом примере: ((TheCppEnum)(3)). (Это оказалось самым простым способом реализации.)
Директива Importcpp для процедур
Обратите внимание, что вариант importcpp для процедур использует несколько загадочный синтаксис для максимальной гибкости:
- Символ #
#заменяется на первый или следующий аргумент. - Точка после символа #
#.указывает, что вызов должен использовать в C++ точечную или стрелочную нотацию. - Символ @
@заменяется оставшимися аргументами, разделенными запятыми.
Например:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "#.CppMethod(@)".}
var x: ptr CppObj
cppMethod(x[], 1, 2, 3)
Создаёт:
x->CppMethod(1, 2, 3)
В качестве специального правила для сохранения обратной совместимости со старыми версиями директивы importcpp , если нет специальных символов (любой из # ' @) , предполагается точечная или стрелочная нотация C++, так что вышеуказанный пример также может быть записан как:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "CppMethod".}
Обратите внимание, что синтаксис шаблонов естественным образом охватывает возможности перегрузки операторов C++:
proc vectorAddition(a, b: Vec3): Vec3 {.importcpp: "# + #".}
proc dictLookup(a: Dict, k: Key): Value {.importcpp: "#[#]".}
- Апостроф
', после которого следует целое числоiв диапазоне от 0 до 9, заменяется типом i-го параметра. Нулевая позиция — тип результата. Это можно использовать для передачи типов в шаблоны функций C++. Между апострофом'и цифрой можно использовать звездочку, чтобы получить базовый тип типа. (Таким образом, "удаляется звезда" из типа;T*становитсяT). Две звездочки могут быть использованы, чтобы получить элемент типа элемента типа и т.д.
Например:
type Input {.importcpp: "System::Input".} = object
proc getSubsystem*[T](): ptr T {.importcpp: "SystemManager::getSubsystem<'*0>()", nodecl.}
let x: ptr Input = getSubsystem[Input]()
Создаёт:
x = SystemManager::getSubsystem<System::Input>()
-
#@— специальный случай для поддержки операцииcnew. Требуется для того, чтобы выражение вызова было вставлено непосредственно, без использования временного места. Это необходимо только для обхода ограничения текущего генератора кода.
Например, оператор C++ new может быть «импортирован» таким образом:
proc cnew*[T](x: T): ptr T {.importcpp: "(new '*0#@)", nodecl.}
# constructor of 'Foo':
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)".}
let x = cnew constructFoo(3, 4)
Создаёт:
x = new Foo(3, 4)
Однако, в зависимости от случая использования new Foo также может быть обернут следующим образом:
proc newFoo(a, b: cint): ptr Foo {.importcpp: "new Foo(@)".}
let x = newFoo(3, 4) Обёртка конструкторов
Иногда у класса C++ есть закрытый конструктор копирования, и поэтому код, подобный Class c = Class(1,2); не должен генерироваться, а вместо этого Class c(1,2);. Для этой цели процедура Nim, которая оборачивает конструктор C++, должна быть аннотирована с помощью директивы constructor. Эта директива также помогает генерировать более быстрый C++ код, так как построение тогда не вызывает конструктор копирования:
# a better constructor of 'Foo':
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)", constructor.} Обёртка деструкторов
Поскольку Nim генерирует C++ напрямую, любой деструктор вызывается неявно компилятором C++ при выходе из области видимости. Это означает, что часто можно обойтись без обёртки деструктора! Однако, когда он должен вызываться явно, он должен быть обернут. Язык шаблонов предоставляет всё необходимое:
proc destroyFoo(this: var Foo) {.importcpp: "#.~Foo()".} Директива Importcpp для объектов
Объекты, обобщённые с помощью importcpp , отображаются на C++ шаблоны. Это означает, что C++ шаблоны можно легко импортировать без необходимости языка шаблонов для типов объектов:
type
StdMap {.importcpp: "std::map", header: "<map>".} [K, V] = object
proc `[]=`[K, V](this: var StdMap[K, V]; key: K; val: V) {.
importcpp: "#[#] = #", header: "<map>".}
var x: StdMap[cint, cdouble]
x[6] = 91.4
Создаёт:
std::map<int, double> x; x[6] = 91.4;
- Если требуется более точный контроль, апостроф
'может быть использован в предоставленном шаблоне для обозначения конкретных параметров типа обобщённого типа. Подробнее об использовании оператора апострофа в шаблонах процедур.
type
VectorIterator {.importcpp: "std::vector<'0>::iterator".} [T] = object
var x: VectorIterator[cint]
Создаёт:
std::vector<int>::iterator x;
Директива ImportJs
Аналогично директиве importcpp для C++, директива importjs может использоваться для импорта методов Javascript или символов в целом. Сгенерированный код затем использует синтаксис вызова метода Javascript: obj.method(arg).
Директива ImportObjC
Аналогично директиве importc для C, директива importobjc может использоваться для импорта методов Objective C. Сгенерированный код затем использует синтаксис вызова метода Objective C: [obj method param1: arg]. В дополнение к директивам header и emit это позволяет нестрогому взаимодействию с библиотеками, написанными на Objective C:
# horrible example of how to interface with GNUStep ...
{.passL: "-lobjc".}
{.emit: """
#include <objc/Object.h>
@interface Greeter:Object
{
}
- (void)greet:(long)x y:(long)dummy;
@end
#include <stdio.h>
@implementation Greeter
- (void)greet:(long)x y:(long)dummy
{
printf("Hello, World!\n");
}
@end
#include <stdlib.h>
""".}
type
Id {.importc: "id", header: "<objc/Object.h>", final.} = distinct int
proc newGreeter: Id {.importobjc: "Greeter new", nodecl.}
proc greet(self: Id, x, y: int) {.importobjc: "greet", nodecl.}
proc free(self: Id) {.importobjc: "free", nodecl.}
var g = newGreeter()
g.greet(12, 34)
g.free()
Для работы компилятор должен быть настроен на генерацию Objective C (команда objc). Условный символ objc определён, когда компилятор генерирует Objective C код.
Директива CodegenDecl
Директива codegenDecl может использоваться для прямого влияния на генератор кода Nim. Она получает строку формата, которая определяет, как переменная или процедура объявляются в сгенерированном коде.
Для переменных $1 в строке формата представляет тип переменной, а $2 — имя переменной.
Следующий код Nim:
var
a {.codegenDecl: "$# progmem $#".}: int
сгенерирует следующий код C:
int progmem a
Для процедур $1 — тип возвращаемого значения процедуры, $2 — имя процедуры, а $3 — список параметров.
Следующий код Nim:
proc myinterrupt() {.codegenDecl: "__interrupt $# $#$#".} =
echo "realistic interrupt handler"
сгенерирует следующий код:
__interrupt void myinterrupt()
Директива InjectStmt
Директива injectStmt может использоваться для вставки оператора перед каждым другим оператором в текущем модуле. Она предназначена только для отладки:
{.injectStmt: gcInvariants().}
# ... complex code here that produces crashes ... Директивы определения времени компиляции
Перечисленные здесь директивы могут использоваться для опционального приема значений из параметра -d/--define во время компиляции.
Текущая реализация предоставляет следующие возможные варианты (позже могут быть добавлены другие).
| Директива | Описание |
|---|---|
| intdefine | Считывает определение времени построения как целое число |
| strdefine | Считывает определение времени построения как строку |
| booldefine | Считывает определение времени построения как логическое значение |
const FooBar {.intdefine.}: int = 5
echo FooBar
nim c -d:FooBar=42 foobar.nim
В приведенном выше примере, указание флага -d приводит к перезаписи символа FooBar во время компиляции, выведя 42. Если -d:FooBar=42 было бы пропущено, использовалось бы значение по умолчанию 5. Чтобы проверить, было ли предоставлено значение, можно использовать defined(FooBar).
Синтаксис -d:flag фактически является сокращением для -d:flag=true.
Пользовательские директивы
Директива pragma
Предикат pragma может использоваться для объявления пользовательских предикатов. Это полезно, потому что шаблоны и макросы Nim не влияют на предикаты. Пользовательские предикаты находятся в отдельном модульном пространстве имён, отличном от всех других символов. Их нельзя импортировать из модуля.
Пример:
when appType == "lib":
{.pragma: rtl, exportc, dynlib, cdecl.}
else:
{.pragma: rtl, importc, dynlib: "client.dll", cdecl.}
proc p*(a, b: int): int {.rtl.} =
result = a+b
В примере вводится новый предикат под названием rtl, который либо импортирует символ из динамической библиотеки, либо экспортирует символ для генерации динамической библиотеки.
Пользовательские аннотации
Можно определить пользовательские предикаты с типом. Пользовательские предикаты не влияют непосредственно на генерацию кода, но их наличие может быть обнаружено макросами. Пользовательские предикаты определяются с помощью шаблонов, аннотированных предикатом pragma:
template dbTable(name: string, table_space: string = "") {.pragma.}
template dbKey(name: string = "", primary_key: bool = false) {.pragma.}
template dbForeignKey(t: typedesc) {.pragma.}
template dbIgnore {.pragma.}
Рассмотрим стилизованный пример возможной реализации Object Relation Mapping (ORM):
const tblspace {.strdefine.} = "dev" # switch for dev, test and prod environments
type
User {.dbTable("users", tblspace).} = object
id {.dbKey(primary_key = true).}: int
name {.dbKey"full_name".}: string
is_cached {.dbIgnore.}: bool
age: int
UserProfile {.dbTable("profiles", tblspace).} = object
id {.dbKey(primary_key = true).}: int
user_id {.dbForeignKey: User.}: int
read_access: bool
write_access: bool
admin_acess: bool
В этом примере пользовательские предикаты используются для описания того, как объекты Nim отображаются в схеме реляционной базы данных. Пользовательские предикаты могут иметь ноль или более аргументов. Для передачи нескольких аргументов используйте один из синтаксисов вызова шаблона. Все аргументы имеют тип и подчиняются стандартным правилам разрешения перегрузки для шаблонов. Поэтому можно задавать значения по умолчанию для аргументов, передавать по имени, использовать varargs и т. д.
Пользовательские предикаты можно использовать во всех местах, где можно указать обычные предикаты. Можно аннотировать определения процедур, шаблонов, типов и переменных, операторы и т. д.
Модуль макросов включает вспомогательные функции, которые можно использовать для упрощения доступа к пользовательским предикатам hasCustomPragma, getCustomPragmaVal. Подробности см. в документации модуля макросов. Эти макросы не являются магическими, всё, что они делают, также можно достичь, пройдясь по AST-представлению объекта.
Дополнительные примеры с пользовательскими предикатами:
- Улучшенный контроль сериализации/десериализации:
type MyObj = object
a {.dontSerialize.}: int
b {.defaultDeserialize: 5.}: int
c {.serializationKey: "_c".}: string
- Использование типа для инспектора GUI в движке игры:
type MyComponent = object
position {.editable, animatable.}: Vector3
alpha {.editRange: [0.0..1.0], animatable.}: float32 Макропредикаты
Все макросы и шаблоны также могут использоваться как предикаты. Их можно прикрепить к процедурам (процедурам, итераторам и т. д.), именам типов или выражениям типов. Компилятор выполнит следующие простые синтаксические преобразования:
template command(name: string, def: untyped) = discard
proc p() {.command("print").} = discard
Это переводится в:
command("print"):
proc p() = discard
type
AsyncEventHandler = proc (x: Event) {.async.}
Это переводится в:
type AsyncEventHandler = async(proc (x: Event))
type
MyObject {.schema: "schema.protobuf".} = object
Это переводится в вызов макроса schema с узлом AST nnkTypeDef, охватывающим как левую, так и правую части определения. Макрос может вернуть потенциально изменённое дерево nnkTypeDef, которое заменит исходную строку в разделе типов.
Если к одному определению применяются несколько макропредикатов, компилятор применит их последовательно слева направо. Каждый макрос получит на вход результат предыдущего.
Интерфейс внешних функций
FFI (Foreign Function Interface) Nim обширный, и здесь документированы только части, масштабируемые до других будущих бэкэндов (например, LLVM/JavaScript).
Предикат Importc
Предикат importc позволяет импортировать процедуру или переменную из C. Необязательный аргумент — строка, содержащая идентификатор C. Если аргумент отсутствует, имя C совпадает с именем Nim буквально:
proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}
Когда importc применяется к оператору let, он может опустить своё значение, которое затем ожидается из C. Это можно использовать для импорта C const:
{.emit: "const int cconst = 42;".}
let cconst {.importc, nodecl.}: cint
assert cconst == 42
Обратите внимание, что в прошлом этот предикат использовался и для работы в js-бэкэнде для js-объектов и функций. : Другие бэкэнды предоставляют ту же возможность под тем же именем. Также, если целевой язык не C, доступны другие предикаты:
proc p(s: cstring) {.importc: "prefix$1".}
В примере внешнее имя p задано как prefixp. Доступен только $1, и литерный знак доллара необходимо писать как $$.
Предикат Exportc
Предикат exportc позволяет экспортировать тип, переменную или процедуру в C. Перечислители и константы экспортировать нельзя. Необязательный аргумент — строка, содержащая идентификатор C. Если аргумент отсутствует, имя C совпадает с именем Nim буквально:
proc callme(formatstr: cstring) {.exportc: "callMe", varargs.}
Обратите внимание, что этот предикат несколько неточно назван: другие бэкэнды предоставляют ту же возможность под тем же именем.
Строковый литерал, переданный в exportc, может быть строкой формата:
proc p(s: string) {.exportc: "prefix$1".} =
echo s
В примере внешнее имя p задано как prefixp. Доступен только $1, а литерный знак доллара необходимо писать как $$.
Если символ также должен быть экспортирован в динамическую библиотеку, следует использовать предикат dynlib дополнительно к предикату exportc. См. Предикат Dynlib для экспорта.
Предикат Extern
Как exportc или importc, предикат extern влияет на имя подстановки. Строковый литерал, переданный в extern, может быть строкой формата:
proc p(s: string) {.extern: "prefix$1".} =
echo s
В примере внешнее имя p задано как prefixp. Доступен только $1, а литерный знак доллара необходимо писать как $$.
Предикат Bycopy
Предикат bycopy может применяться к типу объекта или кортежа и указывает компилятору передавать тип по значению в процедуры:
type
Vector {.bycopy.} = object
x, y, z: float Предикат Byref
Предикат byref может применяться к типу объекта или кортежа и указывает компилятору передавать тип по ссылке (скрытая указатель) в процедуры.
Предикат Varargs
Предикат varargs может применяться только к процедурам (и типам процедур). Он сообщает Nim, что процедура может принимать переменное число параметров после последнего указанного параметра. Значения Nim-строк будут автоматически преобразовываться в C-строки:
proc printf(formatstr: cstring) {.nodecl, varargs.}
printf("hallo %s", "world") # "world" will be passed as C string Предикат Union
Предикат union может применяться к любому типу object. Это означает, что все поля объекта перекрываются в памяти. Это создаёт union вместо struct в сгенерированном C/C++ коде. Объявление объекта не должно использовать наследование или память, управляемую сборщиком мусора, но это в данный момент не проверяется.
Будущие направления: Должна быть разрешена память, управляемая сборщиком мусора, в объединениях, и сборщик мусора должен консервативно сканировать объединения.
Предикат Packed
Предикат packed может применяться к любому типу object. Он гарантирует, что поля объекта упакованы друг за другом в памяти. Это полезно для хранения пакетов или сообщений из/в сети или драйверов устройств, а также для межплатформенной совместимости с C. Объединение предикатов packed с наследованием не определено, и его не следует использовать с памятью, управляемой сборщиком мусора (ссылками).
Будущие направления: Использование памяти, управляемой сборщиком мусора, в предикате packed приведёт к статической ошибке. Использование с наследованием должно быть определено и задокументировано.
Предикат Dynlib для импорта
С помощью предикатом dynlib процедура или переменная может быть импортирована из динамической библиотеки (файлы .dll для Windows, файлы lib*.so для UNIX). Необязательный аргумент должен быть именем динамической библиотеки:
proc gtk_image_new(): PGtkWidget
{.cdecl, dynlib: "libgtk-x11-2.0.so", importc.}
В целом, импорт динамической библиотеки не требует никаких специальных опций компоновщика или компоновки с библиотеками импорта. Это также означает, что не нужно устанавливать пакеты devel.
Механизм импорта dynlib поддерживает схему версионирования:
proc Tcl_Eval(interp: pTcl_Interp, script: cstring): int {.cdecl,
importc, dynlib: "libtcl(|8.5|8.4|8.3).so.(1|0)".}
Во время выполнения динамическая библиотека ищется (в данном порядке):
libtcl.so.1 libtcl.so.0 libtcl8.5.so.1 libtcl8.5.so.0 libtcl8.4.so.1 libtcl8.4.so.0 libtcl8.3.so.1 libtcl8.3.so.0
Предикат dynlib поддерживает не только постоянные строки в качестве аргумента, но и строковые выражения в целом:
import os
proc getDllName: string =
result = "mylib.dll"
if fileExists(result): return
result = "mylib2.dll"
if fileExists(result): return
quit("could not load dynamic library")
proc myImport(s: cstring) {.cdecl, importc, dynlib: getDllName().}
Примечание: Образцы, такие как libtcl(|8.5|8.4).so, поддерживаются только в постоянных строках, потому что они предварительно скомпилированы.
Примечание: Передача переменных в предикат dynlib завершится ошибкой во время выполнения из-за проблем с порядком инициализации.
Примечание: Импорт dynlib можно переопределить с помощью опции командной строки --dynlibOverride:name. Дополнительная информация содержится в Руководстве пользователя по компилятору.
Предикат Dynlib для экспорта
С помощью предикатом dynlib процедура также может быть экспортирована в динамическую библиотеку. Предикат не имеет аргументов и должен использоваться совместно с предикатом exportc:
proc exportme(): int {.cdecl, exportc, dynlib.}
Это полезно только в том случае, если программа скомпилирована как динамическая библиотека с помощью опции командной строки --app:lib.
Потоки
Для включения поддержки потоков необходимо использовать переключатель командной строки --threads:on . Модуль system содержит несколько потоковых примитивов. См. модули threads и channels для низкоуровневого API потоков. Также доступны высокоуровневые конструкции параллелизма. См. spawn для получения дополнительной информации.
Модель памяти Nim для потоков значительно отличается от моделей других распространённых языков программирования (C, Pascal, Java): каждый поток имеет собственную (управляемую сборщиком мусора) кучу, а совместное использование памяти ограничено глобальными переменными. Это помогает предотвратить гонки. Эффективность сборщика мусора значительно улучшается, потому что сборщик мусора никогда не должен останавливать другие потоки и проверять, на что они ссылаются.
Предикат Thread
Процедура, выполняемая в виде нового потока выполнения, должна быть помечена предикатом thread по соображениям читаемости. Компилятор проверяет соблюдение ограничения «без совместного использования кучи» no heap sharing restriction: это ограничение подразумевает, что не допускается создание структуры данных, состоящей из памяти, выделенной из разных (локальных для потока) куч.
Потоковый обработчик передаётся в createThread или spawn и вызывается косвенно; поэтому псевдоним thread подразумевает procvar.
Безопасность сборщика мусора
Мы называем обработчик p безопасным для сборщика мусора, если он не обращается к никаким глобальным переменным, содержащим память, управляемую сборщиком мусора (string, seq, ref или замыкание), ни напрямую, ни косвенно через вызов небезопасного для сборщика мусора обработчика.
Аннотация gcsafe может быть использована для пометки обработчика как безопасного для сборщика мусора, в противном случае это свойство определяется компилятором. Обратите внимание, что noSideEffect подразумевает gcsafe. Единственный способ создать поток — через spawn или createThread. Вызываемый обработчик не должен использовать var параметры, а также ни один из его параметров не должен содержать тип ref или closure. Это обеспечивает соблюдение ограничения отсутствия обмена кучей.
Процедуры, импортированные из C, всегда считаются gcsafe. Чтобы отключить проверку безопасности сборщика мусора, можно использовать командную строку --threadAnalysis:off. Это временное решение для облегчения переноса старого кода в новую модель потоков.
Для переопределения анализа gcsafety компилятора можно использовать блок псевдонима {.cast(gcsafe).}.
var
someGlobal: string = "some string here"
perThread {.threadvar.}: string
proc setPerThread() =
{.cast(gcsafe).}:
deepCopy(perThread, someGlobal)
См. также:
Псевдоним threadvar
Переменная может быть помечена псевдонимом threadvar, что делает её локальной для потока; Кроме того, это подразумевает все эффекты псевдонима global.
var checkpoints* {.threadvar.}: seq[string]
Из-за ограничений реализации локальные для потока переменные не могут быть инициализированы в разделе var. (Каждая локальная для потока переменная должна быть дублирована при создании потока.)
Потоки и исключения
Взаимодействие между потоками и исключениями простое: обработанное исключение в одном потоке не может повлиять на другие потоки. Однако необработанное исключение в одном потоке завершает весь процесс!
© 2006–2021 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/manual.html