Functions & Subroutines

Procedures are where Fortran code starts to be reusable. The language offers two kinds — functions, which compute one return value, and subroutines, which perform effects — and a set of attributes (intent, pure, elemental, optional) that turn a pile of legacy calls into a checked, optimizable interface. This lesson builds that interface vocabulary.

Functions vs subroutines

A function is called in an expression and yields one value; its name acts as the result variable. A subroutine is invoked with call, returns nothing by name, and communicates through its arguments. The rule of thumb from the scientific-computing style guides: use functions for pure math (y = my_sqrt(x)), and subroutines for anything that writes, allocates, or moves data.

program procedure_demo
  implicit none
  real :: a, b
  a = 3.0
  b = 4.0
  print *, hypot2(a, b)        ! function call inside an expression
  call report(a, b)            ! subroutine call as a statement
contains
  ! A function: returns one value through its name.
  real function hypot2(x, y)
    real, intent(in) :: x, y
    hypot2 = sqrt(x*x + y*y)
  end function hypot2

  ! A subroutine: performs an effect, returns nothing by name.
  subroutine report(x, y)
    real, intent(in) :: x, y
    print '(a,2f6.2)', 'args: ', x, y
  end subroutine report
end program procedure_demo

Notice the two spellings of the function header: legacy real function hypot2(x, y) and modern function hypot2(x, y) result(r) with an explicit result name, which is preferred when a result name clarifies the code or is needed for recursion.

INTENT: the argument contract

Fortran passes arguments by reference — the callee sees the caller's actual variable. intent(in) promises the callee only reads it (the compiler may copy it behind your back, enabling huge optimization wins), intent(out) declares the callee writes it fresh, and intent(inout) means read-modify-write. Declaring intent turns bugs like accidentally overwriting an input into compile-time errors.

INTENT data-flow contract diagram

Fig. 1 — INTENT is a legal contract the compiler enforces and optimizes against.

Optional and keyword arguments

Arguments may be declared optional, and every argument may be passed by keyword: call report(x=a, y=b). Keyword calling frees you from memorizing positional order and makes each argument self-documenting at the call site. The intrinsic present(name) tests at runtime whether an optional argument was supplied — the idiom for defaults.

module stats
  implicit none
contains
  function mean(x, mask) result(m)
    real, intent(in) :: x(:)
    logical, intent(in), optional :: mask(:)   ! per-element filter
    real :: m
    logical :: keep(size(x))
    if (present(mask)) then
       keep = mask
    else
       keep = .true.                 ! default: use every element
    end if
    m = sum(x, mask=keep) / count(keep)
  end function mean
end module stats

program optional_demo
  use stats
  implicit none
  real :: data(5) = [1.0, 2.0, 3.0, 4.0, 100.0]
  print *, mean(data)                     ! uses every element: 22.0
  print *, mean(data, mask=data < 10.0)   ! rejects the outlier:  2.5
end program optional_demo

Pure and elemental procedures

pure promises a procedure has no side effects: it reads arguments, computes, returns — it never writes globals or performs I/O. The compiler verifies the promise, and then do concurrent and OpenMP can safely call it from many threads at once. elemental is the sharper tool: an elemental function takes scalar arguments but is applied element-wise when given arrays — one body, both scalar and array callers:

module physics
  implicit none
contains
  ! One body; callable with scalars AND with arrays.
  elemental real function fahrenheit(c)
    real, intent(in) :: c
    fahrenheit = c * 9.0 / 5.0 + 32.0
  end function fahrenheit
end module physics

program elemental_demo
  use physics
  implicit none
  real :: city(3) = [-5.0, 12.0, 27.0]
  print *, fahrenheit(city)      ! whole array in one call
  print *, fahrenheit(city(1))   ! scalar call, same body
end program elemental_demo

Recursion and placement

Functions that call themselves declare recursive (gfortran accepts it even without the keyword, but the standard and other compilers require it). The classic factorial is short but illuminating: watch the call stack grow, then unwind multiplying.

recursive function fact(n) result(r)
  integer, intent(in) :: n
  integer :: r
  if (n <= 1) then
     r = 1
  else
     r = n * fact(n - 1)          ! the recursive step
  end if
end function fact

Where procedures live decides how they are shared. Internal procedures sit in a unit's contains block and see the host's variables (host association) — convenient, but hidden from other files. Module procedures live in a module, are usable by any unit that uses the module, and their interfaces are checked automatically at every call site — the default choice for anything reused. External procedures are the pre-module legacy; they demand hand-written interface blocks and are the source of most "unexpected argument types" bugs in old code.

Takeaway: every procedure gets an intent on every argument; math becomes elemental where possible, pure where it can be, and lives in a module the moment a second file needs it.