Fortran procedure arguments and callbacks
last modified November 14, 2026
A procedure is a value like any other in one respect: it can be passed to another procedure. The receiving procedure declares a dummy procedure, and the caller supplies the name of a real one. This is how a sorting routine gets its comparison, how an integration routine gets the function it integrates, and how a menu maps a name to the code that should run. The procedure that is passed is often called a callback, because the receiving routine calls back into the caller's code.
The one rule that governs all of it is the interface. A procedure with an implicit interface is known only by its name, so the compiler cannot check the number or the types of its arguments. A procedure with an explicit interface is described by an interface block, and every call and every pass is checked. Modern Fortran prefers explicit interfaces, and the examples below show where each form still appears.
An external procedure as an argument
An external procedure is a function or subroutine defined outside any
module, as most FORTRAN 77 code was. The caller marks the name with the
external attribute, and if the procedure returns a value the
declaration also gives its type. The receiving routine describes the argument
with an interface block inside its own specification part, which gives the
compiler enough information to check the call even though the procedure itself
is not in a module.
program p1
implicit none
real, external :: square
real :: y
call apply(square, 3.0, y)
print '(a, f6.2)', 'square(3.0) = ', y
call apply(square, -2.5, y)
print '(a, f6.2)', 'square(-2.5) = ', y
contains
subroutine apply(f, x, y)
interface
real function f(t)
real, intent(in) :: t
end function f
end interface
real, intent(in) :: x
real, intent(out) :: y
y = f(x)
end subroutine apply
end program p1
real function square(x)
implicit none
real, intent(in) :: x
square = x * x
end function square
The declaration real, external :: square says that
square is an external function returning a real, and the interface
inside apply repeats that description for the dummy argument
f. The two must agree, and the compiler compares them. The
procedure square itself is written after the program, with
implicit none of its own, and it knows nothing about the caller.
This style still appears in old libraries, where an external routine cannot be
put into a module without rewriting it.
$ gfortran p1.f90 -o p1 $ ./p1 square(3.0) = 9.00 square(-2.5) = 6.25
Module procedures and abstract interfaces
A procedure that lives in a module already has an explicit interface, and the
receiving routine can describe the dummy with an abstract interface: a
block that gives the shape of the procedure without naming an implementation.
The dummy is declared procedure(real_func), where
real_func is the name of the abstract interface. Any module
procedure with that argument list can then be passed, and so can an intrinsic
procedure if the caller declares it with an intrinsic statement.
module callbacks
implicit none
private
public :: apply_twice, double, negate
abstract interface
pure real function real_func(x)
real, intent(in) :: x
end function real_func
end interface
contains
pure real function apply_twice(f, x) result(r)
procedure(real_func) :: f
real, intent(in) :: x
r = f(f(x))
end function apply_twice
pure real function double(x) result(r)
real, intent(in) :: x
r = 2.0 * x
end function double
pure real function negate(x) result(r)
real, intent(in) :: x
r = -x
end function negate
end module callbacks
program p2
use callbacks
implicit none
intrinsic :: exp
print '(a, f6.2)', 'twice double(3): ', apply_twice(double, 3.0)
print '(a, f6.2)', 'twice negate(3): ', apply_twice(negate, 3.0)
print '(a, f6.4)', 'twice exp(0): ', apply_twice(exp, 0.0)
end program p2
apply_twice(double, 3.0) doubles twice and gives 12, while the
same call with negate gives back 3. The intrinsic exp
is passed in the third call and applied twice, giving e to four
decimal places. The abstract interface serves as a contract: the dummy is
declared pure, so a procedure that modifies a module variable
could not be passed without a compiler error.
$ gfortran p2.f90 -o p2 $ ./p2 twice double(3): 12.00 twice negate(3): 3.00 twice exp(0): 2.7183
Procedure pointers
A procedure pointer is a variable that can refer to a procedure, just
as an ordinary pointer refers to data. It is declared with
procedure(interface), pointer, starts as disassociated, and is
pointed at a procedure with =>. The call syntax is the same as
for a normal procedure, and associated tells whether the pointer
can be called at all. Assigning a different procedure changes the behaviour of
the next call without touching the calling code.
module maths
implicit none
private
public :: real_func, double_it, negate, half
abstract interface
pure real function real_func(x)
real, intent(in) :: x
end function real_func
end interface
contains
pure real function double_it(x) result(r)
real, intent(in) :: x
r = 2.0 * x
end function double_it
pure real function negate(x) result(r)
real, intent(in) :: x
r = -x
end function negate
pure real function half(x) result(r)
real, intent(in) :: x
r = x / 2.0
end function half
end module maths
program p3
use maths
implicit none
procedure(real_func), pointer :: f => null()
real :: x
x = 8.0
print '(a, l1)', 'starts associated: ', associated(f)
f => double_it
print '(a, f6.2)', 'double: ', f(x)
f => negate
print '(a, f6.2)', 'negate: ', f(x)
f => half
print '(a, f6.2)', 'half: ', f(x)
f => null()
print '(a, l1)', 'ends associated: ', associated(f)
end program p3
The pointer starts disassociated, then doubles 8, negates it and halves it as
it is aimed at three different module functions. The final nullify
returns it to the disassociated state, and the program never calls it there: an
attempt to call a disassociated procedure pointer is an error that the compiler
cannot catch, because the value is known only at run time. The
associated test before a call is the equivalent of the null check
that a data pointer needs.
$ gfortran p3.f90 -o p3 $ ./p3 starts associated: F double: 16.00 negate: -8.00 half: 4.00 ends associated: F
A table of procedures
A procedure pointer can be a component of a derived type, which makes a table
of named operations possible. Each entry holds a name and a pointer to the
function that implements it, and the dispatch is a lookup by name followed by a
call through the pointer. The nopass attribute is needed because
the function has no passed object; without it, the compiler would expect the
table entry as an extra first argument.
module opmod
implicit none
private
public :: op_entry, fill_table, find_op, apply_op, add, subtract, &
multiply, divide
abstract interface
pure real function binop(a, b)
real, intent(in) :: a, b
end function binop
end interface
type :: op_entry
character(len=8) :: name = ''
procedure(binop), pointer, nopass :: fn => null()
end type op_entry
contains
pure real function add(a, b) result(r)
real, intent(in) :: a, b
r = a + b
end function add
pure real function subtract(a, b) result(r)
real, intent(in) :: a, b
r = a - b
end function subtract
pure real function multiply(a, b) result(r)
real, intent(in) :: a, b
r = a * b
end function multiply
pure real function divide(a, b) result(r)
real, intent(in) :: a, b
r = a / b
end function divide
subroutine fill_table(t)
type(op_entry), intent(out) :: t(:)
t(1)%name = 'add'
t(1)%fn => add
t(2)%name = 'subtract'
t(2)%fn => subtract
t(3)%name = 'multiply'
t(3)%fn => multiply
t(4)%name = 'divide'
t(4)%fn => divide
end subroutine fill_table
integer function find_op(t, name) result(pos)
type(op_entry), intent(in) :: t(:)
character(len=*), intent(in) :: name
integer :: i
pos = 0
do i = 1, size(t)
if (trim(t(i)%name) == name) then
pos = i
return
end if
end do
end function find_op
real function apply_op(t, name, a, b, found) result(r)
type(op_entry), intent(in) :: t(:)
character(len=*), intent(in) :: name
real, intent(in) :: a, b
logical, intent(out) :: found
integer :: pos
pos = find_op(t, name)
found = pos > 0
if (found) then
r = t(pos)%fn(a, b)
else
r = 0.0
end if
end function apply_op
end module opmod
program p4
use opmod
implicit none
type(op_entry) :: table(4)
character(len=8) :: wanted(5) = [character(len=8) :: 'add', 'subtract', &
'multiply', 'divide', 'power']
integer :: i
real :: r
logical :: found
call fill_table(table)
do i = 1, size(wanted)
r = apply_op(table, trim(wanted(i)), 7.0, 2.0, found)
if (found) then
print '(a, a, a, f7.2)', trim(wanted(i)), '(7, 2) = ', '', r
else
print '(a, a)', trim(wanted(i)), ' is not in the table'
end if
end do
print '(a, i0)', 'table entries with a target: ', &
count([(associated(table(i)%fn), i = 1, size(table))])
end program p4
Four operations are found by name and applied to the same pair of numbers,
while power is reported as missing. The last line counts the
entries whose pointer is associated, using an array constructor of logical
values and the intrinsic count. A table like this is the basis of
a small command interpreter, a calculator or a plug-in system, and adding an
operation means adding one function and one row, not editing a chain of
if statements.
$ gfortran p4.f90 -o p4 $ ./p4 add(7, 2) = 9.00 subtract(7, 2) = 5.00 multiply(7, 2) = 14.00 divide(7, 2) = 3.50 power is not in the table table entries with a target: 4
Subroutine arguments and host association
An argument can be a subroutine as well as a function. The interface below
describes an action that receives one element and modifies it, and
apply_all calls it once per element. The program passes
double_it from the module, and then passes add_shift,
an internal procedure declared after the contains of the
program. The internal procedure sees the variable shift from the
program, a form of access called host association.
module element_ops
implicit none
private
public :: elem_action, apply_all, double_it
abstract interface
subroutine elem_action(x)
real, intent(inout) :: x
end subroutine elem_action
end interface
contains
subroutine apply_all(a, action)
real, intent(inout) :: a(:)
procedure(elem_action) :: action
integer :: i
do i = 1, size(a)
call action(a(i))
end do
end subroutine apply_all
subroutine double_it(x)
real, intent(inout) :: x
x = 2.0 * x
end subroutine double_it
end module element_ops
program p5
use element_ops
implicit none
real :: a(6) = [1.0, -2.0, 3.0, -4.0, 5.0, -6.0]
real :: shift
call apply_all(a, double_it)
print '(a, 6(f7.1))', 'doubled:', a
shift = 10.0
call apply_all(a, add_shift)
print '(a, 6(f7.1))', 'shifted:', a
shift = 0.5
call apply_all(a, add_shift)
print '(a, 6(f7.1))', 'shifted:', a
contains
subroutine add_shift(x)
real, intent(inout) :: x
x = x + shift
end subroutine add_shift
end program p5
The first call doubles every element. The next two add 10 and then 0.5, because
the internal procedure reads the current value of shift each time
it runs. This is the closest Fortran comes to a closure: the procedure captures
the surrounding variables by reference, but only while the surrounding scope is
alive. An internal procedure must not be stored beyond the call that created
the scope, which is why the module's double_it is the safer thing
to keep in a long-lived table.
$ gfortran p5.f90 -o p5 $ ./p5 doubled: 2.0 -4.0 6.0 -8.0 10.0 -12.0 shifted: 12.0 6.0 16.0 2.0 20.0 -2.0 shifted: 12.5 6.5 16.5 2.5 20.5 -1.5
Optional callbacks
A dummy procedure can be optional, like any other dummy argument.
The receiving routine tests it with present and skips the work when
the caller left it out. This pattern lets one routine serve both a plain and a
customised use, without a second copy of the loop. A call that passes the
argument and a call that does not are both valid; the only rule is that the
procedure must not be called when it is absent.
module optional_cb
implicit none
private
public :: scale_fn, transform, square
abstract interface
pure real function scale_fn(x)
real, intent(in) :: x
end function scale_fn
end interface
contains
pure subroutine transform(a, f)
real, intent(inout) :: a(:)
procedure(scale_fn), optional :: f
integer :: i
if (.not. present(f)) return
do i = 1, size(a)
a(i) = f(a(i))
end do
end subroutine transform
pure real function square(x) result(r)
real, intent(in) :: x
r = x * x
end function square
end module optional_cb
program p6
use optional_cb
implicit none
real :: a(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
real :: b(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
call transform(a)
print '(a, 5(f6.1))', 'without hook:', a
call transform(b, square)
print '(a, 5(f6.1))', 'with square: ', b
call transform(b)
print '(a, 5(f6.1))', 'called again: ', b
end program p6
The first call leaves the array untouched, the second squares every element,
and the third leaves the squared values alone because no hook was supplied. The
guard if (.not. present(f)) return at the top is the whole
mechanism; without it the loop would call an absent procedure and the program
would fail. Note that the array is modified in place and that no result is
returned, which is why transform is a subroutine.
$ gfortran p6.f90 -o p6 $ ./p6 without hook: 1.0 2.0 3.0 4.0 5.0 with square: 1.0 4.0 9.0 16.0 25.0 called again: 1.0 4.0 9.0 16.0 25.0
A real example: numerical integration
The trapezoid rule approximates an integral by summing the areas of
trapezoids under the curve. The rule is the same for every function, so the
function should be the argument. The routine below receives any procedure that
matches func, evaluates it at the ends and at the interior
points, and returns the sum. The program integrates its own square function and
the intrinsics sin and exp with the same call.
module integrate
implicit none
private
public :: func, trapezoid, square
abstract interface
pure real function func(x)
real, intent(in) :: x
end function func
end interface
contains
pure real function trapezoid(f, a, b, n) result(s)
procedure(func) :: f
real, intent(in) :: a, b
integer, intent(in) :: n
real :: h, x
integer :: i
h = (b - a) / real(n)
s = 0.5 * (f(a) + f(b))
do i = 1, n - 1
x = a + real(i) * h
s = s + f(x)
end do
s = s * h
end function trapezoid
pure real function square(x) result(r)
real, intent(in) :: x
r = x * x
end function square
end module integrate
program p7
use integrate
implicit none
intrinsic :: sin, exp
print '(a, f8.6)', 'x^2 from 0 to 1: ', trapezoid(square, 0.0, 1.0, 1000)
print '(a, f8.6)', 'sin from 0 to pi: ', trapezoid(sin, 0.0, &
3.14159265, 1000)
print '(a, f8.6)', 'exp from 0 to 1: ', trapezoid(exp, 0.0, 1.0, 1000)
print '(a, f8.6)', 'x^2 with 10 intervals: ', trapezoid(square, 0.0, 1.0, 10)
end program p7
The integral of x**2 from 0 to 1 is one third, and the routine
returns 0.333334 with a thousand intervals; the error of the trapezoid rule
falls with the square of the interval width, so ten intervals give 0.335, a
visible error in the third decimal place. The integral of sin over
a half period is 2 and the routine returns 1.999999, and the integral of
exp from 0 to 1 is e - 1, matched to six decimals.
The numerical integration article takes the same idea further with Simpson's
rule and root finding.
$ gfortran p7.f90 -o p7 $ ./p7 x^2 from 0 to 1: 0.333334 sin from 0 to pi: 1.999999 exp from 0 to 1: 1.718282 x^2 with 10 intervals: 0.335000
Next steps
The sorting article passes a comparison procedure to a record sort, and the procedures article covers the interface blocks that make these calls checkable. The type-bound procedures article shows the object-oriented alternative to a function table, and the operator overloading article uses interfaces to give a symbol a new meaning. The forthcoming numerical integration article builds on the trapezoid example, and the recursion article shows procedures that call themselves rather than a callback.
Source
Fortran 2018 standard (J3/18-007r1)
Fortran-lang: learning resources
In this article we have passed an external function through an interface block, described a callback with an abstract interface, called module procedures and intrinsics through the same dummy, used a procedure pointer and changed its target, built a table of named operations with procedure pointer components, passed an internal subroutine that reads a host variable, made a callback optional, and integrated three functions with one routine. The previous article, Fortran searching, uses a sorted array and a hash table to find values.
Author
List all Fortran tutorials.