top of page

🧩 Understanding Subroutines, Functions, and Modules in Fortran

When writing scientific codes or optimization solvers like PT-OP, you will quickly face one design decision:

Should this piece of logic be written as a subroutine, a function, or wrapped in a module?

At first, these terms can seem similar — but understanding their roles is the foundation of clean, safe, and efficient Fortran programming.

🔹 1. Subroutine vs Function

Type

Purpose

Returns a value?

Called by

Subroutine

Performs an action (procedure)

❌ No

call my_sub(a, b, c)

Function

Computes and returns a value

✅ Yes

x = my_fun(a, b)

🧠 Subroutine: Think “Action”

Subroutines do something — solve FEM equations, assemble stiffness, or print a report.

subroutine add_numbers(a, b, c)
    real(8), intent(in) :: a, b
    real(8), intent(out) :: c
    c = a + b
end subroutine add_numbers

Call it like this:

real(8) :: x, y, z
x = 2.0; y = 3.0
call add_numbers(x, y, z)
print *, "Sum =", z

Good for: multiple outputs, large data updates, or when you just “do something.”

🧮 Function: Think “Value”

Functions return one thing — the computed result.You can use them in expressions or assignments.

real(8) function multiply(a, b)
    real(8), intent(in) :: a, b
    multiply = a * b
end function multiply

Use it like:

print *, "Product =", multiply(2.0d0, 3.0d0)

Good for: pure calculations (e.g. Area = SecProp(h,b,bw,e)).

⚠️ Problem with standalone functions

If you put a function in a separate file and call it from another program,the compiler does not know its type or argument structure — unless you tell it.

That means this compiles:

x = multiply(2.0d0, 3.0d0)

but the compiler guesses the type of multiply from its name (Fortran’s “implicit typing” rule).Result: easy to make hidden bugs.

💡 The Solution — Use Modules

A module acts like a shared library that stores your functions, subroutines, and data types with full interface checking.

module math_ops
  implicit none
contains
  subroutine add_numbers(a, b, c)
      real(8), intent(in) :: a, b
      real(8), intent(out) :: c
      c = a + b
  end subroutine add_numbers

  real(8) function multiply(a, b)
      real(8), intent(in) :: a, b
      multiply = a * b
  end function multiply
end module math_ops

Now you can safely use both:

program main
    use math_ops
    implicit none
    real(8) :: x, y, z

    x = 2.0; y = 3.0
    call add_numbers(x, y, z)
    print *, "Sum =", z
    print *, "Product =", multiply(x, y)
end program main

✅ The compiler now checks:

  • Argument types

  • Array sizes

  • Return types→ preventing 90% of runtime bugs before execution.

🧰 When to Use What

Task

Best choice

Why

Compute one scalar value

Function in module

cleaner, reusable

Perform FEM assembly, I/O, or multi-output tasks

Subroutine (in or out of module)

procedural, flexible

Share constants between routines

Module with save variables

like global memory (e.g. common_var_mod)

⚙️ Example from PT-OP

In PT-OP, the architecture follows exactly this principle:

File

Type

Description

helpers.f90

Module

Contains all small functions (e.g. SecProp, Compute_Drape)

run_fem.f90

Subroutine

Main finite element solver

objective_mod.f90

Module

Defines objective() function used by the optimizer

common_var_mod.f90

Module

Stores constants shared between routines

jaya_main.f90

Program

Reads input and runs optimization loop

This design guarantees that everything connects safely, while keeping each piece readable and testable.

🧩 TL;DR

  • Subroutine = action, no return value → can be standalone.

  • Function = value, must have visible interface → put inside a module.

  • Module = library + safety, preferred for all modern Fortran projects.


 
 
bottom of page