Procedures, Functions and Subroutines
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3 final draft J3/23-007r1) and was checked on 2026-10-18
with GNU Fortran 16.2 and LLVM Flang 23. Procedures are the unit of reuse in Fortran, and most of what makes modern
Fortran code safe and fast is decided in a procedure’s interface: which arguments it takes, which of them it may change
(intent), which may be missing (optional), whether it is allowed to have side effects (pure, elemental,
simple), and whether the compiler is allowed to check any of that at the call. The page follows the order in which
those ideas build on each other: the two kinds of subprogram and where they live, explicit and implicit interfaces,
argument association, intent and value, optional and keyword arguments (including the Fortran 2023 conditional
argument), function results, recursion, the procedure prefixes, internal procedures and host association, character
arguments, procedures as arguments, and the aliasing rules that let a compiler optimise across a call.
Modules, which are the recommended home for procedures, are covered on Modules and Submodules; generic interfaces, defined operators and procedure pointers on Interfaces, Generics and Operator Overloading; array dummy arguments (assumed shape, assumed size, assumed rank) on Array Arguments: Assumed Shape, Size and Rank; and type-bound procedures on Type-Bound Procedures, Abstract Types and Finalization.
Every example belongs to the heatflow scenario (a two-dimensional heat-diffusion solver) and uses its names
(heatflow_* modules, dp for real64), but each one is a complete, self-contained file that can be extracted and
run on its own. Unless a paragraph says otherwise, every example compiles without warnings under
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all and was also built and run with LLVM Flang 23,
and the output shown is what it printed. Examples that are deliberately wrong are labelled as non-conforming in the text
and in a comment on their first line, and the compiler messages or run-time failures they produce are shown.
Two kinds of subprogram
Fortran has exactly two kinds of subprogram. A function is invoked from inside an expression and delivers a value
through its result; a subroutine is invoked by a call statement and communicates through its arguments. Both are
defined by a subprogram (a function or subroutine statement, a specification part, an execution part, an optional
contains part and an end statement) and both can take dummy arguments, which are the names a procedure uses for
the actual arguments supplied at a call.
[prefix] function name([dummy-arguments]) [result(result-name)]
specification part ! declarations, use, import, implicit none ...
execution part
[contains
internal subprograms]
end function [name]
[prefix] subroutine name([dummy-arguments])
specification part
execution part
[contains
internal subprograms]
end subroutine [name]
call name(actual-arguments) ! invokes a subroutine
x = name(actual-arguments) + 1.0 ! invokes a function inside an expression
The points that distinguish the two, and that the rest of the page relies on:
-
A function result is a variable. The type of the result can be written in the
functionstatement (real(dp) function f(x)) or by declaring the result name in the specification part; it cannot be given both ways. Without aresultclause the result variable has the function’s own name, and inside the function that name always means the result variable, never a recursive call. Withresult(r), the function name refers to the function itself andris the variable that holds the value (J3/23-007r1, 15.6.2.2). A function must define its result before it returns (unless the result is a pointer, whose association status must not be undefined). -
A subroutine statement cannot carry a type (constraint C1574); a subroutine has no result.
-
The end name is optional but checked.
end function stable_dtmust repeat the function’s name if it names anything (constraints C1573 and C1576), which catches a misplacedendearly. -
returnis rarely needed. Execution ends at theendstatement;returnis only useful for an early exit. Fortran has noreturn valueform, so an early exit from a function assigns the result first. -
A function with no dummy arguments is still referenced with parentheses:
default_dt(). Without them the name would denote the function, not a reference to it. -
Prefixes appear before
functionorsubroutineand are listed in the next table (R1529 and R1530). Each may appear at most once (C1552).
| Prefix | Meaning | Rules |
|---|---|---|
|
No side effects visible outside the procedure. |
Constraints C1592 to C15110 (clause 15.7). Required to call a procedure from |
|
Written for scalars, applicable to arrays of any rank. Implies |
All dummy arguments are scalar non-pointer non-allocatable (C15118); the result is scalar (C15119). Clause 15.9. |
|
Only valid with |
Cannot be combined with |
|
A pure procedure that also does not reference variables from outside through use or host association. |
Fortran 2023, clause 15.8. A simple procedure may also be |
|
Advisory only since Fortran 2018: every procedure is recursive unless it says otherwise. |
Cannot be combined with |
|
The procedure promises that it never invokes itself, directly or indirectly. |
Fortran 2018. A processor may use static storage for its local variables. |
|
Marks the definition of a separate module procedure (a procedure whose interface was declared elsewhere). |
C1556 to C1560; see Modules and Submodules. |
type specification |
Functions only: |
Constraint C1574 forbids it on a subroutine. |
Where procedures live
A procedure’s place of definition decides how it is found, what interface callers see and which variables it can reach. The standard distinguishes the classes in the following table (J3/23-007r1, 15.2.2).
| Class | Defined by | Interface seen by callers | Reaches host variables? |
|---|---|---|---|
Intrinsic |
The processor ( |
Always explicit. |
Not applicable. |
External |
A subprogram that is not inside another program unit. Compiled and linked independently. |
Implicit in the caller unless an interface body (or a |
No. An external procedure sees only its own scope and whatever it accesses through its own |
Module |
A subprogram in the |
Explicit everywhere the module is used. |
It sees the module’s entities (this is host association with the module). |
Internal |
A subprogram in the |
Explicit, but visible only inside the host. |
Yes: every name of the host is visible (host association) unless a local declaration hides it. |
Dummy procedure |
A dummy argument that is a procedure ( |
Explicit if declared with an interface, implicit if only given |
Not applicable. |
Procedure pointer |
A pointer with the |
As declared. |
Not applicable. |
Statement function |
One-line |
Always implicit. |
Yes. |
The diagram shows why module procedures are the default recommendation in the
fortran-lang Best Practices guide: the explicit
interface comes for free with use, the compiler checks every call, and nothing has to be written twice. External
procedures survive in libraries (BLAS, LAPACK), in code that crosses a language boundary, and in legacy source, and they
are always called through an interface that someone had to write or leave out.
A module, an internal and an external procedure side by side
The first example shows all three in one file: average4 and relax_point are module procedures, twice is an internal
function of the main program, and report is an external subroutine that the program sees only through an external
statement. The program uses implicit none (type, external) (Fortran 2018), which turns a missing declaration of an
external procedure into a compile-time error instead of a silent implicit interface.
! Module, internal and external procedures in one file, with explicit interfaces.
module heatflow_stencil
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: average4, relax_point
contains
! Module function: the interface is explicit wherever the module is used.
function average4(north, south, east, west) result(mean)
real(dp), intent(in) :: north, south, east, west
real(dp) :: mean
mean = 0.25_dp*(north + south + east + west)
end function average4
! Module subroutine: updates one grid point and reports how much it moved.
subroutine relax_point(u, i, j, change)
real(dp), intent(inout) :: u(:, :)
integer, intent(in) :: i, j
real(dp), intent(out) :: change
real(dp) :: old
old = u(i, j)
u(i, j) = average4(u(i, j + 1), u(i, j - 1), u(i + 1, j), u(i - 1, j))
change = abs(u(i, j) - old)
end subroutine relax_point
end module heatflow_stencil
program procedure_kinds
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_stencil, only: relax_point
implicit none (type, external)
real(dp) :: u(3, 3), change
external :: report ! an external procedure: only its name is known here
u = 0.0_dp
u(2, 1) = 100.0_dp ! one hot neighbour of the centre point
call relax_point(u, 2, 2, change)
call report(u(2, 2), change)
print '(a,f6.2)', 'doubled by an internal function: ', twice(u(2, 2))
contains
! Internal function: sees the host's names (host association), has an explicit interface.
function twice(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = 2.0_dp*x
end function twice
end program procedure_kinds
! External subroutine: compiled separately in real projects; callers see only an implicit interface.
subroutine report(value, change)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp), intent(in) :: value, change
print '(a,f6.2,a,f6.2)', 'centre = ', value, ', moved by ', change
end subroutine report
centre = 25.00, moved by 25.00
doubled by an internal function: 50.00
Things to notice in the example:
-
relax_pointcallsaverage4without any declaration: both are in the same module, so each sees the other’s explicit interface through host association. They arepublic/privateas the module decides, which is covered on the modules page. -
reportis written below the program only so that the file runs. In a real build it lives in a separate file, and the caller then knows only thatreportis an external procedure. Even when both are in one file, the standard does not oblige the compiler to check the call against the later definition. -
twiceuses nothing from the program, but it could readuandchangedirectly: that is host association. -
The statement
implicit noneof the module also applies to every module procedure, because module and internal subprograms inherit the implicit-typing mapping of their host (J3/23-007r1, 8.7). External subprograms and interface bodies do not inherit it, soreportrepeats the statement.
Explicit and implicit interfaces
The interface of a procedure is everything a caller needs in order to invoke it: its name, its dummy argument names and
characteristics (type, kind, rank, intent, optional, …), and, for a function, the characteristics of the result.
An interface is explicit when the compiler can see all of that at the call; it is implicit when it can see only what
the call itself says (J3/23-007r1, 15.4.2.1). The interface is explicit for internal, module and intrinsic procedures, for
a procedure defined in the same scoping unit as its reference (a subroutine, or a function with a separate result name),
and for a procedure declared by an interface body or by a procedure declaration with an interface. It is implicit
otherwise; the interface of a statement function is always implicit.
An explicit interface is not merely a convenience. The standard requires one whenever the call uses a feature that needs information the call itself cannot carry (J3/23-007r1, 15.4.2.2):
| The call or the procedure has … | Why an implicit interface cannot work |
|---|---|
a reference with an argument keyword ( |
Keywords name dummy arguments; the caller must be able to see the names. |
a dummy argument that is |
These change how the argument is passed (descriptors, absent arguments, copies). |
an assumed-shape or assumed-rank dummy array, a coarray dummy, a polymorphic dummy, or a dummy of a parameterized derived type |
The caller must pass a descriptor, not just an address. |
an array, pointer or allocatable result, or a character result whose length is not constant |
The caller must know the shape or length to receive the result. |
the |
The caller must be able to check the property. |
Without an explicit interface, an argument mismatch is not an error that any compiler is obliged to find, because the
compiler cannot know what the callee expects. With one, the checks are mandatory. The next file is deliberately
non-conforming: the dummy arguments are real(dp) and the actual arguments are a default integer and a default real,
plus one argument missing. A module procedure has an explicit interface, so gfortran stops at compile time.
! INTENTIONALLY NON-CONFORMING: the actual argument types do not match the dummy arguments.
module heatflow_checks
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
contains
function stable_dt(alpha, dx) result(dt)
real(dp), intent(in) :: alpha, dx
real(dp) :: dt
dt = dx**2/(4.0_dp*alpha)
end function stable_dt
end module heatflow_checks
program interface_mismatch
use heatflow_checks
implicit none
print *, stable_dt(1, 0.1) ! default integer and default real instead of real(dp)
print *, stable_dt(1.0_dp) ! missing actual argument
end program interface_mismatch
02a_error_explicit_interface_mismatch.f90:16:10:
16 | print *, stable_dt(1, 0.1) ! default integer and default real instead of real(dp)
| 1
Error: Type mismatch in argument 'alpha' at (1); passed INTEGER(4) to REAL(8)
02a_error_explicit_interface_mismatch.f90:16:10:
16 | print *, stable_dt(1, 0.1) ! default integer and default real instead of real(dp)
| 1
Error: Type mismatch in argument 'dx' at (1); passed REAL(4) to REAL(8)
02a_error_explicit_interface_mismatch.f90:17:10:
17 | print *, stable_dt(1.0_dp) ! missing actual argument
| 1
Error: Missing actual argument for argument 'dx' at (1)
Flang 23 reports the same three problems with its own wording. If a project truly needs mismatching calls to an external
procedure (a generic message-passing routine, say), the standard-conforming tool is an explicit interface with an
assumed-type dummy type(*), not the legacy escape -fallow-argument-mismatch, which GNU Fortran offers only to
degrade this error to a warning and recommends against.
Interface blocks and import
For an external procedure that is not in a module, an interface body provides the explicit interface. It repeats the
procedure’s function or subroutine statement, the declarations of its dummy arguments and result, and the matching
end statement, and nothing else. An interface body is a scoping unit of its own: it does not inherit the host’s
use associations, kinds or types. If it needs a kind such as dp that the host obtained with use, it has to repeat
the use or bring the name in with an import statement (J3/23-007r1, 8.8). The four forms:
| Statement | Effect | Standard |
|---|---|---|
|
Makes the listed host entities accessible in the interface body. |
Fortran 2003 |
|
Like the list form, and additionally nothing else is imported; if any |
Fortran 2018 |
|
Nothing from the host is accessible. This is already the default for an ordinary interface body; the statement is useful where host access is the default instead (a |
Fortran 2018 |
|
Every host entity is accessible; a local declaration that would hide a host entity is an error (C8106). It guards against accidental shadowing. |
Fortran 2018 |
A bare import (no list) makes every host entity accessible; it is the default for module procedure interface bodies,
internal subprograms and derived-type definitions. import cannot appear in a main program, a module, an external
subprogram or a block data program unit (C8100), because those have no host.
The example declares three external procedures with three styles of import. The use statement of a scope must come
before import (the compiler message when it does not is explicit about it), and the external procedures are defined
below the main program only to keep the file runnable.
! An interface body describes an external procedure; import controls what it can see.
program interface_import
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
interface
! import, only: the body may use dp from the host and nothing else
function legacy_norm(n, x) result(r)
import, only: dp
integer, intent(in) :: n
real(dp), intent(in) :: x(n)
real(dp) :: r
end function legacy_norm
! import, none: the body brings its own use statement (use comes first), nothing else is inherited
subroutine legacy_fill(n, x, v)
use, intrinsic :: iso_fortran_env, only: wp => real64
import, none
integer, intent(in) :: n
real(wp), intent(out) :: x(n)
real(wp), intent(in) :: v
end subroutine legacy_fill
! import, all (Fortran 2018): every entity of the host is accessible, so a local name
! that clashes with a host name would be an error.
function legacy_scale(x, factor) result(y)
import, all
real(dp), intent(in) :: x, factor
real(dp) :: y
end function legacy_scale
end interface
real(dp) :: temperatures(4)
call legacy_fill(size(temperatures), temperatures, 20.0_dp)
temperatures(3) = 24.0_dp
write (*, '(a,f8.4)') 'norm = ', legacy_norm(size(temperatures), temperatures)
write (*, '(a,f8.4)') 'scaled = ', legacy_scale(temperatures(3), 0.5_dp)
end program interface_import
! The two external procedures described above (normally in another file or library).
function legacy_norm(n, x) result(r)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
integer, intent(in) :: n
real(dp), intent(in) :: x(n)
real(dp) :: r
r = sqrt(sum(x**2))
end function legacy_norm
subroutine legacy_fill(n, x, v)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
integer, intent(in) :: n
real(dp), intent(out) :: x(n)
real(dp), intent(in) :: v
x = v
end subroutine legacy_fill
function legacy_scale(x, factor) result(y)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp), intent(in) :: x, factor
real(dp) :: y
y = x*factor
end function legacy_scale
norm = 42.1426
scaled = 12.0000
implicit none (external) and missing declarations
Since Fortran 2018, implicit none can be written with a list: implicit none (type) is the classic form, implicit none
(external) additionally demands that every external or dummy procedure used in the scope has an explicit interface or
the external attribute (J3/23-007r1, 8.7, constraint C899), and implicit none (type, external) does both. Plain
implicit none is the same as implicit none (type). The next file is non-conforming: it calls an external subroutine
that has neither.
! INTENTIONALLY NON-CONFORMING under implicit none (external): no interface, no external statement.
program missing_external
implicit none (type, external)
real :: x
call report(x)
end program missing_external
02c_error_missing_external.f90:5:16:
5 | call report(x)
| 1
Error: Procedure 'report' called at (1) is not explicitly declared
What an implicit interface lets through
An implicit interface checks nothing across separately compiled files. The pair of files below is the textbook disaster:
the callee is correct (it expects one real(dp) value), the caller is syntactically fine (it passes an array of two
default reals to a procedure declared only with external), and every compiler accepts both files. The program is
non-conforming and its behaviour is undefined; on a little-endian IEEE machine the callee reads the eight bytes of the
two single-precision numbers as one double. Compile the files separately, as a build system would:
! File 1 of 2. A perfectly correct external subroutine: it expects one real(dp) value.
subroutine show_value(x)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp), intent(in) :: x
print '(a,es22.15)', 'show_value received ', x
end subroutine show_value
! File 2 of 2. INTENTIONALLY NON-CONFORMING: with only an external statement the interface is implicit,
! so nothing checks that two default reals are passed where one real(dp) is expected.
program implicit_interface_caller
implicit none (type, external)
external :: show_value
real :: pair(2)
pair = [1.0, 2.0]
call show_value(pair)
end program implicit_interface_caller
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 02d_error_implicit_interface_callee.f90
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 02d_error_implicit_interface_caller.f90
gfortran 02d_error_implicit_interface_callee.o 02d_error_implicit_interface_caller.o -o implicit_demo
./implicit_demo
show_value received 2.000000473111868E+00
Source: J3/23-007r1 (Fortran 2023), 15.5.2.5 Ordinary dummy variables, and 15.4.2.1 Interfaces and scopes
Neither -Wall nor -Wextra complains here. GNU Fortran has two diagnostics aimed at this situation:
-Wimplicit-interface warns at every call that uses an implicit interface (it does not check that interfaces agree across
files), and -Wimplicit-procedure warns about a call to a procedure that has neither an explicit interface nor an
external declaration. Within a single source file GNU Fortran does compare a call with a definition it has already seen
and reports a mismatch as an error, but that is a courtesy and it cannot happen
across separately compiled files. The cure is structural:
put the callee in a module so that its interface is explicit, or write an interface body that is shared through a module.
Argument association
When a procedure is invoked, each dummy argument becomes associated with an actual argument for the duration of the
call. Fortran does not say how: it says what a program may observe (J3/23-007r1, 15.5.2.4). In practice a compiler
passes the address of a scalar or contiguous array, or a descriptor of an assumed-shape array, so a dummy argument
without the value attribute is an alias of the actual argument, not a copy of it. Two consequences follow, and both
surprise programmers who come from languages with call-by-value:
-
A change made through the dummy argument is a change to the caller’s variable at once, not when the procedure returns.
-
Note 2 to 15.5.2.4 says that argument association usually resembles call by reference combined with call by value-result, and leaves the mechanism to the processor. A compiler is free to make a temporary copy on entry and copy it back on exit (copy-in/copy-out) when that is the only way to present contiguous storage to the callee. A program that is correct under the rules of the standard cannot tell the difference, and a program that depends on one of them is non-conforming (see the aliasing section below).
! Argument association: by reference, by value, and copy-in/copy-out for a non-contiguous section.
module heatflow_args
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: scale_in_place, bump_copy, scale_explicit_shape, scale_assumed_shape
contains
! Without value, the dummy argument is associated with the actual argument (no copy is made).
subroutine scale_in_place(x, factor)
real(dp), intent(inout) :: x
real(dp), intent(in) :: factor
x = x*factor
end subroutine scale_in_place
! With value, the dummy argument is a local copy that may be modified freely.
subroutine bump_copy(x)
real(dp), value :: x
x = x + 1.0_dp
print '(a,f5.1)', ' inside bump_copy, x = ', x
end subroutine bump_copy
! Explicit-shape dummy: it requires contiguous storage, so a strided actual is copied in and out.
subroutine scale_explicit_shape(n, x, factor)
integer, intent(in) :: n
real(dp), intent(inout) :: x(n)
real(dp), intent(in) :: factor
x = x*factor
end subroutine scale_explicit_shape
! Assumed-shape dummy: the array descriptor carries the stride, no copy is needed.
subroutine scale_assumed_shape(x, factor)
real(dp), intent(inout) :: x(:)
real(dp), intent(in) :: factor
x = x*factor
end subroutine scale_assumed_shape
end module heatflow_args
program argument_association
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_args
implicit none
real(dp) :: t, row(8)
integer :: k
t = 20.0_dp
call scale_in_place(t, 1.5_dp)
print '(a,f5.1)', 'after scale_in_place, t = ', t
call bump_copy(t)
print '(a,f5.1)', 'after bump_copy, t = ', t
row = [(real(k, dp), k=1, 8)]
print '(a)', 'assumed-shape dummy with a strided section (no copy):'
call scale_assumed_shape(row(1:8:2), 10.0_dp)
print '(8f6.1)', row
print '(a)', 'explicit-shape dummy with a strided section (copy-in/copy-out):'
call scale_explicit_shape(4, row(2:8:2), 10.0_dp)
print '(8f6.1)', row
end program argument_association
The output of the program is shown first, followed by what gfortran wrote to the error unit; the -fcheck=array-temps
check, which is part of -fcheck=all, prints the warning once per call site that needs a temporary. The same program
under Flang prints the same numbers and no warning.
after scale_in_place, t = 30.0
inside bump_copy, x = 31.0
after bump_copy, t = 30.0
assumed-shape dummy with a strided section (no copy):
10.0 2.0 30.0 4.0 50.0 6.0 70.0 8.0
explicit-shape dummy with a strided section (copy-in/copy-out):
10.0 20.0 30.0 40.0 50.0 60.0 70.0 80.0
At line 61 of file 04_argument_association.f90
Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'scale_explicit_shape'
What the example shows:
-
scale_in_placechangestin the caller: the dummy argument istunder another name. -
bump_copyhas thevalueattribute, so it works on an anonymous copy initialised from the actual argument; the increment is invisible to the caller.valuemay not be combined withintent(out)orintent(inout), withallocatable,pointerorvolatile(constraint C868), and it cannot be a coarray or an assumed-size array (C867).intent(in)withvalueis legal and, for a scalar, simply forbids the callee from assigning to its copy.valueis also how Fortran receives C-style arguments passed by value (see Interoperability with C). -
scale_assumed_shapetakes a stride-2 section without any copy because an assumed-shape dummy argument is passed with a descriptor that records the stride.scale_explicit_shapedeclaresx(n), which promises contiguous storage; the sectionrow(2:8:2)is not contiguous, so the compiler built a temporary, called the procedure, and copied the result back. That is correct but slow in a hot loop, and the reason why the performance advice for array arguments is to prefer assumed-shape dummies (or addcontiguouswhen the callee needs contiguity and the caller usually has it). GNU Fortran can also report temporaries at compile time with-Warray-temporaries.
The detailed rules for array dummy arguments, including sequence association and the contiguous attribute, are on
Array Arguments: Assumed Shape, Size and Rank.
intent: stating how an argument is used
The intent attribute states what the procedure intends to do with a dummy argument (J3/23-007r1, 8.5.10). It documents
the interface, it lets the compiler reject mistakes, and it lets the optimiser rely on the facts it states.
| Attribute | Meaning |
|---|---|
|
The dummy argument may be used but must neither be defined nor become undefined during the call (C846: it cannot appear in a variable definition context). The actual argument may be any expression, including a constant. |
|
The dummy argument becomes undefined on entry, except for components that have default initialization. The actual argument must be definable. An allocatable actual argument is deallocated on entry (9.7.3.2); a pointer’s association status becomes undefined. |
|
The value is used and may be changed. The actual argument must be definable. |
(none) |
The dummy argument is subject to the limits of whatever it is associated with: it may be defined only if the actual argument is definable, and the compiler cannot reason about it. |
Points that go beyond the table:
-
intent(inout)is not the same as nointent: an actual argument forinoutmust always be definable, whereas with nointentit need be definable only when the callee actually defines it. Pass a literal constant to anintent(inout)dummy argument and the compiler rejects the call. -
intent(out)means the result value is entirely determined by the procedure. If a procedure may leave an argument unchanged and the argument should then keep its value,intent(inout)is the right choice, becauseintent(out)would have made it undefined (Note 4 of 8.5.10). -
For allocatable and pointer dummy arguments,
intentapplies to the allocation status or the association as well as the value.intent(out)on an allocatable deallocates the actual argument on entry;intent(in)on a pointer forbids changing its association, but the target remains modifiable (Note 3). -
A nonpointer dummy argument with
intent(in)is not modifiable as a whole or in part, including every subobject. A pointer component of anintent(in)derived-type dummy can still have its target changed. -
Pure procedures need
intentfor every nonpointer dummy argument (C1592 for functions, C1595 for subroutines). Elemental procedures need it for every dummy argument that does not havevalue(C15120). -
intentis only available for dummy data objects and dummy procedure pointers; a plain dummy procedure has none (C845).
The example below demonstrates intent(inout), intent(out) with default initialization, intent(out) with an
allocatable, and intent(in). run_state_t has default initialization for both components, so every call to start_run
resets the caller’s variable before the first statement of the callee executes.
! What intent(in), intent(out) and intent(inout) mean, and what intent(out) does to its actual argument.
module heatflow_intent
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: run_state_t, start_run, advance, reset_buffer
type :: run_state_t
integer :: steps = 0 ! default initialization
real(dp) :: residual = huge(1.0_dp) ! default initialization
end type run_state_t
contains
! intent(out) with a derived type that has default initialization:
! on entry the dummy argument is reset to the defaults, whatever the caller had.
subroutine start_run(state)
type(run_state_t), intent(out) :: state
print '(a,i0,a)', ' start_run sees steps = ', state%steps, ' on entry'
end subroutine start_run
! intent(inout): the value is used and updated.
subroutine advance(state, new_residual)
type(run_state_t), intent(inout) :: state
real(dp), intent(in) :: new_residual ! intent(in): cannot be assigned here
state%steps = state%steps + 1
state%residual = new_residual
end subroutine advance
! intent(out) with an allocatable dummy: the actual argument is deallocated on entry.
subroutine reset_buffer(buffer, n)
real(dp), allocatable, intent(out) :: buffer(:)
integer, intent(in) :: n
print '(a,l1,a)', ' reset_buffer sees allocated(buffer) = ', allocated(buffer), ' on entry'
allocate (buffer(n), source=0.0_dp)
end subroutine reset_buffer
end module heatflow_intent
program intent_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_intent
implicit none
type(run_state_t) :: state
real(dp), allocatable :: buffer(:)
call start_run(state)
call advance(state, 0.5_dp)
call advance(state, 0.25_dp)
print '(a,i0,a,f5.2)', 'after two steps: steps = ', state%steps, ', residual = ', state%residual
print '(a)', 'second start_run on the same variable:'
call start_run(state)
print '(a,i0)', 'steps is back to ', state%steps
allocate (buffer(5), source=1.0_dp)
print '(a)', 'reset_buffer on an allocated array:'
call reset_buffer(buffer, 3)
print '(a,i0,a,f4.1)', 'size = ', size(buffer), ', sum = ', sum(buffer)
end program intent_demo
start_run sees steps = 0 on entry
after two steps: steps = 2, residual = 0.25
second start_run on the same variable:
start_run sees steps = 0 on entry
steps is back to 0
reset_buffer on an allocated array:
reset_buffer sees allocated(buffer) = F on entry
size = 3, sum = 0.0
An assignment to an intent(in) dummy argument is a constraint violation, and every compiler reports it. The next file is
non-conforming:
! INTENTIONALLY NON-CONFORMING: assigning to an intent(in) dummy argument.
subroutine clamp_residual(residual, limit)
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp), intent(inout) :: residual
real(dp), intent(in) :: limit
if (residual > limit) limit = residual ! wrong: limit is intent(in)
end subroutine clamp_residual
03a_error_intent_in.f90:7:25:
7 | if (residual > limit) limit = residual ! wrong: limit is intent(in)
| 1~~~~~
Error: Dummy argument 'limit' with INTENT(IN) in variable definition context (assignment) at (1)
Common intent mistakes
-
Forgetting that
intent(out)discards the old value. A subroutine that returns early without assigning anintent(out)scalar leaves the caller’s variable undefined. If the old value must survive when nothing is computed, useintent(inout). -
intent(out)and derived types. A derived type with default initialization is reset on every call; a type without default initialization comes back undefined, component by component. -
intent(out)on a finalizable argument. A nonpointer, nonallocatableintent(out)dummy argument is finalized before it becomes undefined (7.5.6.3); pure procedures therefore forbid it when finalization would call an impure procedure (C1596), and forbid polymorphicintent(out)arguments altogether (C1597). -
No
intentat all. The compiler cannot warn about assignments to something that should be read-only, a reader cannot tell input from output, and the optimiser can assume less. -
An unused dummy argument.
-Wunused-dummy-argument, implied by-Wall, reports a dummy argument that is never referenced, which usually means an interface that drifted from its body.
Optional and keyword arguments
A dummy argument with the optional attribute need not have an actual argument at every call (J3/23-007r1, 8.5.12). The
intrinsic function present(x) tells the procedure whether it has one. Calls can then name the arguments they supply
with argument keywords (name = value), so that optional arguments can be skipped and the call documents itself.
The rules, all of which need an explicit interface at the call (15.4.2.2):
-
Arguments are matched first by position and then by keyword. After the first keyword, every later actual argument must have a keyword too (C1531). The keyword is the name of the dummy argument in the interface the caller sees, which may differ from the name in the callee’s own source if the caller sees an interface body (C1532).
-
Exactly one actual argument corresponds to each nonoptional dummy argument; at most one to each optional dummy argument (15.5.2.1).
-
An absent optional argument must not be referenced or defined. It must not be used as the target of a pointer assignment, invoked (if it is a procedure), used as the selector of
associate,select typeorselect rank, or passed on to a nonoptional dummy argument. The only things that can be done with it arepresent(x), passing it to another optional dummy argument (in which case it is absent there too). -
A dummy argument is also absent when its actual argument is an unallocated allocatable or a disassociated pointer and the dummy argument is neither allocatable nor a pointer. This turns an allocatable variable into an optional switch: allocated means "supplied" (15.5.2.13).
-
presentis available for anyoptionaldummy argument, including nested forwarding through several procedures.
The next example gives describe_run three optional numeric arguments and one optional string, and shows keywords in any
order, skipped arguments, the unallocated-allocatable rule and forwarding an absent argument.
! optional dummy arguments, present, keyword calls and forwarding of absent arguments.
module heatflow_options
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: describe_run, stable_step
real(dp), parameter :: default_alpha = 1.0_dp
contains
! Optional dummy arguments need an explicit interface at the call (here: a module procedure).
subroutine describe_run(nx, ny, alpha, tolerance, label)
integer, intent(in) :: nx
integer, intent(in), optional :: ny
real(dp), intent(in), optional :: alpha, tolerance
character(len=*), intent(in), optional :: label
integer :: ny_used
real(dp) :: alpha_used
! Never reference an absent argument; choose a default first.
ny_used = nx
if (present(ny)) ny_used = ny
alpha_used = default_alpha
if (present(alpha)) alpha_used = alpha
print '(a,i0,a,i0,a,f4.2)', 'grid ', nx, ' x ', ny_used, ', alpha = ', alpha_used
if (present(label)) print '(2a)', ' label: ', label
if (present(tolerance)) then
if (tolerance > 0.0_dp) print '(a,es8.1)', ' tolerance: ', tolerance
end if
end subroutine describe_run
! Forwarding: an absent optional actual argument may be passed on to another optional dummy.
function stable_step(dx, alpha, safety) result(dt)
real(dp), intent(in) :: dx
real(dp), intent(in), optional :: alpha, safety
real(dp) :: dt
dt = scaled_step(dx, alpha)
if (present(safety)) dt = safety*dt
end function stable_step
function scaled_step(dx, alpha) result(dt)
real(dp), intent(in) :: dx
real(dp), intent(in), optional :: alpha
real(dp) :: dt
dt = dx**2/(4.0_dp*default_alpha)
if (present(alpha)) dt = dx**2/(4.0_dp*alpha)
end function scaled_step
end module heatflow_options
program optional_keyword
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_options
implicit none
real(dp), allocatable :: tolerance_override
call describe_run(64)
call describe_run(64, 32, 0.5_dp)
call describe_run(64, tolerance=1.0e-6_dp, label='coarse run') ! keywords may skip optional arguments
call describe_run(label='fine run', nx=128, alpha=0.25_dp) ! and may appear in any order
! An unallocated allocatable actual argument is treated as absent by an optional, non-allocatable dummy.
call describe_run(32, tolerance=tolerance_override)
tolerance_override = 1.0e-8_dp
call describe_run(32, tolerance=tolerance_override)
print '(a,es10.3)', 'step, defaults: ', stable_step(0.1_dp)
print '(a,es10.3)', 'step, alpha=2 safety=.5:', stable_step(0.1_dp, safety=0.5_dp, alpha=2.0_dp)
end program optional_keyword
grid 64 x 64, alpha = 1.00
grid 64 x 32, alpha = 0.50
grid 64 x 64, alpha = 1.00
label: coarse run
tolerance: 1.0E-06
grid 128 x 128, alpha = 0.25
label: fine run
grid 32 x 32, alpha = 1.00
grid 32 x 32, alpha = 1.00
tolerance: 1.0E-08
step, defaults: 2.500E-03
step, alpha=2 safety=.5: 6.250E-04
The absent-optional trap
Because an absent argument must not even be referenced, an expression that always evaluates both operands is unsafe.
merge(alpha, 1.0_dp, present(alpha)) looks like a neat way to supply a default, but merge is an ordinary function:
its arguments are evaluated before it is called, so alpha is referenced even when absent. The same applies to
present(x) .and. x > 0: Fortran does not guarantee short-circuit evaluation, and a processor may evaluate x > 0
first (J3/23-007r1, 10.1.7). The safe forms are a nested if, or assigning a default to a local variable first, as
describe_run does. The next program is non-conforming and fails at run time:
! INTENTIONALLY NON-CONFORMING: merge evaluates both value arguments, so an absent optional is referenced.
program absent_optional
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
call show()
contains
subroutine show(alpha)
real(dp), intent(in), optional :: alpha
real(dp) :: used
used = merge(alpha, 1.0_dp, present(alpha)) ! alpha is referenced even when it is absent
print '(f5.2)', used
end subroutine show
end program absent_optional
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.
Backtrace for this error:
...
Segmentation fault
The backtrace lines are omitted. Neither -fcheck=all nor Flang diagnosed the mistake at compile time or run time; both
programs ended with a segmentation fault, which is only one of the possible outcomes of undefined behaviour.
Conditional arguments (Fortran 2023)
Fortran 2023 adds conditional expressions (cond ? a : b) and conditional arguments that may end in .nil., a
consequent that means "pass no argument at all" (J3/23-007r1, 10.1.2.3 and 15.5.1; 15.5.2.3 gives the semantics). A
conditional argument chooses among its consequents by evaluating the logical conditions in order; if the chosen consequent
is .nil. the corresponding optional dummy argument is absent. It replaces the pattern in which the caller wraps a call in
an if construct once per combination of optional arguments, or defines a wrapper procedure that forwards the possibly
missing argument. The rules in short:
-
The whole conditional argument is parenthesised; only the consequents that are evaluated are evaluated (unlike
merge), and at least one consequent must be an expression or variable, never only.nil.(C1540). -
.nil.is allowed only for anoptionaldummy argument. -
All consequent arguments have the same declared type, kind and rank (C1538, C1539). If the dummy argument is
intent(out)orintent(inout), every consequent must be a variable (C1541). -
Nesting of conditional arguments is not allowed; a consequent may be a conditional expression.
The next file uses the new syntax. It is valid Fortran 2023 according to J3/23-007r1 but it is not accepted by either compiler used for this page, so it has not been run; the two diagnostics are shown below it. It is the only example on the page that has not been compiled.
! FORTRAN 2023 SYNTAX (conditional expression and conditional argument, J3/23-007r1 10.1.4 and 15.5.2.3).
! NOT COMPILED SUCCESSFULLY: gfortran 16.2 and LLVM Flang 23 both reject it (see the diagnostics on the page).
program conditional_arguments
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp) :: alpha
logical :: alpha_known
alpha = 0.5_dp
alpha_known = .true.
! The second actual argument is absent when alpha_known is false (.nil.).
call show(64, (alpha_known ? alpha : .nil.))
contains
subroutine show(nx, alpha)
integer, intent(in) :: nx
real(dp), intent(in), optional :: alpha
if (present(alpha)) then
print '(i0,a,f4.2)', nx, ' with alpha ', alpha
else
print '(i0,a)', nx, ' with the default alpha'
end if
end subroutine show
end program conditional_arguments
05a_conditional_arguments_f2023.f90:12:45:
12 | call show(64, (alpha_known ? alpha : .nil.))
| 1
Error: Syntax error in expression at (1)
error: Semantic errors in 05a_conditional_arguments_f2023.f90
05a_conditional_arguments_f2023.f90:12:3: error: not yet implemented: Fortran 2023 conditional arguments are not yet supported
call show(64, (alpha_known ? alpha : .nil.))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The first message is from GNU Fortran 16.2, the second from LLVM Flang 23, whose status table also lists conditional
expressions and arguments as not supported. Until your compiler accepts the syntax, write the same behaviour with an if
construct, which works with every Fortran 2008 compiler (this program was compiled and run with both):
! The same behaviour as a conditional argument, written with an if construct (any Fortran 2008 or later compiler).
program conditional_portable
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none (type, external)
real(dp) :: alpha
integer :: pass
alpha = 0.5_dp
do pass = 1, 2
if (pass == 1) then
call show(64, alpha) ! alpha present
else
call show(64) ! alpha absent
end if
end do
contains
subroutine show(nx, alpha)
integer, intent(in) :: nx
real(dp), intent(in), optional :: alpha
if (present(alpha)) then
print '(i0,a,f4.2)', nx, ' with alpha ', alpha
else
print '(i0,a)', nx, ' with the default alpha'
end if
end subroutine show
end program conditional_portable
64 with alpha 0.50
64 with the default alpha
Support differs between vendors; the compiler-support table on History, Standards and Compilers lists which compilers accept conditional expressions and arguments.
Function results
A function returns whatever type and shape its result variable has: a scalar of an intrinsic type, a derived type, an
array, an allocatable or a pointer. The result is declared like a local variable, either through the type in the
function statement or by name in the specification part, and if it is an array, allocatable or a pointer this must
be said in the specification part (J3/23-007r1, 15.6.2.2).
-
Derived-type results return several values at once without
intent(out)arguments. -
Array results may be explicit-shape, with extents given by specification expressions of the dummy arguments, such as an array
x(n)of typereal(dp). An array result with a shape that depends on the contents of an argument cannot be explicit-shape; it must beallocatable(or a pointer). -
Allocatable results (Fortran 2003 and later) are allocated by the function and released automatically once the expression that used them has been evaluated. In the body the first assignment
p = u(row, :)allocatespwith the right extent (automatic reallocation on assignment), so noallocatestatement is needed. -
Character results are either a constant or specification-expression length (
character(len=len(text))) or a deferred-length allocatable (character(len=:), allocatable). An assumed-length resultcharacter(*)function is obsolescent and is covered with the legacy features below. -
Pointer results are allowed; the pointer’s association status must not be undefined on return. They are rarely the right choice, because the target’s lifetime is then the caller’s problem.
-
A function that does not define its (nonpointer) result on every path is non-conforming. GNU Fortran finds the cases that are visible to its data-flow analysis only when optimising: with
-Wall -O1it reports that the result "may be used uninitialized" (-Wmaybe-uninitialized) or "is used uninitialized" (-Wuninitialized).
All of these require an explicit interface at the call (J3/23-007r1, 15.4.2.2). return ends the function early; the result
variable keeps whatever it held at that point, so assign it first (hot_cells below does).
! Function results: scalar, array, allocatable, character, derived type, and a function without arguments.
module heatflow_results
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: stats_t, grid_stats, grid_spacing, profile, run_label, hot_cells, default_dt
type :: stats_t
real(dp) :: lowest, highest, mean
end type stats_t
contains
! Derived-type result: several values from one call without intent(out) arguments.
function grid_stats(u) result(s)
real(dp), intent(in) :: u(:, :)
type(stats_t) :: s
s = stats_t(minval(u), maxval(u), sum(u)/real(size(u), dp))
end function grid_stats
! Explicit-shape array result: its extent is a specification expression of a dummy argument.
function grid_spacing(n, length) result(x)
integer, intent(in) :: n
real(dp), intent(in) :: length
real(dp) :: x(n)
integer :: i
x = [(length*real(i - 1, dp)/real(n - 1, dp), i=1, n)]
end function grid_spacing
! Assumed-shape arguments cannot size a result, so the result is allocatable (Fortran 2003 and later).
function profile(u, row) result(p)
real(dp), intent(in) :: u(:, :)
integer, intent(in) :: row
real(dp), allocatable :: p(:)
p = u(row, :) ! allocated on assignment to the right extent
end function profile
! Deferred-length allocatable character result.
function run_label(nx, ny) result(text)
integer, intent(in) :: nx, ny
character(len=:), allocatable :: text
character(len=32) :: buffer
write (buffer, '(i0,a,i0)') nx, 'x', ny
text = 'heatflow-' // trim(buffer)
end function run_label
! A function without dummy arguments is still referenced with an empty argument list: default_dt().
function default_dt() result(dt)
real(dp) :: dt
dt = 1.0e-4_dp
end function default_dt
! The result variable can be used like any local variable before the function returns.
function hot_cells(u, limit) result(count)
real(dp), intent(in) :: u(:, :)
real(dp), intent(in) :: limit
integer :: count
count = 0
if (size(u) == 0) return ! return leaves the function with the current result value
count = count_if_above(u, limit)
end function hot_cells
pure function count_if_above(u, limit) result(n)
real(dp), intent(in) :: u(:, :)
real(dp), intent(in) :: limit
integer :: n
n = count(u > limit)
end function count_if_above
end module heatflow_results
program function_results
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_results
implicit none
real(dp) :: u(3, 4)
type(stats_t) :: s
integer :: i, j
u = reshape([((real(10*i + j, dp), i=1, 3), j=1, 4)], shape(u))
s = grid_stats(u)
print '(a,3f7.2)', 'low/high/mean: ', s%lowest, s%highest, s%mean
print '(a,5f6.2)', 'x: ', grid_spacing(5, 1.0_dp)
print '(a,4f6.1)', 'row 2: ', profile(u, 2)
print '(2a)', 'label: ', run_label(64, 48)
print '(a,es9.2)', 'dt: ', default_dt()
print '(a,i0)', 'cells above 25: ', hot_cells(u, 25.0_dp)
print '(a,i0)', 'length of the label: ', len(run_label(8, 8))
end program function_results
low/high/mean: 11.00 34.00 22.50
x: 0.00 0.25 0.50 0.75 1.00
row 2: 21.0 22.0 23.0 24.0
label: heatflow-64x48
dt: 1.00E-04
cells above 25: 4
length of the label: 12
Recursion
A procedure is recursive if it can invoke itself, directly or through another procedure. Each invocation gets its own
set of dummy arguments and unsaved local variables (an "instance", 15.6.2.4), while variables with the save attribute (including
anything initialised in its declaration, which implies save) are shared by every instance.
Since Fortran 2018 every procedure is recursive by default: the recursive prefix no longer changes the meaning of a program, and
non_recursive is the new way to promise that a procedure never invokes itself (15.6.2.1). Before Fortran 2018 a
procedure that called itself had to be marked recursive, and a directly recursive function also needed a result clause
so that its own name could be told apart from its result variable. Without result, the function name inside the body is
always the result variable, so fact(n - 1) would be a reference to a scalar with a subscript, not a call.
Support is not uniform. GNU Fortran 16.2 still requires recursive for a procedure that calls itself and rejects
non_recursive; LLVM Flang 23 accepts both and checks non_recursive. The safe, portable style is to write recursive on
any procedure that recurses, along with result:
! Recursion: recursive prefix, result clause, mutual recursion, and the cost of saved locals.
module heatflow_recursion
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: coarsen_levels, refine_until, is_even, is_odd
contains
! Number of times a grid of n points per side can be halved while staying at least 3 points wide.
! A directly recursive function needs a result clause so that its name can be referenced as a function.
recursive function coarsen_levels(n) result(levels)
integer, intent(in) :: n
integer :: levels
if (n < 5) then
levels = 0
else
levels = 1 + coarsen_levels((n + 1)/2)
end if
end function coarsen_levels
! A recursive subroutine halving a step until it meets a stability limit.
recursive subroutine refine_until(dt, limit, halvings)
real(dp), intent(inout) :: dt
real(dp), intent(in) :: limit
integer, intent(inout) :: halvings
if (dt <= limit) return
dt = dt/2.0_dp
halvings = halvings + 1
call refine_until(dt, limit, halvings)
end subroutine refine_until
! Indirect (mutual) recursion between two module procedures.
recursive function is_even(n) result(r)
integer, intent(in) :: n
logical :: r
if (n == 0) then
r = .true.
else
r = is_odd(n - 1)
end if
end function is_even
recursive function is_odd(n) result(r)
integer, intent(in) :: n
logical :: r
if (n == 0) then
r = .false.
else
r = is_even(n - 1)
end if
end function is_odd
end module heatflow_recursion
program recursion_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_recursion
implicit none
real(dp) :: dt
integer :: halvings
print '(a,i0)', 'levels for 129 points per side: ', coarsen_levels(129)
dt = 0.01_dp
halvings = 0
call refine_until(dt, 1.0e-4_dp, halvings)
print '(a,i0,a,es9.3)', 'halvings: ', halvings, ', final dt = ', dt
print '(a,l1,a,l1)', 'is_even(10) = ', is_even(10), ', is_odd(10) = ', is_odd(10)
end program recursion_demo
levels for 129 points per side: 6
halvings: 7, final dt = 7.813E-05
is_even(10) = T, is_odd(10) = F
Practical points:
-
Direct recursion without
recursiveon gfortran 16.2 is a compile-time error ("Function 'fact' at (1) cannot be called recursively, as it is not RECURSIVE"); Flang 23 accepts it and prints the result. Declaring a procedurenon_recursiveand then recursing is an error in Flang ("NON_RECURSIVE procedure 'fact' cannot call itself"), and a syntax error in gfortran 16.2. -
Indirect recursion is where compilers disagree at run time. A procedure that is called back through a callback or a procedure pointer may be re-entered without any visible self-call, as the next file shows. gfortran 16.2 only knows a procedure is recursive if it carries the
recursiveprefix (or-frecursiveis given);-fcheck=recursion, part of-fcheck=all, makes a re-entry into a non-recursive procedure a run-time error. The file below is conforming Fortran 2018 and later but fails under gfortran 16.2 because of this convention; Flang runs it correctly. -
Stack and static storage. Local arrays of a non-recursive procedure may be placed in static storage by some compilers, especially above a size limit (
-fmax-stack-var-size); recursion needs automatic (stack) storage.-frecursivemakes GNU Fortran allocate every local array on the stack and cannot be combined with-fmax-stack-var-size=or-fno-automatic. Deep recursion on large locals can exhaust the stack: the symptom is a segmentation fault, not a Fortran error message.
! Indirect recursion through a callback. Conforming since Fortran 2018 (procedures are recursive by
! default), but gfortran 16.2 still treats a procedure without the recursive prefix as non-recursive.
module walker
implicit none
abstract interface
subroutine visitor_i(depth)
integer, intent(in) :: depth
end subroutine visitor_i
end interface
contains
subroutine walk(depth, visit)
integer, intent(in) :: depth
procedure(visitor_i) :: visit
call visit(depth)
end subroutine walk
end module walker
program indirect_recursion
use walker
implicit none (type, external)
call walk(0, descend)
contains
subroutine descend(depth)
integer, intent(in) :: depth
print '(a,i0)', 'depth ', depth
if (depth < 2) call walk(depth + 1, descend)
end subroutine descend
end program indirect_recursion
The gfortran 16.2 run (the warning is printed at compile time, then the check fires at run time; the backtrace is shortened) and the Flang 23 run:
07b_indirect_recursion_check.f90:26:40:
26 | if (depth < 2) call walk(depth + 1, descend)
| 1
Warning: Non-RECURSIVE procedure 'descend' at (1) is possibly calling itself recursively. Declare it RECURSIVE or use '-frecursive'
depth 0
At line 11 of file 07b_indirect_recursion_check.f90
Fortran runtime error: Recursive call to nonrecursive procedure 'walk'
Error termination. Backtrace:
...
depth 0
depth 1
depth 2
The warning message in the first output shows that GNU Fortran treats the program as suspect; adding recursive to
walk and descend makes it run under gfortran, and also under every older standard.
Purity: pure, elemental, impure elemental and simple
The prefixes pure, elemental, impure and simple are promises about side effects. They exist so that a compiler
(and a human) can call a procedure in contexts where the order of evaluation is unspecified or parallel: inside
do concurrent, inside the expressions of an array statement, and inside other pure procedures. The summary:
| Kind | What it promises | Typical use |
|---|---|---|
|
No variable outside the procedure is defined (no assignment to host- or use-associated variables, no |
Numerical kernels, helper functions called from |
|
|
Scalar formulas that should work for scalars, vectors and matrices alike ( |
|
Elemental without the purity promise: it may read or write files, change module variables, or call impure procedures. Elements are processed in array element order. |
Logging, counters, random numbers, applying an action to every element of an array. |
|
|
Library kernels that must not depend on global state, and procedures that a compiler can reorder, vectorise or offload freely. A simple procedure may also be |
The constraints on pure procedures
The summary in Note 4 of 15.7 says it best: a pure subprogram cannot contain any operation that could conceivably assign
to a variable the caller can see, perform external file I/O, or execute an image control statement (including stop).
Note the word conceivably: a pure subprogram is not allowed to contain an assignment to a global variable even in a branch
that never executes. The main constraints (C1592 to C15110):
-
Every nonpointer dummy argument of a pure function has
intent(in)orvalue; every nonpointer dummy argument of a pure subroutine has an explicitintent. Dummy procedures must be pure. -
No local variable has
saveorvolatile. Initialization in a declaration impliessave, sointeger :: n = 0is not allowed in a pure procedure. -
No variable that is accessed by use or host association, is an
intent(in)dummy argument, is incommonor is a coindexed object may appear in a variable definition context, as the target of a pointer assignment, or as an actual argument for a pointer dummy argument. -
No
print,open,close,backspace,endfile,rewind,flush,wait,inquire, and noreadorwritewith an external unit or*. Internal I/O (reading and writing acharactervariable) is allowed. -
Every procedure referenced, including through defined operators, defined assignment, defined I/O and finalization, is pure. All internal procedures of a pure procedure are pure.
-
The interface must be explicit and say
pureat every place where purity is required, such as the body of ado concurrentconstruct.
Because an elemental procedure is pure unless it says impure, the same constraints apply to it, plus the elemental rules:
scalar, nonpointer, nonallocatable, noncoarray dummy arguments and result (C15118, C15119), and an intent for every
dummy argument (C15120). A nonintrinsic elemental procedure cannot be used as an actual argument (C1533); wrap it in a
nonelemental procedure first. A nonelemental pure procedure can be passed as a dummy-procedure argument (see
"Procedures as arguments" below), provided the dummy procedure is declared pure in its interface.
An elemental procedure applied to arrays
In a reference to an elemental procedure with at least one array argument, all array arguments must be conformable, scalar
arguments are broadcast, and the result has the shape of the arrays (15.9.2). Every actual argument corresponding to an
intent(out) or intent(inout) dummy argument must then be an array. The array actual arguments are considered to be
associated with the scalar dummy arguments throughout the entire elemental reference, so the aliasing restrictions of
15.5.2.14 apply to the arrays as a whole.
! pure, elemental and impure elemental procedures, and their use inside do concurrent.
module heatflow_physics
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: stable_dt, fourier_number, celsius_to_kelvin, clamp_temperature, log_reading, readings_logged
integer :: readings_logged = 0
contains
! pure: no side effects, so it may be called from do concurrent and from other pure procedures.
pure function stable_dt(alpha, dx, dy) result(dt)
real(dp), intent(in) :: alpha, dx, dy
real(dp) :: dt
dt = 0.5_dp/(alpha*(1.0_dp/dx**2 + 1.0_dp/dy**2))
end function stable_dt
! elemental: written for scalars, applied element by element to arrays of any rank.
elemental function fourier_number(alpha, dt, dx) result(fo)
real(dp), intent(in) :: alpha, dt, dx
real(dp) :: fo
fo = alpha*dt/dx**2
end function fourier_number
elemental function celsius_to_kelvin(t) result(k)
real(dp), intent(in) :: t
real(dp) :: k
k = t + 273.15_dp
end function celsius_to_kelvin
! elemental subroutine with an intent(inout) argument.
elemental subroutine clamp_temperature(t, lo, hi)
real(dp), intent(inout) :: t
real(dp), intent(in) :: lo, hi
t = min(max(t, lo), hi)
end subroutine clamp_temperature
! impure elemental: allowed to do I/O and to change module state; elements are processed in array element order.
impure elemental subroutine log_reading(t)
real(dp), intent(in) :: t
readings_logged = readings_logged + 1
print '(a,i0,a,f6.1)', ' reading ', readings_logged, ': ', t
end subroutine log_reading
end module heatflow_physics
program pure_elemental
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_physics
implicit none
real(dp) :: dt_limit(4), u(2, 3)
integer :: i
! The elemental function works on a scalar, a vector and a matrix with the same source code.
print '(a,f6.4)', 'Fourier number (scalar): ', fourier_number(1.0_dp, 0.002_dp, 0.1_dp)
print '(a,4f7.4)', 'Fourier numbers (dt varies): ', fourier_number(1.0_dp, [0.001_dp, 0.002_dp, 0.003_dp, 0.004_dp], 0.1_dp)
u = reshape([-5.0_dp, 0.0_dp, 20.0_dp, 40.0_dp, 80.0_dp, 120.0_dp], shape(u))
call clamp_temperature(u, 0.0_dp, 100.0_dp)
print '(a,6f6.1)', 'clamped: ', u
print '(a,6f7.2)', 'kelvin: ', celsius_to_kelvin(u)
! A pure function is allowed in do concurrent.
do concurrent (i = 1:4)
dt_limit(i) = stable_dt(real(i, dp), 0.1_dp, 0.1_dp)
end do
print '(a,4es10.2)', 'stable dt: ', dt_limit
print '(a)', 'impure elemental subroutine applied to an array:'
call log_reading([21.5_dp, 22.0_dp, 22.5_dp])
print '(a,i0)', 'readings logged: ', readings_logged
end program pure_elemental
Fourier number (scalar): 0.2000
Fourier numbers (dt varies): 0.1000 0.2000 0.3000 0.4000
clamped: 0.0 0.0 20.0 40.0 80.0 100.0
kelvin: 273.15 273.15 293.15 313.15 353.15 373.15
stable dt: 2.50E-03 1.25E-03 8.33E-04 6.25E-04
impure elemental subroutine applied to an array:
reading 1: 21.5
reading 2: 22.0
reading 3: 22.5
readings logged: 3
Things to notice: fourier_number is written once and called with a scalar, then with a vector where dt varies and
alpha and dx are broadcast; clamp_temperature is an elemental subroutine whose intent(inout) argument
is an array, so lo and hi are broadcast; log_reading is impure elemental, which is the only way to put print or a
counter into an elemental procedure; and stable_dt is pure, which makes the reference inside do concurrent legal
(see do concurrent and GPU Offload).
The standard states that the elements of an elemental reference are processed as if in array element order for subroutines
and function elements (15.9.2, 15.9.3), which is what makes impure elemental output predictable here. For pure elemental calls the
order is deliberately unobservable, since a pure procedure has no side effects; that is the freedom a compiler uses to
vectorise or parallelise the loop.
Violating purity
A pure subprogram with a side effect is a constraint violation that every compiler reports. The next module is non-conforming; it modifies a module variable and does external I/O from a pure function.
! INTENTIONALLY NON-CONFORMING: a pure function with side effects.
module bad_pure
implicit none
integer :: calls = 0
contains
pure function counted_square(x) result(y)
real, intent(in) :: x
real :: y
calls = calls + 1 ! modifies a module variable
print *, 'squaring', x ! external I/O
y = x*x
end function counted_square
end module bad_pure
08a_error_impure_in_pure.f90:10:48:
10 | print *, 'squaring', x ! external I/O
| 1
Error: PRINT statement at (1) not allowed within PURE procedure
08a_error_impure_in_pure.f90:9:5:
9 | calls = calls + 1 ! modifies a module variable
| 1~~~~~
Error: Variable 'calls' cannot appear in a variable definition context (assignment) at (1) in PURE procedure
The messages are the gfortran 16.2 output. Flang 23 reports the same two problems ("Left-hand side of assignment is not definable … 'calls' may not be defined in pure subprogram" and "External I/O is not allowed in a pure subprogram").
simple procedures (Fortran 2023)
simple is new in Fortran 2023. A simple procedure is pure and in addition cannot reference variables from outside through
use or host association (a named constant is not a variable, so unit_alpha below is fine), cannot use common, cannot
have an entry, and may call only simple procedures. All intrinsic functions, all functions of the intrinsic modules,
and the subroutines mvbits, split and tokenize are simple (WG5 N2212, clause 7.1). It is a stronger promise than pure: a pure procedure may read a module
variable, a simple one depends only on its arguments, so a compiler can evaluate it in any order, on any image or on any
device.
GNU Fortran 16.2 does not accept the simple prefix. LLVM Flang 23 does accept it, so the example below was built and run
with Flang only. The Flang status table still lists simple procedures as unsupported, and in practice the prefix is
parsed but its extra constraints are not checked: Flang compiled the non-conforming module that follows
the example without a diagnostic. A conforming program must not rely on that.
! Fortran 2023 simple procedures (J3/23-007r1 15.8). Accepted by LLVM Flang 23, rejected by gfortran 16.2.
module heatflow_simple
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: fourier_number, scaled
real(dp), parameter :: unit_alpha = 1.0_dp
contains
! simple: pure, and in addition it cannot reference host- or use-associated variables
! (named constants are fine), so it depends only on its arguments.
simple elemental function fourier_number(dt, dx) result(fo)
real(dp), intent(in) :: dt, dx
real(dp) :: fo
fo = unit_alpha*dt/dx**2
end function fourier_number
simple function scaled(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = 2.0_dp*fourier_number(x, 0.1_dp) ! a simple procedure may call only simple procedures
end function scaled
end module heatflow_simple
program simple_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_simple
implicit none (type, external)
print '(a,3f6.2)', 'Fourier numbers: ', fourier_number([0.001_dp, 0.002_dp, 0.003_dp], 0.1_dp)
print '(a,f6.2)', 'scaled: ', scaled(0.001_dp)
end program simple_demo
Fourier numbers: 0.10 0.20 0.30
scaled: 0.20
The gfortran 16.2 diagnostic for the first simple line is "Error: Unclassifiable statement at (1)" followed by cascading
errors. The next module breaks constraint C15115 by reading a variable (alpha) through host association from a simple
function; a conforming compiler must reject it. Flang 23 accepted it silently (the file was compiled with flang -c):
! INTENTIONALLY NON-CONFORMING Fortran 2023: a simple function that references a module variable.
module heatflow_simple_bad
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: alpha = 1.0_dp ! a variable, not a named constant
contains
simple function fourier_number(dt, dx) result(fo)
real(dp), intent(in) :: dt, dx
real(dp) :: fo
fo = alpha*dt/dx**2 ! not allowed: alpha is accessed by host association
end function fourier_number
end module heatflow_simple_bad
Until simple is widely available, use pure and keep to the discipline yourself: a pure procedure that touches no
module variable is simple in everything but name, and can be re-labelled when your toolchain allows it.
Internal procedures and host association
An internal procedure is defined after the contains statement of a main program, an external subprogram or a module
subprogram, and is called an internal subprogram of its host. It has an explicit interface, and it is visible only
inside the host. Everything declared in the host, including its dummy arguments, local variables, types and the host’s
own use associations, is accessible in the internal procedure by host association (J3/23-007r1, 19.5.1.4) unless an
internal declaration of the same name hides it. Because the internal procedure shares the host’s variables rather than
receiving them as arguments, it works like a lightweight closure over its host’s state: it sees the current value of a
host variable every time it runs.
The rules:
-
An internal subprogram cannot itself contain an internal subprogram (constraints C1572 and C1575). Nesting stops at one level; a module procedure may contain internal procedures, an internal procedure may not.
-
An internal procedure’s name is not accessible outside the host, so it cannot be called from other program units.
-
The host’s
implicit noneis inherited, so internal procedures need not repeat it (8.7). -
importin an internal procedure restricts host association:import, noneblocks it,import, only: ...limits it (8.8). It is a good way to keep a "pure computation" internal procedure from touching state by accident. -
An internal procedure may be an actual argument (Fortran 2008) and may be the target of a procedure pointer. In both cases it remains associated with the instance of the host that was active when the association was made (15.6.2.4). Once the host returns, using the pointer is undefined; a procedure pointer to an internal procedure must not outlive its host.
-
Internal procedures are the natural place for helpers that need the host’s state and are not worth a module-level interface. When they grow or are reused, move them to the module and pass the data as arguments.
! Internal procedures: host association, shadowing, and the rule that they are visible only to their host.
module heatflow_report
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: summarize
contains
subroutine summarize(u, unit_label)
real(dp), intent(in) :: u(:, :)
character(len=*), intent(in) :: unit_label
real(dp) :: lo, hi
lo = minval(u)
hi = maxval(u)
call show('minimum', lo)
call show('maximum', hi)
call show('spread', spread_of())
call show('half range', half_range())
contains
! show uses unit_label from the host through host association.
subroutine show(name, value)
character(len=*), intent(in) :: name
real(dp), intent(in) :: value
print '(a,a,f7.2,1x,a)', name, ': ', value, unit_label
end subroutine show
! spread_of uses lo and hi from the host: it always sees their current values.
function spread_of() result(d)
real(dp) :: d
d = hi - lo
end function spread_of
! A local variable of the same name hides the host's variable inside this procedure.
function half_range() result(h)
real(dp) :: h
real(dp) :: lo ! hides the host's lo
lo = (hi - minval(u))/2.0_dp
h = lo
end function half_range
end subroutine summarize
end module heatflow_report
program internal_procedures
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_report, only: summarize
implicit none (type, external)
real(dp) :: u(2, 3)
u = reshape([12.5_dp, 15.0_dp, 17.5_dp, 20.0_dp, 25.0_dp, 30.0_dp], shape(u))
call summarize(u, 'degC')
! call show('x', 1.0_dp) would be an error here: show is visible only inside summarize
end program internal_procedures
minimum: 12.50 degC
maximum: 30.00 degC
spread: 17.50 degC
half range: 8.75 degC
The example shows host association of unit_label, lo and hi; a local variable lo in half_range that hides the
host’s lo; and an internal procedure called only from its host (a call to show from the main program would be rejected
because the name is not accessible there). A common source of bugs is the accidental shadowing the standard’s Note 2 of 8.8
describes: assigning to an undeclared name inside an internal procedure under implicit typing creates a local; with
implicit none it is an error instead, and with import, all no host name can ever be hidden by accident.
Nesting an internal procedure inside another one is non-conforming:
! INTENTIONALLY NON-CONFORMING: an internal procedure cannot contain another internal procedure.
program nested_internal
implicit none (type, external)
call outer()
contains
subroutine outer()
call inner()
contains
subroutine inner()
print *, 'inner'
end subroutine inner
end subroutine outer
end program nested_internal
09a_error_nested_internal.f90:8:10:
8 | contains
| 1
Error: CONTAINS statement at (1) is already in a contained program unit
09a_error_nested_internal.f90:11:24:
11 | end subroutine inner
| 1
Error: Expected label 'outer' for END SUBROUTINE statement at (1)
09a_error_nested_internal.f90:7:16:
7 | call inner()
| 1
Error: Procedure 'inner' called at (1) is not explicitly declared
The first error is gfortran’s; Flang says "An internal subprogram may not contain an internal subprogram".
An internal procedure as a callback
Since an internal procedure can see its host’s variables, it can be passed to a general-purpose routine as a callback that
"captures" them. integrate is a module function that takes any function matching an abstract interface, and decay
is an internal function of the main program that uses alpha, t and calls from the host. Changing alpha between
the two integrations changes the second result because decay reads the current value.
! Internal procedures: host association, an internal procedure used as a callback, and what it cannot do.
module heatflow_quadrature
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: integrate, scalar_function
abstract interface
function scalar_function(x) result(y)
import, only: dp
real(dp), intent(in) :: x
real(dp) :: y
end function scalar_function
end interface
contains
! Composite trapezoid rule for any function that matches the abstract interface.
function integrate(f, a, b, n) result(area)
procedure(scalar_function) :: f
real(dp), intent(in) :: a, b
integer, intent(in) :: n
real(dp) :: area
real(dp) :: h
integer :: i
h = (b - a)/real(n, dp)
area = 0.5_dp*(f(a) + f(b))
do i = 1, n - 1
area = area + f(a + real(i, dp)*h)
end do
area = area*h
end function integrate
end module heatflow_quadrature
program internal_procedures
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_quadrature
implicit none (type, external)
real(dp), parameter :: pi = 3.14159265358979323846_dp
real(dp) :: alpha, t, energy
integer :: calls
calls = 0
alpha = 0.5_dp
t = 0.05_dp
! decay uses alpha and t from the host through host association and counts its own calls.
energy = integrate(decay, 0.0_dp, pi, 200)
print '(a,f7.4,a,i0)', 'integral of sin(x) exp(-alpha t): ', energy, ', calls: ', calls
alpha = 2.0_dp ! the internal procedure sees the new value: it shares the host's variables
energy = integrate(decay, 0.0_dp, pi, 200)
print '(a,f7.4,a,i0)', 'after changing alpha: ', energy, ', calls: ', calls
contains
function decay(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
calls = calls + 1
y = sin(x)*exp(-alpha*t)
end function decay
end program internal_procedures
integral of sin(x) exp(-alpha t): 1.9506, calls: 201
after changing alpha: 1.8096, calls: 402
On Linux, both GNU Fortran and Flang implement this by creating a small piece of code on the stack at run time (a
trampoline) that binds the host’s frame to the procedure’s address, so the object file is marked as needing an executable
stack and the linker prints a warning ("requires executable stack"). The program still runs and prints the output above. To
avoid the executable stack on GNU Fortran, compile with -ftrampoline-impl=heap (implemented in libgcc for x86_64 Darwin and for x86_64
and aarch64 Linux, and useful on platforms that do not permit an executable stack), or avoid the trampoline altogether by passing the
state as an argument instead:
-
put the data in a derived type and pass it through a
class(*)or fixed-type "context" argument to a module procedure; -
store the data in a module variable (shared state, with all that implies for threads and recursion);
-
use a type-bound procedure with a
type(...)object that carries the state (see Object Oriented Programming).
Character dummy arguments and results
A character dummy argument is almost always declared character(len=*) (or character(*)), meaning that the length is
assumed from the actual argument at each call (J3/23-007r1, 8.5 and 15.5.2.5). Inside the procedure len(text) returns
that length, len_trim the length without trailing blanks, and the dummy argument cannot be larger than what was passed.
The rules:
-
A scalar
character(len=n)dummy argument of fixed lengthnmay be associated with a longer actual argument and then sees the leftmostncharacters, but it must not be longer than the actual argument (15.5.2.5). Assumed length avoids the question altogether. -
The result of a function can have its length from a dummy argument by a specification expression (
character(len=len(text))) or be deferred (character(len=:), allocatable) so that the assignment sets the length. -
A character dummy argument with
intent(out)keeps the caller’s length. Assignment pads a shorter value with blanks and truncates a longer one; a formatted internal write that does not fit is a run-time error ("End of record"), not truncation. -
An
character(*)function result (assumed-length result) is obsolescent (B.3.6) and cannot be recursive; use a deferred-length allocatable result instead.
! Assumed-length character dummy arguments, character results and what len() sees.
module heatflow_text
implicit none
private
public :: banner, shout, fill_label
contains
! character(len=*): the length comes from the actual argument at every call.
function banner(text) result(line)
character(len=*), intent(in) :: text
character(len=:), allocatable :: line
line = repeat('=', len(text) + 4) // new_line('a') // '| ' // text // ' |' // new_line('a') // &
repeat('=', len(text) + 4)
end function banner
! The result length is a specification expression of a dummy argument.
function shout(text) result(upper)
character(len=*), intent(in) :: text
character(len=len(text)) :: upper
integer :: i
do i = 1, len(text)
if (text(i:i) >= 'a' .and. text(i:i) <= 'z') then
upper(i:i) = achar(iachar(text(i:i)) - 32)
else
upper(i:i) = text(i:i)
end if
end do
end function shout
! An assumed-length intent(out) dummy keeps the caller's length. An internal write pads a short text
! with blanks and fails with a runtime error when the text does not fit.
subroutine fill_label(label, nx, ny)
character(len=*), intent(out) :: label
integer, intent(in) :: nx, ny
write (label, '(i0,a,i0)') nx, 'x', ny
end subroutine fill_label
end module heatflow_text
program character_dummies
use heatflow_text
implicit none (type, external)
character(len=12) :: fixed
character(len=:), allocatable :: flexible
print '(a)', banner('heatflow')
print '(3a)', '[', shout('stable dt'), ']'
call fill_label(fixed, 64, 48)
print '(a,i0,3a)', 'fixed: len = ', len(fixed), ', value = "', fixed, '"'
print '(a,i0)', 'len_trim(fixed) = ', len_trim(fixed)
flexible = 'abc '
print '(a,i0,a,i0)', 'flexible: len = ', len(flexible), ', len_trim = ', len_trim(flexible)
flexible = 'abcdefgh'
print '(a,i0)', 'after reassignment, len = ', len(flexible)
end program character_dummies
============
| heatflow |
============
[STABLE DT]
fixed: len = 12, value = "64x48 "
len_trim(fixed) = 5
flexible: len = 5, len_trim = 3
after reassignment, len = 8
Passing a label that is too short to fill_label is non-conforming use (the formatted internal write does not fit) and
stops the program at run time:
! INTENTIONALLY NON-CONFORMING USE: the caller's label is too short for the text written into it.
module heatflow_label
implicit none
private
public :: fill_label
contains
subroutine fill_label(label, nx, ny)
character(len=*), intent(out) :: label
integer, intent(in) :: nx, ny
write (label, '(i0,a,i0)') nx, 'x', ny
end subroutine fill_label
end module heatflow_label
program label_too_short
use heatflow_label, only: fill_label
implicit none (type, external)
character(len=3) :: tiny
call fill_label(tiny, 64, 48)
print '(a)', tiny
end program label_too_short
At line 10 of file 10a_error_label_too_short.f90
Fortran runtime error: End of record
Error termination. Backtrace:
...
The message and the shortened backtrace are gfortran’s; Flang 23 stops with "Excessive output to fixed-size record".
Strings, len_trim, index, trim, deferred-length characters and the Fortran 2023 split and tokenize intrinsics
are covered on Characters and Strings.
Procedures as arguments
A dummy argument may itself be a procedure. The caller passes a procedure name; the callee invokes it through the dummy name. Three declaration styles exist, in order of preference:
| Style | Example | Notes |
|---|---|---|
|
|
Preferred. The interface is declared once in |
|
|
Takes the interface from a procedure that is already accessible. Fortran 2003. |
Interface body inside the specification part |
|
The Fortran 90 spelling. Equivalent, but longer and without reuse. |
|
|
Implicit interface. No argument checking; avoid. Required to be given the |
Rules for the actual argument that is associated with a dummy procedure (15.5.2.10): it must be an external, internal,
module or dummy procedure, a specific intrinsic function listed in the standard’s table of specific names (and
declared intrinsic), or a procedure pointer. A nonintrinsic elemental procedure cannot be an actual argument (C1533).
The characteristics of the actual procedure must agree with the dummy’s interface, including pure if the dummy is
declared pure.
A procedure pointer (declared with procedure(boundary_rule) and the pointer attribute) is the run-time version: it can be associated, nullified,
returned from a function, stored in a derived-type component and passed to another procedure. Dummy procedures and
procedure pointers are covered in full on
Interfaces, Generics and Operator Overloading;
the next example is the part that belongs here.
! Dummy procedures: an abstract interface, procedure dummy arguments and a procedure pointer.
module heatflow_boundary_rules
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: boundary_rule, fixed_value, insulated, apply_left_edge, choose_rule
abstract interface
! One rule decides the value of a ghost point from its interior neighbour.
pure function boundary_rule(interior, setting) result(edge)
import, only: dp
real(dp), intent(in) :: interior, setting
real(dp) :: edge
end function boundary_rule
end interface
contains
pure function fixed_value(interior, setting) result(edge)
real(dp), intent(in) :: interior, setting
real(dp) :: edge
edge = 2.0_dp*setting - interior ! ghost value: the face between the two points sits at the setting
end function fixed_value
pure function insulated(interior, setting) result(edge)
real(dp), intent(in) :: interior, setting
real(dp) :: edge
edge = interior - setting ! prescribed gradient (flux); setting = 0 copies the neighbour
end function insulated
! A dummy procedure: any procedure with a matching interface can be passed.
subroutine apply_left_edge(u, rule, setting)
real(dp), intent(inout) :: u(:, :)
procedure(boundary_rule) :: rule
real(dp), intent(in) :: setting
integer :: j
do j = 1, size(u, 2)
u(1, j) = rule(u(2, j), setting)
end do
end subroutine apply_left_edge
! A procedure pointer chosen at run time.
function choose_rule(name) result(rule)
character(len=*), intent(in) :: name
procedure(boundary_rule), pointer :: rule
select case (name)
case ('dirichlet')
rule => fixed_value
case default
rule => insulated
end select
end function choose_rule
end module heatflow_boundary_rules
program procedure_arguments
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_boundary_rules
implicit none (type, external)
real(dp) :: u(3, 2)
procedure(boundary_rule), pointer :: rule
u = reshape([0.0_dp, 5.0_dp, 9.0_dp, 0.0_dp, 6.0_dp, 9.0_dp], shape(u))
call apply_left_edge(u, fixed_value, 100.0_dp) ! pass a module procedure by name
print '(a,2f7.1)', 'dirichlet 100: ', u(1, :)
rule => choose_rule('neumann')
call apply_left_edge(u, rule, 0.0_dp) ! pass the procedure a pointer designates
print '(a,2f7.1)', 'insulated: ', u(1, :)
print '(a,l1)', 'pointer associated: ', associated(rule)
print '(a,l1)', 'rule is insulated: ', associated(rule, insulated)
end program procedure_arguments
dirichlet 100: 195.0 194.0
insulated: 5.0 6.0
pointer associated: T
rule is insulated: T
The example declares one abstract interface, boundary_rule, with two module procedures that satisfy it. apply_left_edge
takes whatever rule is passed, and the main program passes fixed_value by name, then the rule that choose_rule selected
through a procedure pointer. associated(rule, insulated) tests which target the pointer has.
A callback needs a matching interface
The compiler checks that the actual procedure matches the dummy’s interface only if both are explicit. A callback declared
with an implicit interface (the old external style) accepts any procedure, so a mismatch in argument types or counts
shows up as a crash or as wrong numbers, a frequent problem in legacy numerical libraries. In new code give every callback an
abstract interface, declare it pure if the callee is pure or is called from do concurrent, and pass extra state in
a derived type or a module variable rather than in common.
Aliasing and the rules of argument association
Because a dummy argument is normally an alias of the actual argument, two dummy arguments can be aliases of the same memory. The standard forbids the cases that would prevent optimisation (15.5.2.14), in exchange for letting the compiler assume the opposite. While a dummy argument is associated with an entity:
-
Any action that changes the entity’s value, or its allocation status, must be taken through that dummy argument. The exceptions are a dummy argument with the
pointerattribute, and a scalar, assumed-shape or assumed-rank dummy argument that hastargetbut neitherintent(in)norcontiguousand whose actual argument is a target (coarray cases aside). -
If the value is changed through the dummy argument, the entity must not be referenced by any other path during the call (same exceptions).
-
If the actual arguments of two dummy arguments overlap and neither dummy is a pointer or a target, the overlapping part cannot be defined or redefined through either of them.
Examples from the standard’s notes: after call sub(a(1:5), a(3:9)) the section a(3:5) cannot be defined through either
dummy argument, while a(1:2) and a(6:9) remain definable through their own dummy argument; and if a subroutine init(v)
is called as call init(x) and defines v, then x cannot be referenced by its own name during the call, only through
v, even when x is a module variable that init also accesses by use association. These restrictions let the compiler keep values in registers across a call and vectorise a
loop that writes through one dummy argument and reads through another (J3/23-007r1, 15.5.2.14, Note 4, which names
such optimisations as the purpose of the restrictions).
The compiler need not diagnose a violation; many cannot be detected at all. GNU Fortran does catch the simplest case, an
identical actual argument for an intent(in) and an intent(out) dummy argument. The next program is non-conforming:
it calls an in-place smoothing routine written for two arrays with the same array twice.
! INTENTIONALLY NON-CONFORMING: one variable is the actual argument for an intent(in) and an intent(out) dummy.
module heatflow_alias
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
contains
subroutine smooth(src, dst)
real(dp), intent(in) :: src(:)
real(dp), intent(out) :: dst(:)
integer :: i
dst(1) = src(1)
do i = 2, size(src) - 1
dst(i) = 0.5_dp*src(i) + 0.25_dp*(src(i - 1) + src(i + 1))
end do
dst(size(src)) = src(size(src))
end subroutine smooth
end module heatflow_alias
program same_actual
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_alias
implicit none (type, external)
real(dp) :: u(5)
u = [0.0_dp, 0.0_dp, 8.0_dp, 0.0_dp, 0.0_dp]
call smooth(u, u) ! in-place smoothing through two dummy arguments: not allowed
print '(5f6.2)', u
end program same_actual
12a_error_same_actual.f90:24:14:
24 | call smooth(u, u) ! in-place smoothing through two dummy arguments: not allowed
| 1
Warning: Same actual argument associated with INTENT(IN) argument 'src' and INTENT(OUT) argument 'dst' at (1)
0.00 2.00 4.50 1.12 0.00
Source: J3/23-007r1 (Fortran 2023), 15.5.2.14 Restrictions on entities associated with dummy arguments
The warning is the only diagnostic, and the result is wrong: with two separate arrays the smoothing of [0, 0, 8, 0, 0] is
the symmetric [0, 2, 4, 2, 0], but with one array the loop reads values it has already overwritten. Flang 23 printed
the same wrong numbers with no diagnostic at all. At higher optimisation levels the result could differ again, because the
compiler is entitled to assume that src and dst do not overlap. The conforming ways to update in place are an extra
array (smooth) or a local copy inside a procedure with one dummy argument (smooth_in_place):
! Aliasing rules: an in-place stencil needs a separate output array or a local copy.
module heatflow_alias
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: smooth, smooth_in_place
contains
! Conforming: src and dst are different objects, so the compiler may keep src values in registers.
subroutine smooth(src, dst)
real(dp), intent(in) :: src(:)
real(dp), intent(out) :: dst(:)
integer :: i
dst(1) = src(1)
do i = 2, size(src) - 1
dst(i) = 0.5_dp*src(i) + 0.25_dp*(src(i - 1) + src(i + 1))
end do
dst(size(src)) = src(size(src))
end subroutine smooth
! The in-place version uses one dummy argument and a local copy.
subroutine smooth_in_place(u)
real(dp), intent(inout) :: u(:)
real(dp), allocatable :: old(:)
old = u
call smooth(old, u)
end subroutine smooth_in_place
end module heatflow_alias
program aliasing
use, intrinsic :: iso_fortran_env, only: dp => real64
use heatflow_alias
implicit none (type, external)
real(dp) :: u(5), v(5)
u = [0.0_dp, 0.0_dp, 8.0_dp, 0.0_dp, 0.0_dp]
call smooth(u, v)
print '(a,5f6.2)', 'smooth (two arrays): ', v
call smooth_in_place(u)
print '(a,5f6.2)', 'smooth_in_place: ', u
end program aliasing
smooth (two arrays): 0.00 2.00 4.00 2.00 0.00
smooth_in_place: 0.00 2.00 4.00 2.00 0.00
Source: J3/23-007r1 (Fortran 2023), 15.5.2.14 Restrictions on entities associated with dummy arguments
The target and pointer attributes are the sanctioned exceptions: they tell the compiler that aliasing is possible, at
the cost of some optimisation. See Pointers and Targets.
Function references, side effects and order of evaluation
Rules that matter when functions are used in expressions (J3/23-007r1, 10.1.7 and Annex C.6.1):
-
A processor need not evaluate all operands of an expression if the value can be determined otherwise, which is why a logical expression such as
x > y .or. l(z)may skip the call tol. If the skipped function would have defined an argument, that argument becomes undefined after the expression. -
If a statement contains more than one function reference, they may run in any order, and their values must not depend on that order. A function with side effects therefore must not be used twice in one expression.
-
A function reference that is not pure may not be used where purity is required:
do concurrent, a specification expression, or another pure procedure. -
Prefer pure functions for computation and subroutines for effects. Use
intent(out)for results of a subroutine rather than side effects on module variables. Avoid hidden state: a function that increments a counter in a module works until someone writesf(x) + f(y).
Compiler support and diagnostics
The following was verified on 2026-10-18 with the examples above; the "not verified" cells mean that no compiler of that family was available for this page.
| Feature | GNU Fortran 16.2 | LLVM Flang 23 | Recursion by default (F2018) |
|---|---|---|---|
No: |
Yes. |
|
Rejected (syntax error). |
Accepted and checked. |
|
Accepted. |
Accepted. |
|
Accepted; an undeclared external procedure is an error. |
Accepted; an undeclared external procedure is an error. |
|
Accepted. |
Accepted. |
|
Rejected (syntax error). |
Accepted, but its constraints are not checked. |
Conditional expressions and arguments, |
Rejected (syntax error). |
Rejected ("not yet implemented"). |
The other compilers (Intel ifx, NAG, HPE Cray, NVIDIA HPC SDK) are not covered by this table because none was available for this page; the compiler-support table on History, Standards and Compilers collects their published status.
GNU Fortran options that matter for procedures (see the manual for the full text):
| Option | Effect |
|---|---|
|
Same as |
|
Reports unused dummy arguments and mismatching dummy procedures. |
|
Warns at every call that uses an implicit interface. Not part of |
|
Warns about a call to a procedure with neither an explicit interface nor an |
|
Compile-time report of every array temporary created to pass an argument. |
|
Run-time warning, once per call site, when a temporary was created. Part of |
|
Run-time error when a procedure not marked |
|
Checks array subscripts, including those of dummy arguments and of conforming-array operations. |
|
Allows indirect recursion by putting all local arrays on the stack; incompatible with |
|
Largest local array kept on the stack in a non-recursive procedure; larger ones are static. |
|
Treats every local variable of a procedure not marked |
|
Downgrades argument type/rank mismatches in external procedure calls from errors to warnings. Strongly discouraged by the manual. |
|
Allocates trampolines for internal procedures passed as arguments on the heap instead of the stack. |
|
Accepts the Fortran 2023 language and rejects GNU extensions. |
Standard-version provenance
| Feature | Standard | Notes |
|---|---|---|
External procedures, function and subroutine subprograms, |
FORTRAN 66 and 77 |
|
Module procedures, internal procedures, |
Fortran 90 |
The basis of modern procedure design. |
|
Fortran 95 |
|
Allocatable and pointer dummy arguments, allocatable function results, |
Fortran 2003 |
|
|
Fortran 2008 |
|
|
Fortran 2018 |
|
|
Fortran 2023 |
Not widely implemented in 2026. |
Templates and generic procedures |
Fortran 2028 (planned) |
Described on What’s New and Fortran 2028 Preview. |
Statement functions, |
Obsolescent |
Annex B.3 of J3/23-007r1. |
Legacy procedure features you may meet
Old code still contains forms that new code should not copy. The table lists what each looks like, its status in the standard, and the replacement. They are described in more detail on Legacy Features and Modernization.
| Legacy form | Status | Modern replacement |
|---|---|---|
|
Obsolescent (B.3.9) |
Separate procedures that share a private helper in a module. |
Statement functions, |
Obsolescent (B.3.4) |
An internal function. |
Alternate return, |
Obsolescent (B.3.2) |
A status argument ( |
Assumed-length character function results, |
Obsolescent (B.3.6) |
|
Specific intrinsic names such as |
Obsolescent (B.3.12) |
Generic names with kind parameters. |
|
Not obsolescent, but error-prone |
Module procedures, or an interface body. |
|
Obsolescent (B.3.11) |
Module variables or arguments. |
Where the books are outdated
Modern Fortran Explained (Metcalf, Reid, Cohen and Bader, 3rd edition, Fortran 2023) covers procedures in its chapter on program units and procedures, with the Fortran 2023 additions in its closing chapter. It remains an accurate reference to the language, and the following points are where a reader has to look beyond it:
-
Compiler status. The book describes
non_recursive, recursion by default,simpleprocedures and conditional arguments as part of the language but gives no information on which compilers implement them. As the compiler table above shows, GNU Fortran 16.2 still lacks all three groups and Flang 23 implements only recursion by default andnon_recursive. Programs that must build on several compilers still writerecursiveand useifconstructs instead of.nil.. -
Appendix A. The book’s list of deprecated features includes implicit interfaces. That is the authors' recommendation, not the standard’s: a procedure with an implicit interface is still conforming. The enforceable modern tool is
implicit none (type, external)together with modules, as used throughout this page. -
Build practice. The book says nothing about compiler diagnostics (
-Wimplicit-interface,-fcheck=array-temps,-fcheck=recursion), about trampolines for internal procedures passed as arguments, or about module build order. They are covered here and on Compilers and Compiler Options.
The MathWorks MATLAB C/C++, Fortran, Java and Python API Reference (R2023a) shows Fortran MEX gateways written in fixed form with
integer*4 and real*8 declarations and hand-written type statements instead of interfaces, so every call into the MATLAB
libraries has an implicit interface. Modern code should write free-form source with iso_c_binding or iso_fortran_env
kinds and explicit interfaces, as described on
MATLAB: MEX, Engine and MAT-File APIs.
Common mistakes at a glance
| Mistake | Symptom | Fix |
|---|---|---|
Calling an external procedure with an implicit interface and the wrong argument types |
No diagnostic; wrong values or crashes at run time. |
Put the callee in a module or write an interface body; use |
No |
Accidental writes are not reported; weaker optimisation. |
Give every dummy argument an |
Referencing an absent |
Segmentation fault or garbage. |
Use |
Passing a strided section to an explicit-shape dummy |
Run-time warning of an array temporary, and a slower program. |
Use assumed-shape dummy arguments or add |
Recursion without |
Compile-time error or run-time "Recursive call to nonrecursive procedure". |
Write |
Side effects in a |
Constraint violation error. |
Use |
Passing the same array to two dummy arguments |
Wrong results that depend on the optimisation level. |
Use a separate output array or a local copy. |
Forgetting that |
Lost values or deallocated arrays. |
Use |
Using an internal procedure after its host has returned |
Undefined behaviour. |
Pass state in an argument instead of through a procedure pointer to an internal procedure. |