subroutine и function
Процедуры — способ разбить программу на части: subroutine выполняет действие, function вычисляет значение.
Процедура — именованная единица кода, которую можно вызывать многократно; в Fortran есть два вида:
subroutine(выполняет действие, вызывается операторомcall) иfunction(вычисляет и возвращает значение, используется в выражении).
Программа из тысяч строк в одном program — кошмар сопровождения. Решение во всех языках одно: разбить код на процедуры, каждая из которых решает одну задачу. Fortran различает два вида процедур, и понимание разницы между ними — основа структурного программирования на этом языке. Этот урок вводит subroutine и function, показывает, как и где их размещать через раздел contains, и объясняет, почему явный интерфейс делает код надёжным. Это фундамент, на котором держится модульность всей программы.
Subroutine: процедура-действие
subroutine выполняет некоторое действие и может изменять переданные ей аргументы. Вызывается она оператором call. Размещают процедуры внутри программы после ключевого слова contains.
program use_subroutine
implicit none
real :: a, b
a = 3.0
b = 4.0
call swap(a, b) ! вызов через call
print *, "После обмена: a =", a, " b =", b
contains
subroutine swap(x, y)
real, intent(inout) :: x, y
real :: tmp
tmp = x
x = y
y = tmp
end subroutine swap
end program use_subroutine
Вывод:
После обмена: a = 4.00000000 b = 3.00000000
Процедура swap меняет местами значения двух аргументов. Раздел contains отделяет основной код от внутренних процедур; такие процедуры называют внутренними и они видят переменные охватывающей программы. Вызов call swap(a, b) передаёт a и b, и после возврата их значения изменены — об этом механизме (передача по ссылке) подробно в следующем уроке.
Function: процедура-вычисление
function вычисляет одно значение и возвращает его — её вызывают прямо в выражении, как математическую функцию. Тип результата объявляют, а само значение присваивают имени результата.
program use_function
implicit none
real :: r
r = 5.0
print *, "Площадь круга:", circle_area(r)
print *, "Удвоенная:", 2.0 * circle_area(r)
contains
function circle_area(radius) result(area)
real, intent(in) :: radius
real :: area ! тип результата
real, parameter :: pi = 3.14159265
area = pi * radius**2
end function circle_area
end program use_function
Вывод:
Площадь круга: 78.5398178 Удвоенная: 157.079636
Синтаксис function circle_area(radius) result(area) объявляет функцию с результатом по имени area. Внутри мы присваиваем area = ..., и это значение возвращается. Указывать result(...) — современная и рекомендуемая практика: иначе результат пришлось бы возвращать через само имя функции, что менее наглядно, особенно при рекурсии.
Когда что выбирать
Различие между ними — концептуальное, и выбор влияет на ясность кода. Простое правило: если процедура отвечает на вопрос «чему равно?» — это function; если выполняет команду «сделай!» — это subroutine.
| Аспект | function | subroutine |
| Назначение | вычислить значение | выполнить действие |
| Вызов | в выражении: y = f(x) | оператором: call s(x) |
| Возврат | одно значение через имя/result | через аргументы (intent out/inout) |
| Типичный пример | sin(x), area(r) | сортировка массива, печать |
Хороший стиль: функции делают чистыми — вычисляют результат только из аргументов, не меняя ничего вокруг (об этом урок про pure). Тогда y = f(x) ведёт себя как математическая функция, и код легко читать. Изменение нескольких значений или действия с побочными эффектами логичнее оформить субрутиной.
Явный интерфейс: зачем нужен contains
Размещение процедур после contains даёт важное свойство — явный интерфейс (explicit interface). Это значит, что компилятор знает сигнатуру процедуры (типы и атрибуты аргументов) в точке вызова и проверяет корректность. Старый стиль — внешние процедуры без интерфейса — лишён этой проверки и опасен.
program interface_check
implicit none
print *, "Куб 3:", cube(3.0)
! print *, cube(3) ! ОШИБКА компиляции: 3 это integer, нужен real
contains
function cube(x) result(c)
real, intent(in) :: x
real :: c
c = x**3
end function cube
end program interface_check
Вывод:
Куб 3: 27.0000000
Благодаря явному интерфейсу попытка вызвать cube(3) с целым аргументом будет поймана компилятором — он знает, что нужен real. Без интерфейса (внешняя процедура) такая ошибка прошла бы молча и дала мусор. Поэтому современная рекомендация: размещайте процедуры в contains или в модулях, обеспечивая явный интерфейс везде.
Чем внешние процедуры без интерфейса так опасны
Стоит задержаться на том, почему отсутствие явного интерфейса — это не мелкое неудобство, а источник самых трудноуловимых ошибок Fortran. Историческое наследие языка таково: процедуру можно объявить вне всякого contains и вне модуля — просто как отдельную единицу компиляции. Тогда в точке вызова компилятор не имеет ни малейшего представления о её сигнатуре. Он не знает, сколько у неё аргументов, какого они типа, какие у них атрибуты intent и являются ли они скалярами или массивами. Всё, что у него есть, — это имя. В результате он генерирует вызов «вслепую», передавая то, что вы написали, ровно так, как если бы это было правильно.
Представьте, что внешняя функция ожидает аргумент типа real (четыре байта), а вы по ошибке передали ей integer или, хуже того, real двойной точности (восемь байт). При наличии интерфейса компилятор немедленно остановит сборку. Без интерфейса он молча сложит в стек неверное число байт, процедура прочитает их по-своему, и вы получите не аварийное завершение, а тихо испорченные числа — иногда правдоподобные, иногда абсурдные. Такие баги особенно коварны тем, что проявляются далеко от места ошибки и часто лишь на отдельных платформах или уровнях оптимизации.
Ещё опаснее рассинхронизация по самому количеству аргументов или по способу передачи массива: внешняя процедура, объявленная с массивом неизвестной формы, не получит дескриптор с границами, и попытка работать с современным предполагаемо-формованным массивом (vec(:)) попросту не скомпонуется правильно. Именно поэтому существует историческая практика дублировать сигнатуру внешней процедуры вручную в интерфейсном блоке (interface ... end interface) — это способ вернуть компилятору знание о сигнатуре. Но ручной интерфейсный блок легко рассинхронизировать с реальной процедурой, и тогда мы возвращаемся к исходной проблеме. Размещение процедуры в contains или в модуле решает всё это автоматически: интерфейс генерируется из самого определения и не может разойтись с ним.
Процедуры в Fortran глазами программиста на C и Python
Чтобы прочнее усвоить разделение на subroutine и function, полезно сопоставить его с привычными языками. В C нет такого деления вовсе: там есть единственная сущность — функция. Процедуру-действие в C изображают функцией с типом возврата void, а несколько результатов возвращают, передавая указатели на переменные вызывающего. Иными словами, то, что Fortran выражает явной парой «subroutine + intent(out)», в C достигается ручной работой с указателями, причём язык никак не помогает отличить вход от выхода — это остаётся на совести программиста и комментариев.
В Python картина иная: там тоже единая конструкция def, но любая функция может вернуть кортеж, поэтому несколько результатов отдают просто через return a, b. Идея «subroutine» в Python почти не нужна — функция, ничего не возвращающая явно, отдаёт None и работает как процедура-действие за счёт побочных эффектов. При этом Python вообще не проверяет типы аргументов при компиляции (её и нет), тогда как Fortran с явным интерфейсом ловит несоответствие типов ещё до запуска. Таблица ниже сводит эти различия воедино.
| Аспект | Fortran | C | Python |
| Действие vs значение | отдельные subroutine и function | только функция (void для действия) | только def (None для действия) |
| Несколько результатов | через intent(out)-аргументы | через указатели вручную | через кортеж в return |
| Проверка типов аргументов | да, при явном интерфейсе | да, по прототипу в заголовке | нет (динамическая) |
| Вход/выход в сигнатуре | явно через intent | не выражается языком | не выражается языком |
Из этого сравнения видно, в чём сильная сторона подхода Fortran: разделение на два вида процедур и атрибуты intent делают намерение программиста частью самого языка, а не пожеланием в комментарии. Компилятор не просто переводит код — он становится соавтором проверки контракта. Эта философия «явных контрактов» пронизывает весь раздел и объясняет, почему столько внимания уделяется, казалось бы, формальным деталям объявления процедур.
Как работает под капотом
Что происходит при вызове процедуры на уровне машины? Создаётся кадр стека (stack frame): в стек кладётся адрес возврата (куда вернуться после процедуры), под локальные переменные выделяется место, аргументы передаются (в Fortran — обычно как адреса, см. следующий урок). Управление передаётся в тело процедуры; по достижении end кадр сворачивается, и выполнение продолжается с адреса возврата. Разница между function и subroutine на этом уровне невелика — функция дополнительно возвращает значение (часто в регистре). «Явный интерфейс» — понятие времени компиляции: когда процедура внутренняя (в contains) или модульная, компилятор видит её полную сигнатуру и при каждом вызове сверяет число, типы и атрибуты аргументов, вставляя при необходимости преобразования или выдавая ошибку. Внешняя процедура без описанного интерфейса лишает компилятор этих знаний — он вынужден «верить» вызову на слово, и рассогласование типов проявится лишь как порча данных в рантайме. Именно поэтому явный интерфейс — не формальность, а защита.
Частые ошибки
- Вызов функции через
call.call f(x)для функции — ошибка; функцию вызывают в выражении:y = f(x). - Вызов субрутины в выражении.
y = swap(a, b)неверно; субрутину вызываютcall swap(a, b). - Забытое объявление типа результата функции. Тип результата должен быть объявлен; иначе сработает неявная типизация (если нет
implicit none). - Внешние процедуры без интерфейса. Лишают проверки типов; размещайте процедуры в
containsили модулях. - Побочные эффекты в функции. Функция, меняющая глобальное состояние, ломает интуицию
y = f(x); держите функции чистыми.
Итоги
subroutineвыполняет действие и вызывается операторомcall.functionвычисляет значение и используется в выражении, какsin(x).- Современный синтаксис функции — с
result(имя); тип результата объявляют явно. - Процедуры размещают после
contains(внутренние) или в модулях. - Такое размещение даёт явный интерфейс — компилятор проверяет типы аргументов.
- Правило выбора: «чему равно?» — функция, «сделай!» — субрутина; функции держите чистыми.