Модули: module, use и контроль доступа
Модуль — это единица, которая собирает данные, типы и процедуры в одно именованное пространство и предоставляет их другим частям программы.
module — программная единица Fortran, которая инкапсулирует объявления (переменные, параметры, производные типы, интерфейсы) и процедуры, делая их доступными через оператор
useс проверкой типов на этапе компиляции.
Зачем вообще нужны модули
До Fortran 90 крупные программы держались на двух подпорках: COMMON-блоках для разделяемых данных и текстовых include-файлах для общих объявлений. Обе подпорки были опасны. COMMON-блок — это просто кусок памяти, который разные подпрограммы интерпретировали каждая по-своему: если в одной подпрограмме блок описан как массив из 100 вещественных, а в другой — как 50 целых и 50 вещественных, компилятор молчал, а программа считала мусор. Согласованность раскладки памяти лежала целиком на программисте, и любая правка грозила тихой катастрофой в дальнем углу проекта.
Модуль решает обе проблемы разом. Он даёт единственный источник истины: тип, параметр или процедуру вы описываете один раз внутри модуля, а все потребители получают ровно то же определение через use. Компилятор знает интерфейс каждой процедуры модуля и проверяет каждый вызов: число аргументов, их типы, ранг массивов, намерения. Ошибка, которая в эпоху COMMON проявлялась как загадочный сбой во время счёта, теперь ловится на компиляции. Именно поэтому современный Fortran — это в первую очередь Fortran модулей: новый код почти не содержит ни COMMON, ни внешних процедур без интерфейса.
Анатомия модуля
Модуль состоит из двух частей. Сначала идёт спецификационная часть: объявления типов, констант, переменных уровня модуля. Затем — после ключевого слова contains — часть процедур: функции и подпрограммы, которые модуль предоставляет. Разберём небольшой модуль геометрии.
module geometry
implicit none
real, parameter :: pi = 3.14159265358979_8
contains
pure function circle_area(r) result(area)
real, intent(in) :: r
real :: area
area = pi * r**2
end function circle_area
pure function circle_circumference(r) result(c)
real, intent(in) :: r
real :: c
c = 2.0 * pi * r
end function circle_circumference
end module geometry
Здесь pi — именованная константа уровня модуля, доступная всем процедурам внутри и (по умолчанию) всем, кто подключит модуль. Две функции вычисляют площадь и длину окружности. Слово contains — это граница: всё до него описывает «что хранит модуль», всё после — «что модуль умеет». Обратите внимание на implicit none в начале: его стоит ставить в каждом модуле, чтобы отключить старое неявное правило типизации и заставить компилятор требовать явного объявления всех имён.
Оператор use и его уточнения
Потребитель подключает модуль оператором use. В простейшей форме это втягивает все публичные сущности модуля в текущую область видимости.
program areas
use geometry
implicit none
print *, "Площадь круга r=2: ", circle_area(2.0)
print *, "Число pi: ", pi
end program areas
Но «втянуть всё» — не всегда хорошо. Если два модуля экспортируют сущность с одинаковым именем, возникнет конфликт. И даже без конфликтов засорять пространство имён всеми символами большой библиотеки вредно для читаемости. Поэтому use поддерживает два уточнения. Первое — only: импортировать строго перечисленное.
use geometry, only: circle_area, pi
Теперь видны лишь circle_area и pi; circle_circumference остаётся «снаружи». Списком only стоит пользоваться по умолчанию в серьёзных проектах: он документирует зависимости (видно, что именно код берёт из модуля) и защищает от случайных коллизий при росте библиотек.
Второе уточнение — переименование через =>. Если имя из модуля конфликтует с локальным или просто неудобно, его можно подключить под другим именем.
use geometry, only: area => circle_area
! теперь функция доступна как area(...)
Переименование и only комбинируются в одном операторе, и именно так разрешают коллизии между библиотеками: use lib_a, only: solve => lib_a_solve и use lib_b, only: solve => lib_b_solve дали бы два разных имени, но это сделать нельзя в одной области для одного имени — для разных модулей используют разные псевдонимы.
public и private: проектирование интерфейса
По умолчанию всё, что объявлено в модуле, имеет атрибут public — видно снаружи. Но хорошая библиотека прячет внутренности. Атрибут доступа private делает сущность видимой только внутри модуля. Грамотный приём — поставить private по умолчанию для всего модуля, а затем явно открыть лишь то, что составляет публичный API.
module stats
implicit none
private ! всё закрыто по умолчанию
public :: mean, variance ! открываем только это
real, parameter :: tiny_eps = 1.0e-12 ! приватная деталь
contains
pure function mean(x) result(m)
real, intent(in) :: x(:)
real :: m
m = sum(x) / max(size(x), 1)
end function mean
pure function variance(x) result(v)
real, intent(in) :: x(:)
real :: v, m
integer :: n
n = size(x)
if (n <= 1) then
v = 0.0
return
end if
m = mean(x)
v = sum((x - m)**2) / (n - 1)
end function variance
end module stats
Снаружи доступны только mean и variance. Константа tiny_eps и любые вспомогательные процедуры остаются скрытыми. Это даёт свободу: внутреннюю реализацию можно переписать, не сломав ни одного потребителя, потому что граница API зафиксирована явно. Такой стиль — «private by default, public по списку» — считается признаком зрелого модуля.
Как работает под капотом
Когда компилятор обрабатывает модуль, он порождает не только объектный код процедур, но и особый файл с описанием интерфейса — у gfortran это файл .mod (по сути сериализованное дерево объявлений). Когда другая единица компиляции пишет use geometry, компилятор читает geometry.mod и узнаёт сигнатуры всех публичных сущностей. Отсюда два практических следствия. Во-первых, порядок компиляции важен: модуль должен быть скомпилирован раньше тех, кто его использует, иначе .mod-файла ещё нет. Системы сборки (make, fpm, CMake) выстраивают граф зависимостей именно по операторам use. Во-вторых, изменение публичного интерфейса модуля делает устаревшими все зависящие объектные файлы — их нужно перекомпилировать, и хорошая сборочная система это отслеживает.
Важно не путать .mod (информация для компилятора) с библиотекой или объектным файлом (исполняемый код). .mod нельзя «слинковать» — это чисто метаданные. При раздаче скомпилированной библиотеки поставляют и .mod-файлы (чтобы пользователь мог делать use), и собственно объектный код или статическую/динамическую библиотеку (чтобы линковщик нашёл тела процедур).
Переменные уровня модуля и их время жизни
Помимо процедур, типов и констант, модуль может объявлять обычные переменные — и это мощный, но коварный инструмент. Переменная, объявленная в спецификационной части модуля, существует всё время работы программы (имеет атрибут save неявно) и разделяется между всеми, кто подключает модуль. По сути это глобальная переменная, но с проверкой типов и контролем доступа — куда безопаснее старого COMMON. Такие переменные удобны для общих настроек, разделяемых таблиц, счётчиков. Однако глобальное состояние всегда несёт риск: если две процедуры из разных мест меняют одну модульную переменную, отследить логику становится трудно, а в многопоточном или параллельном коде это прямой источник гонок. Поэтому модульные переменные стоит делать private и менять только через процедуры модуля, инкапсулируя состояние. Ещё одно правило безопасности — инициализатор в объявлении (integer :: counter = 0) выполняется один раз при старте программы, а не при каждом обращении; это удобно, но важно понимать, что повторного обнуления не происходит.
Отдельного упоминания заслуживают именованные константы (parameter) уровня модуля. В отличие от переменных, они не занимают изменяемой памяти и вычисляются на этапе компиляции, поэтому их публичность безопасна и даже желательна: математические постоянные, размерности, коды состояний удобно держать в модуле и импортировать. Хороший приём — собрать все «магические числа» проекта в один модуль констант, чтобы изменить значение в единственном месте. Именно так оформляют, например, модуль с параметром точности dp или модуль физических постоянных, к которому обращается весь расчётный код.
Модули как единица проектирования
За техникой стоит более глубокая идея: модуль — это не просто контейнер, а единица архитектуры. Грамотно спроектированный модуль обладает высокой связностью (всё внутри относится к одной теме) и слабой связанностью с другими (минимум зависимостей через use). Когда вы разбиваете большую программу на модули, вы фактически проводите границы ответственности: модуль сетки, модуль решателя, модуль ввода-вывода, модуль граничных условий. Каждый скрывает свои детали и предоставляет узкий, осмысленный интерфейс. Это даёт три практических выигрыша. Во-первых, понятность: чтобы разобраться в части системы, достаточно прочитать один модуль и его публичный API, а не всю программу. Во-вторых, тестируемость: модуль с чётким интерфейсом легко покрыть тестами в изоляции. В-третьих, заменяемость: реализацию модуля можно переписать (сменить алгоритм, оптимизировать), не трогая остальной код, пока сохраняется контракт. Эти принципы — те же, что в любой развитой инженерии ПО, и современный Fortran полностью их поддерживает. Поэтому проектирование начинают не с процедур, а с вопроса «какие модули и какие границы между ними», а уже потом наполняют каждый модуль реализацией. Такой подход отличает поддерживаемый научный код от «скрипта, который разросся» — и именно модульная декомпозиция позволяет большим расчётным проектам жить и развиваться десятилетиями силами сменяющихся команд.
Частые ошибки
- Забыть
implicit noneв модуле. Без него опечатка в имени переменной создаёт новую переменную неявного типа, и ошибка уходит в рантайм. Ставьтеimplicit noneсразу послеmodule имя. - Полагать, что
useбезonly«дешевле». На производительность это не влияет —useне копирует код, а лишь делает имена видимыми. Но безonlyвы рискуете коллизиями и теряете читаемость зависимостей. - Циклические зависимости модулей. Если модуль A использует B, а B использует A, компилятор не сможет построить ни один
.modпервым. Разорвать цикл помогает либо вынесение общих типов в третий модуль, либо механизм submodules (о нём — отдельный урок). - Менять публичный интерфейс и забывать пересобрать потребителей. При ручной компиляции это даёт рассинхрон
.modи объектных файлов и странные ошибки линковки. Используйте систему сборки, которая отслеживает зависимости.
Итоги
- Модуль — современная замена
COMMON-блокам иinclude: единый источник истины с проверкой типов на компиляции. - Структура: спецификация (типы, константы, переменные) →
contains→ процедуры. useподключает публичные сущности;onlyограничивает импорт,=>переименовывает и разрешает коллизии.- Стиль «private by default + public по списку» фиксирует API и даёт свободу менять реализацию.
- Под капотом —
.mod-файл с метаданными; отсюда требование правильного порядка компиляции.