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.

Аспектfunctionsubroutine
Назначениевычислить значениевыполнить действие
Вызовв выражении: 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 с явным интерфейсом ловит несоответствие типов ещё до запуска. Таблица ниже сводит эти различия воедино.

АспектFortranCPython
Действие 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 (внутренние) или в модулях.
  • Такое размещение даёт явный интерфейс — компилятор проверяет типы аргументов.
  • Правило выбора: «чему равно?» — функция, «сделай!» — субрутина; функции держите чистыми.
Проверьте себя
1. Чем function отличается от subroutine?
AНичем, это синонимы
Bfunction вычисляет значение и используется в выражении, subroutine выполняет действие и вызывается через call
Cfunction нельзя передавать аргументы
Dsubroutine всегда быстрее
2. Что даёт размещение процедуры после contains?
AУскорение
BЯвный интерфейс: компилятор проверяет типы и число аргументов в точке вызова
CАвтоматическую рекурсию
DГлобальную видимость переменных везде
3. Как правильно вызвать subroutine swap(a, b)?
Aswap(a, b)
By = swap(a, b)
Ccall swap(a, b)
Dreturn swap(a, b)