🧩 Understanding Subroutines, Functions, and Modules in Fortran
- Adisorn O.
- Nov 8
- 3 min read
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.


