ZetCode

Fortran type-bound procedures

last modified November 14, 2026

A derived type groups data, but the procedures that work on that data still stand outside it: every call passes the value as an argument, as in distance(a, b). A type-bound procedure moves the procedure into the type definition, so that it is called through the value itself, as in a%distance(b). Other languages call such a procedure a method, and the value before the percent sign the object. Fortran keeps the model explicit: the object is still an ordinary first argument of the procedure, and the compiler merely allows the friendlier call syntax.

This article declares procedures inside a type, hides components behind a private boundary, groups several procedures under one generic name, distinguishes the pass and nopass bindings, and builds a stack, an account, a calendar date and a polynomial as types that carry their own behaviour.

Declaring a type-bound procedure

The component list of a type ends at contains, and every line after it binds a name to a module procedure. The form procedure :: norm => point_norm says that norm is the name seen by the caller and point_norm is the procedure that implements it. The first dummy argument of that procedure is the passed object: it receives the variable written before the percent sign. It is declared with class(point) rather than type(point) so that values of an extended type can be passed as well, a subject the inheritance article takes up.

t1.f90
module points
    implicit none
    private
    public :: point
    type :: point
        real :: x = 0.0, y = 0.0
    contains
        procedure :: norm => point_norm
        procedure :: move => point_move
        procedure :: show => point_show
    end type point
contains
    pure real function point_norm(self) result(d)
        class(point), intent(in) :: self

        d = sqrt(self%x**2 + self%y**2)
    end function point_norm

    subroutine point_move(self, dx, dy)
        class(point), intent(inout) :: self
        real, intent(in) :: dx, dy

        self%x = self%x + dx
        self%y = self%y + dy
    end subroutine point_move

    subroutine point_show(self)
        class(point), intent(in) :: self

        print '(a, f6.2, a, f6.2, a)', 'point(', self%x, ', ', self%y, ')'
    end subroutine point_show
end module points

program t1
    use points
    implicit none
    type(point) :: p

    p = point(3.0, 4.0)
    print *, 'norm:', p%norm()
    call p%move(1.0, -1.0)
    call p%show()
    print *, 'new norm:', p%norm()
end program t1

The point (3, 4) has a norm of 5, and moving it by (1, -1) gives (4, 3), whose norm is still 5. A function binding is used inside an expression, while a subroutine binding needs call, exactly like the underlying procedure. The binding changes only the syntax: p%norm() is the same call as point_norm(p), and the compiler checks the arguments of both in the same way.

$ gfortran t1.f90 -o t1
$ ./t1
 norm:   5.00000000    
point(  4.00,   3.00)
 new norm:   5.00000000    

Private components

A type can hide its components with private, and the hiding is enforced by the compiler: code outside the module can name the type and call its procedures, but a reference to a%balance is an error. The components then form the state of the object, which only the procedures of the type may read or change. This is the same idea as a private variable of a module, applied to one value rather than to the whole module.

t2.f90
module bank
    implicit none
    private
    public :: account
    type :: account
        private
        character(len=12) :: owner = 'unknown'
        real :: balance = 0.0
        integer :: moves = 0
    contains
        procedure :: open => account_open
        procedure :: deposit => account_deposit
        procedure :: withdraw => account_withdraw
        procedure :: report => account_report
        procedure, nopass :: currency => account_currency
    end type account
contains
    subroutine account_open(self, owner, amount)
        class(account), intent(out) :: self
        character(len=*), intent(in) :: owner
        real, intent(in) :: amount

        self%owner = owner
        self%balance = amount
        self%moves = 0
    end subroutine account_open

    subroutine account_deposit(self, amount)
        class(account), intent(inout) :: self
        real, intent(in) :: amount

        self%balance = self%balance + amount
        self%moves = self%moves + 1
    end subroutine account_deposit

    subroutine account_withdraw(self, amount)
        class(account), intent(inout) :: self
        real, intent(in) :: amount

        if (amount > self%balance) then
            print '(a, f8.2)', ' refused, balance is', self%balance
            return
        end if
        self%balance = self%balance - amount
        self%moves = self%moves + 1
    end subroutine account_withdraw

    subroutine account_report(self)
        class(account), intent(in) :: self

        print '(a, a, a, f8.2, a, i0, a)', trim(self%owner), ': ', &
            self%currency(), self%balance, ' after ', self%moves, ' moves'
    end subroutine account_report

    pure function account_currency() result(c)
        character(len=3) :: c

        c = 'EUR'
    end function account_currency
end module bank

program t2
    use bank
    implicit none
    type(account) :: a

    call a%open('alice', 100.0)
    call a%report()
    call a%deposit(50.0)
    call a%withdraw(30.0)
    call a%withdraw(500.0)
    call a%report()
    print *, 'currency: ', a%currency()
end program t2

The account starts with 100, receives 50 and gives up 30, so the second report shows 120 after two successful moves. The attempted withdrawal of 500 is refused before anything changes, which is why the count stays at two. The currency binding is declared nopass: it has no passed object, and the call a%currency() only uses the name a as a way to reach the procedure. Without the private line inside the type, the program could have changed balance directly and bypassed the check.

$ gfortran t2.f90 -o t2
$ ./t2
alice: EUR  100.00 after 0 moves
 refused, balance is  120.00
alice: EUR  120.00 after 2 moves
 currency: EUR

A stack as a type

A type with an array component can implement a small container whose state survives between calls. The stack below keeps its values in items and the index of the last one in top; pushing adds at top + 1 and popping removes from top, so the last value pushed is the first one popped. Both components are private, which guarantees that no caller can move top behind the procedure's back.

t3.f90
module stacks
    implicit none
    private
    public :: stack, max_size
    integer, parameter :: max_size = 100

    type :: stack
        private
        integer :: items(max_size) = 0
        integer :: top = 0
    contains
        procedure :: push => stack_push
        procedure :: pop => stack_pop
        procedure :: size => stack_size
        procedure :: is_empty => stack_empty
        procedure :: show => stack_show
    end type stack
contains
    subroutine stack_push(self, value)
        class(stack), intent(inout) :: self
        integer, intent(in) :: value

        if (self%top >= max_size) then
            print *, 'push refused: stack is full'
            return
        end if
        self%top = self%top + 1
        self%items(self%top) = value
    end subroutine stack_push

    integer function stack_pop(self) result(v)
        class(stack), intent(inout) :: self

        if (self%top == 0) then
            print *, 'pop refused: stack is empty'
            v = -1
            return
        end if
        v = self%items(self%top)
        self%top = self%top - 1
    end function stack_pop

    pure integer function stack_size(self) result(n)
        class(stack), intent(in) :: self

        n = self%top
    end function stack_size

    pure logical function stack_empty(self) result(flag)
        class(stack), intent(in) :: self

        flag = self%top == 0
    end function stack_empty

    subroutine stack_show(self)
        class(stack), intent(in) :: self
        integer :: i

        print '(a, 10(i0, 1x))', 'top to bottom:', &
            (self%items(i), i = self%top, 1, -1)
    end subroutine stack_show
end module stacks

program t3
    use stacks
    implicit none
    type(stack) :: s
    integer :: i

    print *, 'empty:', s%is_empty()
    do i = 1, 5
        call s%push(i * i)
    end do
    print *, 'size: ', s%size()
    call s%show()
    print *, 'popped:', s%pop(), s%pop()
    print *, 'size: ', s%size()
    print *, 'empty:', s%is_empty()
end program t3

Five squares go in and the stack reports a size of five; the display runs from the top down, so it starts with 25 and ends with 1. The two pops return 25 and 16, the size falls to three, and is_empty is still false. The initializer items(max_size) = 0 is not strictly needed, because only elements up to top are read, but it makes a freshly declared stack deterministic and costs nothing at this size.

$ gfortran t3.f90 -o t3
$ ./t3
 empty: T
 size:            5
top to bottom:25 16 9 4 1
 popped:          25          16
 size:            3
 empty: F

Generic bindings

One type can offer several procedures under a single name by declaring them as a generic binding. The line generic :: set => set_real, set_ints creates a set of specific bindings, and the compiler chooses among them from the number and the types of the arguments, exactly as it does for an overloaded operator. The choice is made once, at compile time, so a call that matches none of the specifics is an error and a call that matches two is ambiguous.

t4.f90
module vectors
    implicit none
    private
    public :: vec
    type :: vec
        real :: x = 0.0, y = 0.0
    contains
        procedure :: set_real => vec_set_real
        procedure :: set_ints => vec_set_ints
        generic :: set => set_real, set_ints
        procedure :: show => vec_show
    end type vec
contains
    subroutine vec_set_real(self, r)
        class(vec), intent(inout) :: self
        real, intent(in) :: r

        self%x = r
        self%y = r
    end subroutine vec_set_real

    subroutine vec_set_ints(self, i, j)
        class(vec), intent(inout) :: self
        integer, intent(in) :: i, j

        self%x = real(i)
        self%y = real(j)
    end subroutine vec_set_ints

    subroutine vec_show(self)
        class(vec), intent(in) :: self

        print '(a, f5.1, a, f5.1, a)', '(', self%x, ', ', self%y, ')'
    end subroutine vec_show
end module vectors

program t4
    use vectors
    implicit none
    type(vec) :: v

    call v%set(2.5)
    call v%show()
    call v%set(3, 4)
    call v%show()
    call v%set_real(7.0)
    call v%show()
end program t4

The first call passes one real and reaches set_real, the second passes two integers and reaches set_ints, and the third names the specific binding directly. A generic binding does not hide its specifics; it only adds a shorter way to reach them. Note that the two procedures differ in the number of arguments, which is enough to tell them apart; two procedures with identical argument lists could not share a generic name.

$ gfortran t4.f90 -o t4
$ ./t4
(  2.5,   2.5)
(  3.0,   4.0)
(  7.0,   7.0)

pass and nopass

By default the passed object is the first dummy argument, whatever its name. The specifier pass(self) states that name explicitly, which is useful documentation when a procedure has several arguments, and nopass removes the passed object altogether. A nopass binding can therefore be a function with no arguments at all, reached through any value of the type; it behaves like a constructor or a constant attached to the type.

t5.f90
module players
    implicit none
    private
    public :: player
    type :: player
        character(len=10) :: name = ''
        integer :: score = 0
    contains
        procedure, pass(self) :: add => player_add
        procedure, nopass :: best => player_best
        procedure :: beats => player_beats
    end type player
contains
    subroutine player_add(self, points)
        class(player), intent(inout) :: self
        integer, intent(in) :: points

        self%score = self%score + points
    end subroutine player_add

    function player_best() result(p)
        type(player) :: p

        p = player('nobody', 0)
    end function player_best

    pure logical function player_beats(self, other) result(flag)
        class(player), intent(in) :: self
        type(player), intent(in) :: other

        flag = self%score > other%score
    end function player_beats
end module players

program t5
    use players
    implicit none
    type(player) :: a, b, leader

    a = player('alice', 0)
    b = player('bob', 0)
    call a%add(12)
    call a%add(8)
    call b%add(15)
    print '(a, a, i0, a, a, a, l1)', trim(a%name), ' with ', a%score, &
        ' beats ', trim(b%name), ': ', a%beats(b)
    print '(a, a, i0, a, a, a, l1)', trim(b%name), ' with ', b%score, &
        ' beats ', trim(a%name), ': ', b%beats(a)
    leader = a%best()
    print '(a, a, a, i0)', 'best starts as ', trim(leader%name), ' with ', &
        leader%score
end program t5

Alice collects 20 points and Bob 15, so the first comparison is true and the second false. The passed object of beats is the player before the percent sign, while the other player travels as an ordinary argument. a%best() passes nothing at all and returns the empty player used as a starting point for a search; the same function could also be written as a plain module function, and the binding only gives it a home in the type.

$ gfortran t5.f90 -o t5
$ ./t5
alice with 20 beats bob: T
bob with 15 beats alice: F
best starts as nobody with 0

Keeping the object valid

Because the procedures of a type are the only code that may touch its private components, they are the natural place to enforce the rules that keep a value meaningful. The date type below validates its own fields, advances by one day with the correct month and leap-year handling, and prints itself in ISO form. The helpers is_leap and month_length are ordinary module functions; they are not bound to the type because they have no object of their own.

t6.f90
module dates
    implicit none
    private
    public :: date
    type :: date
        integer :: year = 2000
        integer :: month = 1
        integer :: day = 1
    contains
        procedure :: is_valid => date_valid
        procedure :: next => date_next
        procedure :: show => date_show
    end type date
contains
    pure logical function date_valid(self) result(flag)
        class(date), intent(in) :: self

        flag = .false.
        if (self%month < 1 .or. self%month > 12) return
        if (self%day < 1 .or. self%day > month_length(self%year, self%month)) return
        flag = .true.
    end function date_valid

    subroutine date_next(self)
        class(date), intent(inout) :: self

        self%day = self%day + 1
        if (self%day > month_length(self%year, self%month)) then
            self%day = 1
            self%month = self%month + 1
            if (self%month > 12) then
                self%month = 1
                self%year = self%year + 1
            end if
        end if
    end subroutine date_next

    subroutine date_show(self)
        class(date), intent(in) :: self

        print '(i4.4, a, i2.2, a, i2.2)', self%year, '-', self%month, '-', self%day
    end subroutine date_show

    pure logical function is_leap(y) result(flag)
        integer, intent(in) :: y

        flag = (mod(y, 4) == 0 .and. mod(y, 100) /= 0) .or. mod(y, 400) == 0
    end function is_leap

    pure integer function month_length(y, m) result(n)
        integer, intent(in) :: y, m
        integer, parameter :: lengths(12) = [31, 28, 31, 30, 31, 30, &
                                             31, 31, 30, 31, 30, 31]

        n = lengths(m)
        if (m == 2 .and. is_leap(y)) n = 29
    end function month_length
end module dates

program t6
    use dates
    implicit none
    type(date) :: d
    integer :: i

    d = date(2024, 2, 28)
    print *, 'valid:', d%is_valid()
    call d%show()
    do i = 1, 3
        call d%next()
        call d%show()
    end do
    d = date(2023, 2, 29)
    print *, 'valid:', d%is_valid()
    d = date(1900, 2, 29)
    print *, 'valid:', d%is_valid()
end program t6

Starting at 28 February 2024, three steps cross the leap day and the end of the month, so the dates run through 29 February and 1 and 2 March. The year 2023 is not a leap year, so 29 February 2023 is invalid, and 1900 is a century that is not divisible by 400, so its 29 February is invalid as well. The constructor date(...) accepts such a value without complaint, because a structure constructor only copies components; a stricter program would provide an init binding that rejects bad input before the value exists.

$ gfortran t6.f90 -o t6
$ ./t6
 valid: T
2024-02-28
2024-02-29
2024-03-01
2024-03-02
 valid: F
 valid: F

Bound functions that return the type

A binding may return a value of its own type, which allows a type to produce new values from old ones without the caller allocating anything. The polynomial below stores its coefficients in an allocatable component, evaluates itself by Horner's rule, and returns its derivative as a new polynomial. The derivative allocates the coefficient array of the result, and the assignment at the call site takes ownership of that array.

t7.f90
module polys
    implicit none
    private
    public :: poly
    type :: poly
        real, allocatable :: c(:)
    contains
        procedure :: eval => poly_eval
        procedure :: derivative => poly_derivative
        procedure :: show => poly_show
    end type poly
contains
    pure real function poly_eval(self, x) result(y)
        class(poly), intent(in) :: self
        real, intent(in) :: x
        integer :: i

        y = 0.0
        do i = size(self%c), 1, -1
            y = y * x + self%c(i)
        end do
    end function poly_eval

    function poly_derivative(self) result(d)
        class(poly), intent(in) :: self
        type(poly) :: d
        integer :: i, n

        n = size(self%c)
        if (n <= 1) then
            allocate (d%c(1))
            d%c = 0.0
            return
        end if
        allocate (d%c(n - 1))
        do i = 2, n
            d%c(i - 1) = self%c(i) * real(i - 1)
        end do
    end function poly_derivative

    subroutine poly_show(self)
        class(poly), intent(in) :: self
        integer :: i

        do i = size(self%c), 1, -1
            if (i == 1) then
                print '(a, f0.1)', ' c: ', self%c(i)
            else
                print '(a, f0.1, a, i0)', ' c: ', self%c(i), ' x^', i - 1
            end if
        end do
    end subroutine poly_show
end module polys

program t7
    use polys
    implicit none
    type(poly) :: p, d

    p%c = [1.0, 0.0, 3.0, 2.0]
    print *, 'p(2)  =', p%eval(2.0)
    print *, 'p(-1) =', p%eval(-1.0)
    call p%show()
    d = p%derivative()
    print *, 'p''(2) =', d%eval(2.0)
    call d%show()
end program t7

The coefficients [1, 0, 3, 2] describe 2x^3 + 3x^2 + 1, which is 29 at x = 2 and 2 at x = -1. The derivative 6x^2 + 6x is 36 at x = 2, and the loop copies the coefficients from lowest to highest, so the display starts at the highest power. The call d = p%derivative() assigns a value that owns an allocatable array; Fortran copies the array into d and releases the temporary when the statement ends.

$ gfortran t7.f90 -o t7
$ ./t7
 p(2)  =   29.0000000    
 p(-1) =   2.00000000    
 c: 2.0 x^3
 c: 3.0 x^2
 c: .0 x^1
 c: 1.0
 p'(2) =   36.0000000    
 c: 6.0 x^2
 c: 6.0 x^1
 c: .0

Next steps

The inheritance and polymorphism article shows why the passed object is declared with class: an extended type can override a binding, and the call through a polymorphic variable reaches the version that belongs to the runtime type. The operator overloading article adds the operator and assignment interfaces, and the derived types article covers the components and constructors used throughout this one. The modules article explains the private and public rules that hide the components.

Source

Fortran 2018 standard (J3/18-007r1)

GNU Fortran manual

Fortran-lang: learning resources

In this article we have declared procedures inside a type and called them with the percent sign, hidden components with private, built a stack and an account whose state only their own procedures can change, grouped bindings under a generic name, separated pass from nopass, and returned a new polynomial from a binding. The previous article, Fortran derived types, introduces the components that these procedures work on.

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.