Modules and Programs#

Modules are the preferred way create modern Fortran libraries and applications. As a convention, one source file should always contain only one module, while the module name should match the filepath to allow easy navigation in larger projects. It is also recommended to prefix module names with the library name to avoid name clashes when used as dependency in other projects.

An example for such a module file is given here

!> Interface to TOML processing library.
!>
!> ...
module fpm_toml
  use fpm_error, only : error_t, fatal_error, file_not_found_error
  use fpm_strings, only : string_t
  use tomlf, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
    & set_value, toml_parse, toml_error, new_table, add_table, add_array, &
    & toml_serializer, len
  implicit none
  private

  public :: read_package_file
  public :: toml_table, toml_array, toml_key, toml_stat, get_value, set_value
  public :: new_table, add_table, add_array, len
  public :: toml_error, toml_serializer, toml_parse

contains

  !> Process the configuration file to a TOML data structure
  subroutine read_package_file(table, manifest, error)
    !> TOML data structure
    type(toml_table), allocatable, intent(out) :: table
    !> Name of the package configuration file
    character(len=*), intent(in) :: manifest
    !> Error status of the operation
    type(error_t), allocatable, intent(out) :: error
    ! ...
  end subroutine read_package_file

end module fpm_toml

There are a few things in this example module to highlight. First, every module starts with comments documenting the purpose and content of the module. Similarly, every procedure starts with a comment briefly describing its purpose and the intent of the dummy arguments. Documentation is one of the most important parts of creating long-living software, regardless of language.

Second, imports (use) and exports (public) are explicitly given, this allows on a glance at the module source to check the used and available procedures, constants and derived types. The imports are usually limited to the module scope rather than reimported in every procedure or interface scope. Similarly, exports are made explicitly by adding a private statement on a single line and explicitly listing all exported symbols in public statements.

Finally, the implicit none statement works for the whole module and there is no need to repeat it within each procedure.

Variables inside a module are static (implicitly saved). It is highly recommended to limit the usage of module variables to constant expressions, like parameters or enumerators only or export them as protected rather than public.

Submodules can be used to break long dependency chains and shorten recompilation cascades in Fortran programs. They also offer the possibility to provide specialized and optimized implementations without requiring the use of preprocessor.

An example from the Fortran standard library is the quadrature module, which only defines interfaces to module procedures, but no implementations

!> Numerical integration
!>
!> ...
module stdlib_quadrature
  use stdlib_kinds, only: sp, dp, qp
  implicit none
  private

  public :: trapz
  ! ...

  !> Integrates sampled values using trapezoidal rule
  interface trapz
    pure module function trapz_dx_dp(y, dx) result(integral)
      real(dp), intent(in) :: y(:)
      real(dp), intent(in) :: dx
      real(dp) :: integral
    end function trapz_dx_dp
    module function trapz_x_dp(y, x) result(integral)
      real(dp), intent(in) :: y(:)
      real(dp), intent(in) :: x(:)
      real(dp) :: integral
    end function trapz_x_dp
  end interface trapz

  ! ...
end module stdlib_quadrature

While the implementation is provided in separate submodules like the one for the trapezoidal integration rule given here.

!> Actual implementation of the trapezoidal integration rule
!>
!> ...
submodule (stdlib_quadrature) stdlib_quadrature_trapz
  use stdlib_error, only: check
  implicit none

contains

  pure module function trapz_dx_dp(y, dx) result(integral)
    real(dp), intent(in) :: y(:)
    real(dp), intent(in) :: dx
    real(dp) :: integral
    integer :: n

    n = size(y)
    select case (n)
    case (0:1)
      integral = 0.0_dp
    case (2)
      integral = 0.5_dp*dx*(y(1) + y(2))
    case default
      integral = dx*(sum(y(2:n-1)) + 0.5_dp*(y(1) + y(n)))
    end select
  end function trapz_dx_dp

  ! ...
end submodule stdlib_quadrature_trapz

Note that the module procedures do not have to be implemented in the same submodule. Several submodules can be used to reduce the compilation load for huge modules.

Finally, when setting up a program, it is recommended to keep the actual implementations in the program body at minimum. Reusing implementations from modules allows you to write reusable code and focus the program unit on conveying user input to the respective library functions and objects.