Содержание


Язык программирования Nimrod

Часть 6. Модули и макроопределения

Comments

Серия контента:

Этот контент является частью # из серии # статей: Язык программирования Nimrod

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Язык программирования Nimrod

Следите за выходом новых статей этой серии.

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

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

1. Модули

В Nimrod поддерживается разделение программы на модули, каждый из которых размещается в отдельном файле и имеет собственное пространство имён (namespace), тем самым обеспечивая одну из степеней защиты данных (а именно, делая их недоступными извне). Раздельная компиляция модулей позволяет сэкономить время при сборке программы.

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

Из модуля экспортируются только те символы самого верхнего уровня, которые помечены звёздочкой (*), как показано в примере в листинге 1.

Листинг 1. Модуль с символами верхнего уровня, помеченными для экспортирования
# Модуль A, размещённый в отдельном файле
var
  attr*, 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:
  # проверка нового оператора умножения для последовательностей
  if( @[1, 2, 3, 4, 5] * @[1, 2, 3, 4, 5] == @[1, 4, 9, 16, 25] ):
    echo("OK")
  else:
    echo("Оператор * для последовательностей определён неверно")

В приведённом выше примере модуль разрешает экспортировать только переменную attr и определённый внутри данного модуля новый оператор умножения двух последовательностей *. Переменные x и y не могут быть экспортированы. Именно поэтому при компиляции данного модуля выводятся сообщения о том, что переменные x и y объявлены, но не использованы, а для переменной attr такое сообщение не выводится, поскольку предполагается, что она может использоваться за пределами этого модуля, как экспортируемая переменная.

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

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

1.1. Случаи взаимозависимости модулей

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

Листинг 2. Демонстрация рекурсивного импортирования модулей
# Модуль A
type
  T1* = int  # экспортируемый тип T1
import B  # здесь начинается импортирование и синтаксический анализ модуля B

proc main() =
  var i = p(3)  # процедура p из модуля B известна, т.к. модуль B уже импортирован

main()

# Модуль B
import A  # здесь модуль A не будет анализироваться, а импортируются только
          # уже известные (распознанные) на данный момент символы
proc p*( x: A.T1 ): A.T1 =
  # тип T1 из модуля A уже известен, так как при синтаксическом анализе модуля A
  # компилятор уже добавил T1 в таблицу символов
  return x+1

1.2. Разрешение конфликтов имён при использовании нескольких модулей

Синтаксическая форма "имя_модуля.символ" позволяет точно определять принадлежность символа к конкретному модулю. Это становится необходимым, когда возникает неоднозначность, связанная с наличием одинаковых символов в разных модулях, используемых совместно. Небольшой пример в листинге 3 иллюстрирует различные варианты определения принадлежности символов в случаях возникновения неоднозначности.

Листинг 3. Определение принадлежности символов (разрешение конфликта имён)
# Модуль 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 ) # неоднозначность не возникает, т.к. в модуле C определён свой символ x

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

1.3. Команда from

Если не требуется импорт всех символов, экспортируемых из некоторого модуля, как происходит при выполнении команды import, а нужны лишь отдельные символы, то можно воспользоваться командой from, например:

from moduleA import x, y

1.4. Команда include

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

include file1, file2, file3

2. Макроопределения

Макроопределения или макрокоманды позволяют выполнять преобразования кода во время компиляции, но никоим образом не изменяют синтаксис языка.

Для написания макрокоманды необходимо понимать, каким образом выполняется преобразование конкретной синтаксической конструкции языка Nimrod в абстрактное синтаксическое дерево (AST), которое подробно описано в модуле macros.

Вызов макроопределения может быть выполнен двумя способами: аналогично вызову процедуры, то есть макроопределение как выражение (expression macro), или с помощью специальной синтаксической конструкции macrostmt, то есть как макрокоманда (statement macro).

2.1. Макроопределение как выражение

В листинге 4 приведено макроопределение debug для вывода различной отладочной информации, принимающее переменное количество аргументов:

Листинг 4. Макроопределение debug
# Для работы с AST Nimrod требуется API, определённый в модуле macros
import macros

macro debug( n: varargs[expr] ): stmt =
  # n - Nimrod AST, содержащее список выражений
  # макроопределение возвращает список инструкций
  result = newNimNode( nnkStmtList, n )
  # проход по каждому аргументу, переданному в макро
  for i in 0..n.len-1:
    # добавление в список инструкций вызова, выводящего рассматриваемое выражение
    result.add( newCall( "write", newIdentNode("stdout"), toStrLit(n[i]) ))
    # добавление в список инструкций вызова, выводящего символ двоеточия
    result.add( newcall( "write", newIdentNode("stdout"), toStrLit(": ") ))
    # добавление в список инструкций вызова, выводящего значение выражения
    result.add( newCall( "writeln", newIdentNode("stdout"), n[i] ))

var
  a: array[0..10: int]
  x: "просто строка"

a[0] = 42
a[1] = 45

debug( a[0], a[1], x )

# После раскрытия макроопределения:

write( stdout, "a[0]" )
write( stdout. ": " )
write( stdout, a[0] )

write( stdout, "a[1]" )
write( stdout. ": " )
write( stdout, a[1] )

write( stdout, "x" )
write( stdout. ": " )
write( stdout, x )

2.2. Макрокоманда

Макрокоманда определяется точно также, но вызывается с обязательным суффиксом "двоеточие" (:). В листинге 5 приведён пример макрокоманды, генерирующей лексический анализатор для регулярных выражений.

Листинг 5. Макрокоманда - лексический анализатор для регулярных выражений
macro case_regexp_token( n: stmt ): stmt =
  # реализация собственно лексического анализатора здесь не приводится в виду
  # ограниченности размера статьи и разнообразия методических подходов к решению
  # (можно считать написание реализации домашним заданием для читателей)
  nil

case_regexp_token:  # здесь двоеточие служит признаком макрокоманды
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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Linux
ArticleID=856917
ArticleTitle=Язык программирования Nimrod: Часть 6. Модули и макроопределения
publish-date=01312013