ZetCode

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.

p1.f90
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.

p2.f90
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.

p3.f90
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.

p4.f90
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.

p5.f90
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.

p6.f90
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.

p7.f90
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)

GNU Fortran manual

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.