Учебное пособие по Nim (часть 1)

Original author: Andreas Rumpf
  • Translation
  • Tutorial
Примечание от переводчика
Этот перевод делался по мотивам комментария от пользователя stas3k, в котором он предложил frol перевести две части «Nim Tutorial». Меня это заинтересовало и я перевёл их самостоятельно, в меру своего разумения. Ежели кто найдёт ошибки (они там наверняка есть — глаз под конец совсем уже замылился), сообщайте в личку, буду править.

Введение


“Der Mensch ist doch ein Augentier – schöne Dinge wünsch ich mir.”
(Цитата из песни «Morgenstern» группы «Rammstein». Примерный перевод: «Но человек – глазастый зверь, – мне нужно множество красивых вещей».)

Это – обучающий материал (tutorial) по языку программирования Nim. Предполагается, что вы знакомы с базовыми концепциями программирования, такими как переменные, типы или команды, но глубокие знания не обязательны. Большое количество примеров по сложным нюансам языка, вы можете найти в официальном руководстве. Все примеры кода в этом документе следуют руководству по стилю языка Nim.

Первая программа


Начнём с модифицированной программы «hello world»:

# Это комментарий
echo("What's your name? ")
var name: string = readLine(stdin)
echo("Hi, ", name, "!")

Сохраните этот код в файл greetings.nim. Теперь скомпилируйте и запустите его:

nim compile --run greetings.nim

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

nim compile --run greetings.nim arg1 arg2

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

nim c -r greetings.nim

Чтобы скомпилировать релизную версию, используйте такую команду:

nim c -d:release greetings.nim

По умолчанию компилятор Nim генерирует много проверок времени выполнения, для упрощения отладки. Ключ -d:release отключает эти проверки и включает оптимизацию.

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

Строковые литералы заключаются в двойные кавычки. Команда var объявляет новую переменную с именем name и типом string, после чего ей присваивается значение, которое вернула процедура readLine. Поскольку компилятор знает, что readLine возвращает строку, вы можете не писать тип в объявлении (это называется локальным выведением типов). Так что такой вариант тоже будет работать:

var name = readLine(stdin)

Обратите внимание – это почти единственная форма выведения типов, присутствующая в Nim: это хороший компромисс между краткостью и читаемостью.

Программа «hello world» содержит некоторые идентификаторы, которые уже известны компилятору: echo, readLine и т.д. Эти встроенные команды объявлены в модуле system, который неявно импортируется любым другим модулем.

Лексические элементы


Рассмотрим лексические элементы Nim подробнее. Как и другие языки программирования, Nim состоит из литералов, идентификаторов, ключевых слов, комментариев, операторов и знаков пунктуации.

Строковые и символьные литералы


Строковые литералы заключаются в двойные кавычки; символьные – в одинарные. Специальные символы экранируются обратным слэшем \: \n означает перевод строки, \t – табуляцию и так далее. Также бывают сырые (raw) строковые литералы:

r"C:\program files\nim"

В сырых литералах обратный слэш не является экранирующим символом.

Третий и последний способ записи строковых литералов, это длинные строковые литералы. Они обрамляются тройными кавычками: """ ... """, могут содержать перевод строки и \ в них не является экранирующим символом. Они очень полезны, например, для включения в код HTML-фрагментов.

Комментарии


Комментарий может находиться где угодно за пределами строкового или символьного литерала и начинается с символа решётки #. Документирующие комментарии начинаются с ##:

# Комментарий.

var myVariable: int ## документирующий комментарий

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

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

discard """ Здесь можно разместить текст любого кода
на Nim без каких-либо ограничений по отступам.
      yes("May I ask a pointless question?") """

Числа


Численные литералы пишутся так же, как и в большинстве других языков. Для повышения читабельности разрешается отбивать разряды подчёркиваниями: 1_000_000 (один миллион). Числа, содержащие точку (или e, или E) считаются литералами чисел с плавающей запятой: 1.0e9 (один миллиард). Шестнадцатеричные литералы начинаются с префикса 0x, двоичные – с 0b, а восьмеричные – с 0o. Ведущий ноль не превращает число в восьмеричное.

Команда var


Команда var объявляет новую локальную или глобальную переменную:

var x, y: int # объявляем x и y, имеющие тип `int`

С помощью отступов после ключевого слова var можно перечислить целую секцию переменных:

var
  x, y: int
  # здесь может быть комментарий
  a, b, c: string

Команда присваивания


Команда присваивания назначает новое значение переменной или, более общими словами, месту хранения:

var x = "abc" # вводит новую переменную `x` и присваивает ей значение
x = "xyz"     # присваивает новое значение `x`

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

var x, y = 3  # присваивает переменным `x` и `y` значение 3
echo "x ", x  # выведет "x 3"
echo "y ", y  # выведет "y 3"
x = 42        # изменяет значение `x` на 42, не меняя `y`
echo "x ", x  # выведет "x 42"
echo "y ", y  # выведет "y 3"

Обратите внимание, что объявление нескольких переменных одним присваиванием, которое вызывает процедуру, может дать неожиданный результат: компилятор развернёт присваивание и в конечном счёте вызовет процедуру несколько раз. Если результат процедуры зависит от побочных эффектов, ваши переменные могут получить различные значения! Чтобы избежать этого, используйте только константные значения.

Константы


Константы это символы, связанные со значением. Значение константы не может меняться. Компилятор должен быть способен вычислить выражение в объявлении константы на этапе компиляции:

const x = "abc" # константа x содержит строку "abc"

С помощью отступов после ключевого слова const можно перечислить целую секцию констант:

const
  x = 1
  # комментарий, который может здесь присутствовать
  y = 2
  z = y + 5 # вычисления возможны

Команда let


Команда let работает примерно как var, но объявляет переменные однократного присваивания: после инициализации их значение не может быть изменено.

let x = "abc" # вводит новую переменную `x` и присваивает ей значение
x = "xyz"     # не сработает: присваивание `x`

Различие между let и const следующее: let вводит переменную, которая не может быть переприсвоена, а const означает «принудительно вычислить во время компиляции и поместить результат в секцию данных»:

const input = readLine(stdin) # Ошибка: предполагается константное выражение

let input = readLine(stdin)   # а это сработает

Команды управления потоком


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

Команда if


Команда if – один из способов организовать ветвление потока выполнения:

let name = readLine(stdin)
if name == "":
  echo("Poor soul, you lost your name?")
elif name == "name":
  echo("Very funny, your name is name.")
else:
  echo("Hi, ", name, "!")

Веток elif может быть ноль и более, ветка else не обязательна. Ключевое слово elif является сокращением для else if, чтобы не делать излишних отступов. ("" это пустая строка, она не содержит символов.)

Команда case


Другой способ ветвления реализуется командой case. Она разделяет поток выполнения на несколько веток:

let name = readLine(stdin)
case name
of "":
  echo("Poor soul, you lost your name?")
of "name":
  echo("Very funny, your name is name.")
of "Dave", "Frank":
  echo("Cool name!")
else:
  echo("Hi, ", name, "!")

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

Команда case может работать с целыми числами, другими перечислимыми типами и строками. (Что такое перечислимые типы будет рассказано позже.) Для целых чисел и перечислимых типов можно использовать диапазоны значений:

# эта команда будет объясняться позже:
from strutils import parseInt

echo("A number please: ")
let n = parseInt(readLine(stdin))
case n
of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}")
of 3, 8: echo("The number is 3 or 8")

Впрочем, код выше не будет компилироваться. Причина в том, что вам надо покрыть все значения, которые может принимать n, а код обрабатывает только значения 0..8. Поскольку не слишком практично перечислять все возможные целые числа (хотя такое и возможно, благодаря нотации диапазонов), мы исправим это, указав компилятору, что для всех остальных значений ничего делать не нужно:

...
case n
of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}")
of 3, 8: echo("The number is 3 or 8")
else: discard

Пустая команда discard это команда ничего не делать. Компилятор знает, что команда case с секцией else охватывает все возможные варианты и, таким образом, ошибка исчезает. Обратите внимание, что все строковые значения покрыть невозможно: вот почему в случае строк ветка else обязательна.

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

Команда while


Команда while представляет собой простой цикл:

echo("What's your name? ")
var name = readLine(stdin)
while name == "":
  echo("Please tell me your name: ")
  name = readLine(stdin)
  # `var` отсутствует, поскольку здесь не объявляется новая переменная

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

Команда for


Команда for реализует цикл по всем элементам итератора. Вот пример использования встроенного итератора countup:

echo("Считаем до десяти: ")
for i in countup(1, 10):
  echo($i)
# --> На выходе 1 2 3 4 5 6 7 8 9 10 на разных строчках

Встроенный оператор $ преобразует целое число (int) и многие другие типы в строку. Переменная i неявно объявляется циклом for и имеет тип int, поскольку countup возвращает именно этот тип. i проходит по значениям 1, 2, .., 10. Каждое значение выводится с помощью echo. Этот код делает то же самое:

echo("Считаем до 10: ")
var i = 1
while i <= 10:
  echo($i)
  inc(i) # увеличиваем i на 1
  # --> На выходе 1 2 3 4 5 6 7 8 9 10 на разных строчках

Обратный отсчёт реализуется столь же просто (но не так часто нужен):

echo("Считаем от 10 до 1: ")
for i in countdown(10, 1):
  echo($i)
# --> На выходе 10 9 8 7 6 5 4 3 2 1 на разных строчках

Поскольку отсчёт с увеличением часто используется в программах, в Nim есть итератор .., который делает то же, что и countup:

for i in 1..10:
  ...

Области видимости и команда block


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

while false:
  var x = "hi"
echo(x) # не работает

Команда while (for) создаёт неявный блок. Идентификаторы видимы только внутри того блока, в котором они были объявлены. Команду block можно использовать, чтобы открыть новый блок явно:

block myblock:
  var x = "hi"
echo(x) # тоже не работает

Метка блока (myblock в примере) не обязательна.

Команда break


Из блока можно досрочно выйти командой break. Эта команда может прерывать блоки команд while, for или block. Она выходит из ближайшего блока, если не задана метка блока, из которого надо выйти:

block myblock:
  echo("входим в блок")
  while true:
    echo("цикл")
    break # покидаем цикл, но не блок
  echo("мы всё ещё в блоке")

block myblock2:
  echo("входим в блок")
  while true:
    echo("цикл")
    break myblock2 # покидаем блок (и цикл)
  echo("мы всё ещё в блоке")

Команда continue


Как и во многих других языках программирования, команда continue немедленно переходит к следующей итерации:

while true:
  let x = readLine(stdin)
  if x == "": continue
  echo(x)

Команда when


Пример:

when system.hostOS == "windows":
  echo("running on Windows!")
elif system.hostOS == "linux":
  echo("running on Linux!")
elif system.hostOS == "macosx":
  echo("running on Mac OS X!")
else:
  echo("unknown operating system")

Команда when почти идентична команде if, но есть некоторые различия:
  • каждое условие должно быть константным выражением, поскольку вычисляется компилятором;
  • команды внутри ветки не открывают новую область видимости;
  • компилятор проверяет синтаксис и генерирует код только для команд, принадлежащих ветке по первому условию, которое вернуло true.

Команда when полезна для написания платформенно-зависимого кода, по аналогии с конструкцией #ifdef языка C.
Примечание: Чтобы закомментировать большой кусок кода, часто бывает удобнее использовать вместо комментариев конструкцию when false:. Её можно делать неоднократно вложенной.

Команды и отступы


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

В Nim есть различие между простыми и сложными командами. Простые команды, такие как присваивание, вызов процедур или команда return, не могут содержать других команд. Сложные команды, такие как if, when, for, while могут содержать другие команды. Чтобы избежать неоднозначности сложные команды всегда пишутся с отступом, а простые – нет:

# для одиночного присваивания отступ не нужен:
if x: x = false

# нужен отступ для вложенного if:
if x:
  if y:
    y = false
  else:
    y = true

# нужен отступ, потому что две команды соответствуют условию:
if x:
  x = false
  y = false

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

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

Если кратко, то отступы в выражениях разрешены после операторов, после открытия скобок и после запятых.

С помощью скобок и точек с запятой (;) вы можете использовать команды там, где разрешены только выражения:

# вычисляем fac(4) на этапе компиляции:
const fac4 = (var x = 1; for i in 1..4: x *= i; x)

Процедуры


Чтобы создавать новые команды, такие как echo и readLine из примеров, нам понадобится концепция процедур. (В некоторых языках они называются методами или функциями.) В Nim новые процедуры определяются с помощью ключевого слова proc:

proc yes(question: string): bool =
  echo(question, " (y/n)")
  while true:
    case readLine(stdin)
    of "y", "Y", "yes", "Yes": return true
    of "n", "N", "no", "No": return false
    else: echo("Please be clear: yes or no")

if yes("Should I delete all your important files?"):
  echo("I'm sorry Dave, I'm afraid I can't do that.")
else:
  echo("I think you know what the problem is just as well as I do.")

В этом примере показана процедура с названием yes, которая задаёт пользователю вопрос и возвращает true, если он ответил «yes», и false, если он ответил «no». Команда return приводит к немедленному выходу из процедуры (и, соответственно, цикла while). Синтаксис (question: string): bool означает, что процедура ожидает получить параметр с именем question и типом string и вернёт значение типа bool. bool это встроенный тип: единственные значения, которые он может принимать, это true и false. Условия в командах if или while должны иметь тип bool.

Немного терминологии: в примере question формально называется параметром, а "Should I..."аргументом, который передаётся в этом параметре.

Переменная result


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

proc sumTillNegative(x: varargs[int]): int =
  for i in x:
    if i < 0:
      return
    result = result + i

echo sumTillNegative() # выведет 0
echo sumTillNegative(3, 4, 5) # выведет 12
echo sumTillNegative(3, 4 , -1 , 6) # выведет 7

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

Параметры


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

proc printSeq(s: seq, nprinted: int = -1) =
  var nprinted = if nprinted == -1: s.len else: min(nprinted, s.len)
  for i in 0 .. <nprinted:
    echo s[i]

Если процедуре нужно модифицировать аргумент для передачи вызвавшему, можно использовать var-параметр:

proc divmod(a, b: int; res, remainder: var int) =
  res = a div b        # целочисленное деление
  remainder = a mod b  # целочисленное взятие остатка

var
  x, y: int
echo(x)
divmod(8, 5, x, y) # модифицирует x и y
echo(y)

В примере res и remainder являются var-параметрами. Такие параметры могут модифицироваться процедурой и изменения станут видимы вызвавшему. Отметим, что в примере выше вместо var-параметров лучше было бы вернуть кортеж.

Команда discard


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

discard yes("Можно мне задать бесполезный вопрос?")

Возвращённое значение можно проигнорировать неявно, если вызываемая процедура или итератор были объявлены с прагмой discardable:

proc p(x, y: int): int {.discardable.} =
  return x + y

p(3, 4) # теперь работает

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

Именованные аргументы


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

proc createWindow(x, y, width, height: int; title: string;
                  show: bool): Window =
   ...

var w = createWindow(show = true, title = "My Application",
                     x = 0, y = 0, height = 600, width = 800)

Теперь, когда мы использовали именованные аргументы для вызова createWindow, порядок аргументов больше не имеет значения. Можно смешивать именованные аргументы с неименованными, но это ухудшает читаемость:

var w = createWindow(0, 0, title = "My Application",
                     height = 600, width = 800, true)

Компилятор проверяет, что каждый параметр получает ровно один аргумент.

Значения по умолчанию


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

proc createWindow(x = 0, y = 0, width = 500, height = 700,
                  title = "unknown",
                  show = true): Window =
   ...

var w = createWindow(title = "My Application", height = 600, width = 800)

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

Отметим, что для параметров со значениями по умолчанию работает вывод типов, так что нет необходимости писать, например, title: string = "unknown".

Перегруженные процедуры


Nim даёт возможность перегружать процедуры по аналогии с C++:

proc toString(x: int): string = ...
proc toString(x: bool): string =
  if x: result = "true"
  else: result = "false"

echo(toString(13))   # вызывается процедура toString(x: int)
echo(toString(true)) # вызывается процедура toString(x: bool)

(Не забудьте, что toString в Nim обычно реализуется оператором $.) Компилятор выберет наиболее подходящую процедуру для вызова toString. Как именно работает алгоритм выбора перегружаемых процедур, тут обсуждаться не будет (этот вопрос будет скоро разобран в руководстве). Однако он не приводит к неприятным сюрпризам и базируется на довольно простом алгоритме унификации. Неоднозначные вызовы приводят к выводу сообщения об ошибке.

Операторы


Библиотека Nim интенсивно пользуется перегрузкой – одна из причин этого в том, что каждый оператор наподобие + это просто перегруженная процедура. Парсер позволяет использовать операторы в инфиксной (a + b) или префиксной нотации (+ a). Инфиксный оператор всегда получает два аргумента, а префиксный – всегда один. Постфиксные операторы запрещены, поскольку могут привести к неоднозначности: a @ @ b означает (a) @ (@b) или (a@) @ (b)? Поскольку постфиксных операторов в Nim нет, это выражение всегда будет означать (a) @ (@b).

Кроме нескольких встроенных операторов-ключевых слов, таких как and, or и not, операторы всегда состоят из таких символов: + - * \ / < > = @ $ ~ & % ! ? ^ . |

Определяемые пользователем операторы разрешаются. Ничто не помешает вам определить свой собственный оператор @!?+~, но читаемость может пострадать.

Приоритет оператора определяется по его первому символу. Подробности можно найти в руководстве.

Чтобы определить оператор, заключите его в апострофы:

proc `$` (x: myDataType): string = ...
# теперь оператор $ также работает и с myDataType, перекрывая поведение $,
# реализованное ранее только для встроенных типов

Эта нотация также может использоваться для вызова оператора как процедуры:

if `==`( `+`(3, 4), 7): echo("True")

Предварительные объявления


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

# предварительное объявление:
proc even(n: int): bool
proc even(n: int): bool

proc odd(n: int): bool =
  assert(n >= 0) # гарантирует, что мы не свалимся в отрицательную рекурсию
  if n == 0: false
  else:
  n == 1 or even(n-1)

proc even(n: int): bool =
  assert(n >= 0) # гарантирует, что мы не свалимся в отрицательную рекурсию
  if n == 1: false
  else:
  n == 0 or odd(n-1)

Здесь odd зависит от even и наоборот. Таким образом, even должна встретиться компилятору до того, как будет полностью определена. Синтаксис для такого предварительного объявления несложен: просто пропустите = и тело процедуры. assert добавляет граничные условия и будет описан позднее в разделе Модули.

В дальнейших версиях языка требования для предварительных объявлений будут менее строгими.

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

Итераторы


Давайте вернёмся к скучному примеру с подсчётом:

echo("Считаем до десяти: ")
for i in countup(1, 10):
  echo($i)

Можно ли самим написать процедуру countup для использования в таком цикле? Давайте попробуем:

proc countup(a, b: int): int =
  var res = a
  while res <= b:
    return res
    inc(res)

Увы, это не работает. Проблема в том, что процедура должна не просто вернуть значение, а вернуть и продолжить работу на следующей итерации. Вот это «вернуть и продолжить» называется командой yield. Теперь осталось лишь заменить ключевое слово proc на iterator и вот он – наш первый итератор:

iterator countup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

Итераторы очень похожи на процедуры, но имеют несколько важных различий:
  • итераторы могут быть вызваны только из циклов for;
  • итераторы не могут содержать команду return, а процедуры не могут содержать команду yield;
  • у итераторов нет неявной переменной result;
  • итераторы не поддерживают рекурсию;
  • итераторы не могут быть объявлены предварительно, поскольку компилятор должен иметь возможность встроить (inline) итератор (это ограничение будет убрано в будущих версиях компилятора).

Однако, вы можете также использовать итератор closure, имеющий другой набор ограничений. Подробности смотрите в разделе документации «Итераторы первого класса». Итераторы могут иметь те же имена и параметры, что и процедуры: у них своё собственное пространство имён. Поэтому есть общепринятая практика оборачивания итераторов в процедуры с теми же именами, которые накапливают результат итератора и возвращают его в виде последовательности, как split из модуля strutils.

Базовые типы


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

Логические значения


Логический тип в Nim называется bool и состоит из двух предопределённых значений true и false. Условия в командах while, if, elif и when должны иметь тип bool.

Для типа bool определены операторы not, and, or, xor, <, <=, >, >=, != и ==. Операторы and и or выполняют сокращённые вычисления. Пример:

while p != nil and p.name != "xyz":
  # p.name не вычисляется, если p == nil
  p = p.next

Символы


Символьный тип в Nim называется char. Его размер равен одному байту. Таким образом, он не может представлять собой символ UTF-8, только его часть. Причина этого в эффективности: в подавляющем большинстве случаев готовые программы будут правильно обрабатывать данные в UTF-8, так как UTF-8 был разработан специально для этого. Символьные литералы заключаются в одиночные кавычки.

Символы могут сравниваться с помощью операторов ==, <, <=, > и >=. Оператор $ преобразует char в string. Символы не могут смешиваться с целыми числами; для получения численного значения символа используйте процедуру ord. Конвертирование из численного значения в символ выполняется с помощью процедуры chr.

Строки


Строковые значения в Nim изменяемые, так что операция добавления подстроки к строке довольно эффективна. Строки в Nim одновременно заканчиваются нулём и содержат поле длины. Длину строки можно получить встроенной процедурой len; длина никогда не учитывает завершающий ноль. Доступ к завершающему нолю не вызывает ошибки и часто приводит к упрощению кода:

if s[i] == 'a' and s[i+1] == 'b':
  # не нужно проверять, что i < len(s)!
  ...

Оператор присвоения для строк копирует строку. Вы можете использовать оператор & для конкатенации строк и add для добавления подстроки.

Сравнение строк производится в лексикографическом порядке. Допустимы любые операторы сравнения. Согласно соглашению, все строки являются строками UTF-8, но это не обязательно. Например, при чтении строк из бинарных файлов, они, скорее, представляют собой последовательность байтов. Операция s[i] означает iсимвол (а не iюникодный символ) строки s.

Строковые переменные инициализируются специальным значением под названием nil. Однако большинство строковых операций не могут работать с nil (это приводит к возбуждению исключения) по соображениям производительности. Вместо nil в качестве пустого значения нужно использовать пустую строку "". Но "" создаёт строковый объект в куче, так что здесь нужно искать компромисс между памятью и производительностью.

Целые числа


Nim имеет следующие встроенные целочисленные типы: int, int8, int16, int32, int64, uint, uint8, uint16, uint32 и uint64.

По умолчанию используется целочисленный тип int. Целочисленные литералы могут иметь суффикс, обозначающий их принадлежность к тому или иному целочисленному типу:

let
  x = 0     # x имеет тип int
  y = 0'i8  # y имеет тип int8
  z = 0'i64 # z имеет тип int64
  u = 0'u   # u имеет тип uint

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

Основные операторы +, -, *, div, mod, <, <=, ==, !=, > и >= определены для целых чисел. Операторы and, or, xor и not также определены для целых чисел и выполняют побитовые операции. Битовый сдвиг влево делается с помощью оператора shl, а сдвиг вправо – с помощью оператора shr. Операторы битового сдвига всегда трактуют свой аргумент как беззнаковое число. Они могут использоваться для умножения или деления.

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

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

Числа с плавающей запятой


Nim имеет следующие встроенные типы чисел с плавающей запятой: float, float32 и float64.

По умолчанию используется тип float. В текущей реализации float всегда имеет размер 64 бита.

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

var
  x = 0.0      # x имеет тип float
  y = 0.0'f32  # y имеет тип float32
  z = 0.0'f64  # z имеет тип float64

Основные операторы +, -, *, /, <, <=, ==, !=, > и >= определены для чисел с плавающей запятой и соответствуют стандарту IEEE.

Автоматическое преобразование типов в выражениях с различными видами типов с плавающей запятой выполняется: меньшие типы конвертируются в большие. Целочисленные типы не конвертируются автоматически в типы с плавающей запятой и наоборот. Для таких преобразований могут использоваться процедуры toInt и toFloat.

Преобразование типов


Преобразование между базовыми типами в Nim выполняется путём использования типа как функции:

var
  x: int32 = 1.int32   #  то же, что вызов int32(1)
  y: int8  = int8('a') # 'a' == 97'i8
  z: float = 2.5       # int(2.5) округляется до 2
  sum: int = int(x) + int(y) + int(z) # sum == 100

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


Как упоминалось ранее, встроенный оператор преобразования в строку $ превращает любой базовый тип в строку, которую вы можете вывести на экран с помощью процедуры echo. Однако оператор $ не сможет работать со сложными типами или типами, которые вы создали самостоятельно, пока вы не переопределите его для них.

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

var
  myBool = true
  myCharacter = 'n'
  myString = "nim"
  myInteger = 42
  myFloat = 3.14
echo($myBool, ":", repr(myBool))
# --> true:true
echo($myCharacter, ":", repr(myCharacter))
# --> n:'n'
echo($myString, ":", repr(myString))
# --> nim:0x10fa8c050"nim"
echo($myInteger, ":", repr(myInteger))
# --> 42:42
echo($myFloat, ":", repr(myFloat))
# --> 3.1400000000000001e+00:3.1400000000000001e+00

Дополнительные типы


Новые типы в Nim можно определять с помощью команды type:

type
  biggestInt = int64      # целочисленный тип, больший, чем это доступно
  biggestFloat = float64  # тип с плавающей запятой, больший, чем это доступно

Перечисления и объектные типы не могут определяться «на лету», только в команде type.

Перечисления


Переменная, относящаяся к типу перечислений, может принимать лишь ограниченное множество значений. Это множество состоит из упорядоченных символов. Каждый символ внутренне отображается на целочисленное значение. Первый символ соответствует числу 0, второй – числу 1 и так далее. Пример:

type
  Direction = enum
    north, east, south, west

var x = south      # `x` имеет тип `Direction`; его значение `south`
echo($x)           # выводит "south" на `stdout`

Для перечислений могут использоваться любые операторы сравнения.

Во избежание неоднозначности символы перечислений могут быть квалифицированы: Direction.south.

Оператор $ может конвертировать любое значение перечисления в его имя, а процедура ord – в соответствующее целочисленное значение.

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

Явно пронумерованные перечисления могут содержать пропуски:

type
  MyEnum = enum
    a = 2, b = 4, c = 89

Перечислимые типы


Перечисления без разрывов, целочисленные типы, char, bool (и их поддиапазоны) – все они называются перечислимыми типами. Для перечислимых типов есть несколько специальных операций:
Операция Комментарий
ord(x) возвращает целое число, которое используется для представления значения x
inc(x) увеличивает x на 1
inc(x, n) увеличивает x на n; n – целое число
dec(x) уменьшает x на 1
dec(x, n) уменьшает x на n; n – целое число
succ(x) возвращает следующий за x элемент
succ(x, n) возвращает n-й элемент, следующий за x
pred(x) возвращает предшественника x
pred(x, n) возвращает n-го предшественника x

Операции inc, dec, succ и pred могут выполниться с ошибкой, возбудив исключение EOutOfRange или EOverflow. (Если, конечно, код скомпилирован со включенными проверками на исключения.)

Диапазоны


Этот тип представляет собой диапазон значений целочисленного типа или перечисления (базового типа). Пример:

type
  Subrange = range[0..5]

Subrange это диапазон int, который может содержать значения от 0 до 5. Назначение любых других значений переменной типа Subrange приведёт к ошибке компиляции или времени выполнения. Назначение базового типа одному из его диапазонов (и наоборот) разрешается.

Модуль system определяет важный тип Natural как range[0..high(int)] (high возвращает максимально допустимое значение). Другие языки программирования принуждают использовать для работы с натуральными числами беззнаковые целые числа. Это часто бывает неправильно: вас не должны заставлять использовать беззнаковую арифметику лишь по той причине, что числа не могут быть отрицательными. Тип Natural языка Nim позволяет избежать этой распространённой ошибки программирования.

Множества


Тип set моделирует математическое понятие множества. Базовый тип множества может быть только перечислимым типом определённого размера, а именно:
  • int8-int16
  • uint8/byte-uint16
  • char
  • enum
или эквивалентным. Причина в том, что множества реализованы как высокопроизводительные битовые векторы. Попытка объявить множество большего типа приведёт к ошибке:

var s: set[int64] # Error: set is too large

Множества могут быть сконструированы с помощью конструктора множества: {} это пустое множество. Пустое множество совместимо по типу с любым конкретным типом множества. Конструктор также может быть использован для включения элементов (и диапазонов элементов):

type
  CharSet = set[char]
var
  x: CharSet
x = {'a'..'z', '0'..'9'} # Здесь конструируется множество, содержащее буквы от
                         # 'a' до 'z' и цифры от '0' до '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
  IntArray = array[0..5, int] # массив целых чисел, индексированный от 0 до 5
var
  x: IntArray
x = [1, 2, 3, 4, 5, 6]
for i in low(x)..high(x):
  echo(x[i])

Нотация x[i] используется для получения доступа к i-му элементу x. При доступе к элементам массива всегда производится проверка границ (либо во время компиляции, либо во время выполнения). Эту проверку можно отключить прагмами или вызовом компилятора с ключом --bound_checks:off.

Массивы являются типами-значениями, как и другие типы Nim. Оператор присваивания копирует содержимое массива целиком.

Встроенная процедура len возвращает длину массива. low(a) возвращает наименьший возможный индекс массива a, а high(a) – наибольший возможный индекс.

type
  Direction = enum
    north, east, south, west
  BlinkLights = enum
    off, on, slowBlink, mediumBlink, fastBlink
  LevelSetting = array[north..west, BlinkLights]
var
  level: LevelSetting
level[north] = on
level[south] = slowBlink
level[east] = fastBlink
echo repr(level)  # --> [on, fastBlink, slowBlink, off]
echo low(level)   # --> north
echo len(level)   # --> 4
echo high(level)  # --> west

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

type
  LightTower = array[1..10, LevelSetting]
var
  tower: LightTower
tower[1][north] = slowBlink
tower[1][east] = mediumBlink
echo len(tower)     # --> 10
echo len(tower[1])  # --> 4
echo repr(tower)    # --> [[slowBlink, mediumBlink, ...и т.д...
# Следующие строки не компилируются из-за несовпадения типов
#tower[north][east] = on
#tower[0][1] = on

Обратите внимание, что встроенная процедура len вернёт длину только массива первого уровня. Чтобы ещё лучше показать вложенную природу LightTower, можно было бы не писать предыдущее определение типа LevelSetting, а вместо этого включить его непосредственно в тип первого измерения:

type
  LightTower = array[1..10, array[north..west, BlinkLights]]

Довольно часто массивы начинаются с нуля, так что есть сокращённый синтаксис для задания диапазона от нуля до указанного индекса минус один:

type
  IntArray = array[0..5, int] # массив, индексированный от 0 до 5
  QuickArray = array[6, int]  # массив, индексированный от 0 до 5
var
  x: IntArray
  y: QuickArray
x = [1, 2, 3, 4, 5, 6]
y = x
for i in low(x)..high(x):
  echo(x[i], y[i])

Последовательности


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

Последовательности всегда индексируются int, начинающимся с 0. Операции len, low и high применимы для последовательностей. Нотация x[i] может использоваться для доступа к i-му элементу x.

Последовательности могут конструироваться с помощью конструктора массива [], соединённого с оператором преобразования массива в последовательность @. Другой способ выделить память для последовательности состоит в вызове встроенной процедуры newSeq.

Последовательность может передаваться в параметре openarray.

Пример:

var
  x: seq[int] # ссылка на последовательность целых чисел
x = @[1, 2, 3, 4, 5, 6] # @ превращает массив в последовательность, размещённую в куче

Переменные последовательности инициализируются значением nil. Однако, большая часть операций над последовательностями не может работать с nil (это приведёт к возбуждению исключения) по причинам, связанным с производительностью. Так что в качестве пустого значения желательно использовать пустую последовательность @[], а не nil. Но @[] создаёт объект последовательности в куче, поэтому надо будет искать решение, приемлемое для вашего конкретного случая.

Команда for, использованная для последовательности, может работать с одной или двумя переменными. Если вы используете форму с одной переменной, то переменная будет содержать значение, предоставляемое последовательностью. Команда for проходит по результатам, полученным из итератора items() модуля system. Но если вы используете форму с двумя переменными, тогда первая переменная содержит индекс позиции, а вторая – значение. В этом случае команда for проходит по результатам итератора pairs() из модуля system. Примеры:

for i in @[3, 4, 5]:
  echo($i)
# --> 3
# --> 4
# --> 5

for i, value in @[3, 4, 5]:
  echo("index: ", $i, ", value:", $value)
# --> index: 0, value:3
# --> index: 1, value:4
# --> index: 2, value:5

Открытые массивы


Примечание: Открытые массивы могут использоваться только в качестве параметров.
Часто выясняется, что массивы с фиксированным размером недостаточно гибки: процедурам бывает нужно иметь дело с массивами разных размеров. Для этого есть тип открытого массива. Открытые массивы всегда индексируются целыми числами и нумерация начинается с 0. Для них доступны операции len, low и high. Любой массив с совместимым базовым типом может быть передан в качестве параметра открытого массива, тип индекса не имеет значения.

var
  fruits:   seq[string]       # ссылка на последовательность строк, которая
                              # инициализирована значением 'nil'
  capitals: array[3, string]  # массив строк с фиксированным размером

fruits = @[]                  # создаёт в куче пустую последовательность, на
                              # которую будет ссылаться 'fruits'

capitals = ["New York", "London", "Berlin"]   # массив 'capitals' позволяет
                                              # присвоить лишь три элемента
fruits.add("Banana")          # последовательность 'fruits' динамически
                              # расширяется в ходе выполнения
fruits.add("Mango")

proc openArraySize(oa: openArray[string]): int =
  oa.len

assert openArraySize(fruits) == 2     # процедура принимает последовательность
                                      # в качестве параметра
assert openArraySize(capitals) == 3   # но и массив тоже

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

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


Параметр varargs напоминает открытый массив. Однако, вдобавок он позволяет передать в процедуру любое число аргументов. Компилятор автоматически преобразует список аргументов в массив:

proc myWriteln(f: File, a: varargs[string]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, "abc", "def", "xyz")
# превращается компилятором в:
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)
# превращается компилятором в:
myWriteln(stdout, [$123, $"abc", $4.0])

В данном примере $ применяется к каждому аргументу, переданному через параметр a. Заметьте, что $, применённый к строкам, не делает ничего.

Слайсы


Слайсы по синтаксису выглядят аналогично типам диапазонов, но используются в другом контексте. Слайс – это просто объект типа Slice, который содержит две границы, a и b. Сам по себе слайс не очень полезен, но другие типы коллекций определяют операторы, которые принимают объекты Slice для задания диапазонов.

  var
    a = "Nim is a progamming language"
    b = "Slices are useless."

  echo a[7..12] # --> 'a prog'
  b[11..^2] = "useful"
  echo b # --> 'Slices are useful.'

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

Кортежи (tuples)


Тип кортежа определяет поля с различными названиями и порядок этих полей. Для конструирования кортежей можно использовать конструктор (). Порядок полей в конструкторе должен совпадать с порядком полей в определении кортежа. Различные типы кортежей считаются эквивалентными, если они задают поля тех же типов с теми же именами в том же порядке.

Оператор присваивания для кортежей копирует каждый их компонент. Нотация t.field используется для доступа к полю кортежа. Другая нотация, t[i] даёт доступ к i-му полю (i должно быть целочисленной константой).

type
  Person = tuple[name: string, age: int] # тип представляет персону, которая
                                         # состоит из имени и возраста
var
  person: Person
person = (name: "Peter", age: 30)
# то же, но менее читабельно:
person = ("Peter", 30)

echo(person.name) # "Peter"
echo(person.age)  # 30

echo(person[0]) # "Peter"
echo(person[1]) # 30

# Вам не нужно объявлять кортежи в отдельной секции типов.
var building: tuple[street: string, number: int]
building = ("Rue del Percebe", 13)
echo(building.street)

# Следующая строка не скомпилируется, это разные кортежи!
#person = building
# --> Error: type mismatch: got (tuple[street: string, number: int])
#     but expected 'Person'

# А эта работает, поскольку имена и типы полей те же.
var teacher: tuple[name: string, age: int] = ("Mark", 42)
person = teacher

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

Кортежи могут распаковываться в процессе присваивания переменных (и только в этом случае!). Это бывает полезно, чтобы непосредственно присваивать значения полей кортежа отдельным именованным переменным. В качестве примера рассмотрим процедуру splitFile из модуля os, которая возвращает одновременно каталог, имя и расширение пути. Для правильной распаковки кортежа вам надо использовать круглые скобки вокруг значений, в которые вы распаковываете кортеж, в противном случае вы назначите одно и то же значение каждой из этих переменных! Пример:

import os

let
  path = "usr/local/nimc.html"
  (dir, name, ext) = splitFile(path)
  baddir, badname, badext = splitFile(path)
echo dir      # выводит usr/local
echo name     # выводит nimc
echo ext      # выводит .html
# А следующее выведет одну и ту же строку:
# `(dir: usr/local, name: nimc, ext: .html)`
echo baddir
echo badname
echo badext

Распаковка кортежей работает только в блоках var или let. Следующий код не будет компилироваться:

import os

var
  path = "usr/local/nimc.html"
  dir, name, ext = ""

(dir, name, ext) = splitFile(path)
# --> Error: '(dir, name, ext)' cannot be assigned to

Ссылочные типы и указатели


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

Nim различает отслеживаемые (traced) и неотслеживаемые (untraced) ссылки. Неотслеживаемые ссылки также называют указателями. Отслеживаемые ссылки указывают на объекты в куче со сборкой мусора, неотслеживаемые – на объекты, память под которые была выделена вручную или на объекты в других местах памяти. Таким образом, неотслеживаемые ссылки небезопасны. Однако для некоторых низкоуровневых операций (доступа к «железу») обойтись без них нельзя.

Отслеживаемые ссылки объявляются ключевым словом ref, неотслеживаемые – ключевым словом ptr.

Пустая subscript-нотация [] может использоваться для разыменования ссылки, то есть для получения элемента, на который указывает ссылка. Операторы . (доступ к полю кортежа/объекта) и [] (оператор индексирования массива/строки/последовательности) выполняют неявное разыменование для ссылочных типов:

type
  Node = ref NodeObj
  NodeObj = object
    le, ri: Node
    data: int
var
  n: Node
new(n)
n.data = 9
# не надо писать n[].data, это сильно сбивает с толку!

Для выделения памяти под новый отслеживаемый объект используется встроенная процедура new. Для работы с неотслеживаемой памятью могут использоваться процедуры alloc, dealloc и realloc. Документация модуля system содержит дополнительную информацию по этим вопросам.

Если ссылка ни на что не указывает, она имеет значение nil.

Процедурный тип


Процедурный тип это (несколько абстрактно) указатель на процедуру. Переменные процедурного типа могут принимать значение nil. Nim использует процедурный тип для реализации техник функционального программирования.

Пример:

proc echoItem(x: int) = echo(x)

proc forEach(action: proc (x: int)) =
  const
    data = [2, 3, 5, 7, 11]
  for d in items(data):
    action(d)

forEach(echoItem)

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

Модули


Nim поддерживает разбиение программы на части согласно концепции модульности. Каждый модуль находится в отдельном файле. Модули позволяют выполнять сокрытие информации и раздельную компиляцию. Они могут получать доступ к символам других модулей с помощью команды import. Экспортироваться могут только символы верхнего уровня, отмеченные звёздочкой (*):

# Модуль A
var
  x*, y: int

proc `*` *(a, b: seq[int]): seq[int] =
  # создать новую последовательность:
  newSeq(result, len(a))
  # перемножить две целочисленных последовательности:
  for i in 0..len(a)-1: result[i] = a[i] * b[i]

when isMainModule:
  # проверить новый оператор ``*`` для последовательностей:
  assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9])


Модуль A экспортирует x и *, но не y.

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

В каждом модуле есть особая магическая константа isMainModule, которая истинна, если модуль скомпилирован в качестве основного файла. Это очень полезно для внедрения внутрь модуля тестов, как показано в предыдущем примере.

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

Алгоритм компилирования модулей таков:

  • Скомпилировать модуль как обычно, рекурсивно следуя командам import.
  • Если обнаружен цикл, то импортировать только уже разобранные символы (экспортированные); если встретился неизвестный идентификатор, то прервать работу.

Это лучше всего показать на примере:

# Модуль A
type
  T1* = int  # Модуль A экспортирует тип ``T1``
import B     # компилятор начинает разбирать B

proc main() =
  var i = p(3) # работает, поскольку B здесь уже полностью разобран

main()

# Модуль B
import A  # A пока ещё не разобран до конца! Будут импортированы только те
          # символы, которые разобраны в A на данный момент.

proc p*(x: A.T1): A.T1 =
  # это работает, поскольку компилятор уже добавил T1 к таблице символов A
  result = x + 1

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

# Модуль A
var x*: string

# Модуль B
var x*: int

# Модуль C
import A, B
write(stdout, x) # ошибка: x неоднозначен
write(stdout, A.x) # ошибки нет: используется квалификатор

var x = 4
write(stdout, x) # неоднозначности нет: используется x модуля C

Но это правило неприменимо к процедурам или итераторам. Здесь применяются правила перегрузки:

# Модуль A
proc x*(a: int): string = $a

# Модуль B
proc x*(a: string): string = $a

# Модуль C
import A, B
write(stdout, x(3))   # ошибки нет: вызывается A.x
write(stdout, x(""))  # ошибки нет: вызывается B.x

proc x*(a: int): string = nil
write(stdout, x(3))   # неоднозначность: какой `x` вызывать?

Исключение символов


Обычно, команда import забирает все экспортируемые символы. Это можно изменить, указав исключаемые символы с квалификатором except.

import mymodule except y

Команда from


Мы уже видели простую команду import, импортирующую все экспортированные символы. Можно импортировать только перечисленные символы с помощью команды from import:

from mymodule import x, y, z

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

from mymodule import x, y, z

x()           # использовать x без квалификации

from mymodule import nil

mymodule.x()  # нужно квалифицировать x, указав имя модуля в качестве префикса

x()           # использование x без квалификации приведёт к ошибке компиляции

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

from mymodule as m import nil

m.x()         # m это псевдоним для mymodule

Команда include


Команда include делает нечто, фундаментально отличающееся от импортирования модуля: она вставляет на своё место содержимое файла. Эта команда весьма полезна для разбиения большого модуля на несколько файлов:

include fileA, fileB, fileC


(Добавлено продолжение: «Учебное пособие по Nim (часть 2)».)
Share post

Comments 22

    0
    Ребят сильно вдохновил «Пайтон», судя по всему.
      +1
      Они этого и не отрицают:
      The language borrows heavily from (in order of impact): Modula 3, Delphi, Ada, C++, Python, Lisp, Oberon.
        0
        А как у него с производительностью? Чисто по синтаксису кажется, что на нем было бы удобно всякие расчеты записывать.
          0
          Если кратко — производительность сопоставима с С/С++/Rust/Go.

          Вот есть Benchmark'и: github.com/logicchains/LPATHBench/blob/master/writeup.md

          А вот есть статья на хабре сравнивающая Nim vs Rust: habrahabr.ru/post/259993
            +1
            Если кратко — производительность сопоставима с С/С++/Rust/Go.
            Производительность С/С++/Rust не сопоставима с производительностью Go, последняя сопоставима с таковой у Java/C#.
              0
              Ок, оставим C/C++/Rust. Я вписал Go так как он там где-то рядом, если сравнивать с производительностью CPython (ветка началась с него, вот я с ним и сравнивал).
      0
      Отличный перевод! Спасибо за проделанную работу!

      Мне вот интересно, кто-нибудь уже успел опробовать Nim в реальных задачах? Я, несмотря на свой начальный интерес к языку, так и не нашёл времени и подходящей задачи, так как проект достаточно молод и не хотелось бы совсем всё писать с самого начала.
        +1
        Пожалуйста. :-) Завтра планирую вторую часть закончить.

        Язык довольно интересный, но вот версия 0.12 меня немного смущает. Вон, Rust и Go перевалили уже за 1.0, а Nim что-то не спешит.
        +5
        На момент старта функции переменная result всегда уже объявлена, так что попытка объявить её снова, например, с помощью var result, приведёт к затенению её обычной переменной с тем же именем. Переменная result всегда инициализируется значением по умолчанию для своего типа. Поэтому ссылочные типы данных будут иметь значение nil, так что их при необходимости придётся инициализировать вручную.
        Адище.
          0
          Можно подробнее?
            +6
            1. Неявная переменная. Получается, ради возврата я должен присвоить в неё, и не могу вернуть оттуда, где у меня уже лежит результат.
            2. Значение по-умолчанию для неявной переменной в случае указательного типа — nil. Получается, если я забыл сделать возврат, я возвращаю nil. Привет, «NoneType has no 'foo' method»!
            3. Мало этого, ещё и маскирование добавили в коктейль. Т.е. если я объявил переменную, а потом сделал return result, что вернётся — моя или неявная?
              0
              1. Как я понял, можно написать return "Вася", а можно — result = "Вася", итог один и тот же.
              2. Это да, оригинальное решение. Думаю, автор языка понадеялся на то, что сложно будет одновременно забыть сделать возврат и проинициализировать то, что собирался возвращать.
              3. Если был явный возврат, то должна вернуться ваша переменная. Если явного возврата нет — возвращается изначальный result, т.е. значение по умолчанию для типа. Фичу явно скопипастили с Паскаля. :-)
                0
                Как я понял, можно написать return «Вася», а можно — result = «Вася», итог один и тот же.
                В чём смысл этой конструкции тогда?
                  +1
                  Полагаю, стремление угодить паскалистам, рубистам, питонщикам и сишникам одновременно. :-) Типа, пишите кто как привык.
                    +2
                    Нужно было
                    proc countup(a, b: int): resultA, resultB int =
                      resultA = a + b
                      resultB = a * b
                    

                    Чтобы еще matlab угодить
                      +2
                      Этот синтаксис кстати очень неплохой, в отличие от имени «result» по умолчанию. Потому что здесь имена возвращаемых значений объявляются пользователем явно и могут использоваться при расчетах как обычные переменные.
                        0
                        Так уже сделано, почти такое же:

                        proc test():tuple[x: string, y: int] =
                          result.x = "XXX"
                          result.y = 42
                        
                        var (a, b) = test()
                        echo a
                        echo b
                        
                        
                        0
                        Ещё в Гоу так можно, чем я нередко пользуюсь.
              0
              del
                +3
                Спасибо! Всегда интересно пообсуждать языки программирования. Вот несколько вещей, которые на мой взгляд являются недостатками.
                1. Синтаксис нечувствительный к регистру (все кроме первых символов), да еще и игнорирующий подчеркивания. Якобы для удобства интеграции с другими языками, а как по мне так наоборот неудобства и путаница.
                2. Экспериментальная «фича» (т.е. ее нужно включать явно) — nim-lang.org/docs/manual.html#syntax-strong-spaces — приоритет пользовательских операторов зависит от количества пробелов перед ним.
                3. Странный набор операторов для беззнаковых чисел: a +% b, a -% b и т.д.
                4. Сама по себе концепция «пользовательских операторов» очень сомнительная и явно не добавляет читаемости.
                  +2
                  Подозреваю, что писать на этом языке будет довольно приятно: как хочешь, так и веселись. А вот читать то, что накатали другие, будет не так приятно. :-)
                    +2
                    Нечувствительность к регистру и возвращение результата через переменную result — это то, что досталось от Delphi/Pascal. Хоть я и любил Delphi и Pascal в своё время, сейчас же мне кажется, что это не самые лучшие вещи, которые стоило брать в новый язык. Nim в плане философии для меня близок к Ruby — в языке есть 100500 (изощрённых) способов выстрелить себе и тому, кто будет поддерживать написанный код, в ногу.

                  Only users with full accounts can post comments. Log in, please.