📘 Fortran Programming: Functions, Subroutines, and Modular Design

Andrew J. Pounds, Ph.D.


🎯 Learning Objectives

After completing this exercise, you will be able to:

1️⃣ Functions vs. Subroutines: A C/C++ Programmer's Perspective

If you're coming from C or C++, you're used to having one type of callable unit: the function. In Fortran, there are two distinct types:

Feature C/C++ Function Fortran Function Fortran Subroutine
Returns a value Yes (or void) Yes, exactly one No
How called result = func(args) result = func(args) call sub(args)
Modifies arguments Possible (via pointers) Possible (but discouraged) Primary purpose
Use in expressions Yes Yes No

The Key Difference

! Function usage - returns a value, used in expression
area = circle_area(radius)
total = sum_values(a, b) + sum_values(c, d)

! Subroutine usage - called with 'call', modifies arguments
call swap(x, y)
call read_data(filename, data_array, num_elements)
📝 Note: In C/C++, you might write a void function that modifies pointer arguments. In Fortran, this pattern is handled by subroutines. Functions in Fortran are meant to be "pure" computations that return a result.

2️⃣ Writing Functions in Fortran

Basic Function Syntax

There are two common styles for declaring functions in Fortran:

Style 1: Return type before function name

real function circle_area(radius)
    implicit none
    real, intent(in) :: radius
    real, parameter :: PI = 3.14159265
    
    circle_area = PI * radius * radius
end function circle_area

Style 2: Result variable (more flexible)

function circle_area(radius) result(area)
    implicit none
    real, intent(in) :: radius
    real :: area
    real, parameter :: PI = 3.14159265
    
    area = PI * radius * radius
end function circle_area

Both styles are equivalent. Style 2 is useful when the function name is long or when you want a more descriptive result variable name.

Complete Function Example

program function_demo
    implicit none
    real :: r, a, c
    
    print *, "Enter the radius of a circle: "
    read *, r
    
    ! Call functions in expressions (just like C/C++)
    a = circle_area(r)
    c = circle_circumference(r)
    
    print '(A, F10.4)', "Area: ", a
    print '(A, F10.4)', "Circumference: ", c
    
    ! Functions can be used directly in output
    print '(A, F10.4)', "Double area: ", 2.0 * circle_area(r)
    
contains

    real function circle_area(radius)
        implicit none
        real, intent(in) :: radius
        real, parameter :: PI = 3.14159265
        
        circle_area = PI * radius * radius
    end function circle_area
    
    real function circle_circumference(radius)
        implicit none
        real, intent(in) :: radius
        real, parameter :: PI = 3.14159265
        
        circle_circumference = 2.0 * PI * radius
    end function circle_circumference

end program function_demo

Functions with Multiple Arguments

program multi_arg_function
    implicit none
    real :: x1, y1, x2, y2, dist
    
    x1 = 0.0; y1 = 0.0
    x2 = 3.0; y2 = 4.0
    
    dist = distance(x1, y1, x2, y2)
    print '(A, F10.4)', "Distance: ", dist   ! Should print 5.0
    
contains

    real function distance(x1, y1, x2, y2)
        implicit none
        real, intent(in) :: x1, y1, x2, y2
        
        distance = sqrt((x2 - x1)**2 + (y2 - y1)**2)
    end function distance

end program multi_arg_function

Integer and Character Functions

program various_functions
    implicit none
    integer :: n, fact
    character(len=50) :: name, upper_name
    
    n = 5
    fact = factorial(n)
    print '(A, I0, A, I0)', "Factorial of ", n, " is ", fact
    
    name = "hello world"
    upper_name = to_uppercase(name)
    print '(A, A)', "Uppercase: ", trim(upper_name)
    
contains

    integer function factorial(n)
        implicit none
        integer, intent(in) :: n
        integer :: i
        
        factorial = 1
        do i = 2, n
            factorial = factorial * i
        end do
    end function factorial
    
    function to_uppercase(str) result(upper)
        implicit none
        character(len=*), intent(in) :: str
        character(len=len(str)) :: upper
        integer :: i, ic
        
        upper = str
        do i = 1, len_trim(upper)
            ic = ichar(upper(i:i))
            if (ic >= ichar('a') .and. ic <= ichar('z')) then
                upper(i:i) = char(ic - 32)
            end if
        end do
    end function to_uppercase

end program various_functions
⚠️ Important: A function's return value must be assigned before the function ends. Forgetting to assign a value to the function name (or result variable) leads to undefined behavior.

3️⃣ Writing Subroutines in Fortran

Subroutines are the workhorses of Fortran. They can:

Basic Subroutine Syntax

subroutine swap(a, b)
    implicit none
    integer, intent(inout) :: a, b
    integer :: temp
    
    temp = a
    a = b
    b = temp
end subroutine swap

Calling Subroutines

Unlike functions, subroutines must be called with the call statement:

program subroutine_demo
    implicit none
    integer :: x, y
    
    x = 10
    y = 20
    
    print *, "Before swap: x =", x, ", y =", y
    call swap(x, y)
    print *, "After swap:  x =", x, ", y =", y
    
contains

    subroutine swap(a, b)
        implicit none
        integer, intent(inout) :: a, b
        integer :: temp
        
        temp = a
        a = b
        b = temp
    end subroutine swap

end program subroutine_demo

Output:

Before swap: x = 10 , y = 20
After swap:  x = 20 , y = 10

Subroutines with Multiple Outputs

This is where subroutines excel—returning multiple values:

program multi_output
    implicit none
    real :: a, b, c
    real :: sum_val, product_val, average_val
    
    a = 10.0
    b = 20.0
    c = 30.0
    
    call compute_statistics(a, b, c, sum_val, product_val, average_val)
    
    print '(A, F10.2)', "Sum:     ", sum_val
    print '(A, F10.2)', "Product: ", product_val
    print '(A, F10.2)', "Average: ", average_val
    
contains

    subroutine compute_statistics(x, y, z, total, prod, avg)
        implicit none
        real, intent(in) :: x, y, z
        real, intent(out) :: total, prod, avg
        
        total = x + y + z
        prod = x * y * z
        avg = total / 3.0
    end subroutine compute_statistics

end program multi_output

When to Use Functions vs. Subroutines

Scenario Use Function Use Subroutine
Compute single value ✅
Need multiple outputs ✅
Use result in expression ✅
Modify input arguments ✅
Perform I/O operations ✅ (recommended)
Initialize data structures ✅

4️⃣ Passing Arguments and the Intent Attribute

Fortran Passes Arguments by Reference

By default, Fortran passes all arguments by reference, meaning the subroutine or function receives the actual memory location of the variable, not a copy. This is similar to passing pointers in C or references in C++.

subroutine double_it(x)
    implicit none
    integer :: x    ! No intent specified - can be modified!
    x = x * 2       ! This CHANGES the original variable
end subroutine double_it

The Intent Attribute

To make your code safer and clearer, Fortran provides the intent attribute:

Intent Meaning Compiler Behavior
intent(in) Input only—cannot be modified Error if you try to modify
intent(out) Output only—must be assigned Undefined on entry
intent(inout) Both input and output Can read and modify
program intent_demo
    implicit none
    integer :: a, b, result
    
    a = 5
    b = 3
    
    call add_numbers(a, b, result)
    print *, "Sum:", result
    
contains

    subroutine add_numbers(x, y, sum)
        implicit none
        integer, intent(in) :: x, y      ! Cannot modify x or y
        integer, intent(out) :: sum      ! Must assign sum
        
        sum = x + y
        ! x = 0   ! ERROR: Cannot modify intent(in) argument
    end subroutine add_numbers

end program intent_demo
💡 Best Practice: Always use intent attributes. They:

Passing Arrays to Subprograms

Arrays can be passed in several ways:

Explicit-shape arrays (size passed as argument):

subroutine process_array(arr, n)
    implicit none
    integer, intent(in) :: n
    real, intent(inout) :: arr(n)
    integer :: i
    
    do i = 1, n
        arr(i) = arr(i) * 2.0
    end do
end subroutine process_array

Assumed-shape arrays (modern, preferred):

subroutine process_array(arr)
    implicit none
    real, intent(inout) :: arr(:)    ! Shape determined at runtime
    integer :: i
    
    do i = 1, size(arr)
        arr(i) = arr(i) * 2.0
    end do
end subroutine process_array

Complete array passing example:

program array_passing
    implicit none
    real :: data(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
    real :: total, avg, min_val, max_val
    
    print *, "Original array:", data
    
    call array_stats(data, total, avg, min_val, max_val)
    
    print '(A, F8.2)', "Sum:     ", total
    print '(A, F8.2)', "Average: ", avg
    print '(A, F8.2)', "Minimum: ", min_val
    print '(A, F8.2)', "Maximum: ", max_val
    
    call scale_array(data, 2.0)
    print *, "Scaled array:", data
    
contains

    subroutine array_stats(arr, total, avg, min_val, max_val)
        implicit none
        real, intent(in) :: arr(:)
        real, intent(out) :: total, avg, min_val, max_val
        
        total = sum(arr)
        avg = total / real(size(arr))
        min_val = minval(arr)
        max_val = maxval(arr)
    end subroutine array_stats
    
    subroutine scale_array(arr, factor)
        implicit none
        real, intent(inout) :: arr(:)
        real, intent(in) :: factor
        
        arr = arr * factor   ! Whole-array operation
    end subroutine scale_array

end program array_passing

Passing Allocatable Arrays

program allocatable_passing
    implicit none
    real, allocatable :: data(:)
    integer :: n
    
    print *, "Enter number of elements: "
    read *, n
    
    allocate(data(n))
    
    call fill_array(data)
    call print_array(data)
    
    deallocate(data)
    
contains

    subroutine fill_array(arr)
        implicit none
        real, intent(out) :: arr(:)
        integer :: i
        
        do i = 1, size(arr)
            arr(i) = real(i * i)
        end do
    end subroutine fill_array
    
    subroutine print_array(arr)
        implicit none
        real, intent(in) :: arr(:)
        integer :: i
        
        print *, "Array contents:"
        do i = 1, size(arr)
            print '(A, I3, A, F8.2)', "  arr(", i, ") = ", arr(i)
        end do
    end subroutine print_array

end program allocatable_passing

5️⃣ Internal Subprograms with Contains

The contains statement allows you to define internal subprograms within a program or another subprogram. This is similar to nested functions in some languages.

Advantages of Internal Subprograms

  1. Automatic access to host variables — Internal subprograms can access variables from the containing program (host association)
  2. No interface needed — The compiler knows the interface automatically
  3. Encapsulation — Keeps related code together
program contains_demo
    implicit none
    real :: global_scale = 2.0    ! Accessible to internal subprograms
    real :: values(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
    
    print *, "Original:", values
    call apply_scale(values)
    print *, "Scaled:  ", values
    
contains

    subroutine apply_scale(arr)
        implicit none
        real, intent(inout) :: arr(:)
        
        ! global_scale is accessible here via host association
        arr = arr * global_scale
    end subroutine apply_scale

end program contains_demo
⚠️ Warning: While host association is convenient, it can make code harder to understand and maintain. Prefer passing values as arguments for clarity.

Structure of a Program with Contains

program my_program
    implicit none
    ! Variable declarations
    ! Executable statements
    
contains

    function my_function(args)
        ! Function body
    end function my_function
    
    subroutine my_subroutine(args)
        ! Subroutine body
    end subroutine my_subroutine

end program my_program

6️⃣ Modules: Organizing Reusable Code

Modules are Fortran's primary mechanism for:

Basic Module Structure

module math_utils
    implicit none
    
    ! Module variables (accessible to all who use this module)
    real, parameter :: PI = 3.14159265358979
    real, parameter :: E = 2.71828182845905
    
contains

    real function circle_area(radius)
        real, intent(in) :: radius
        circle_area = PI * radius * radius
    end function circle_area
    
    real function circle_circumference(radius)
        real, intent(in) :: radius
        circle_circumference = 2.0 * PI * radius
    end function circle_circumference
    
    subroutine degrees_to_radians(degrees, radians)
        real, intent(in) :: degrees
        real, intent(out) :: radians
        radians = degrees * PI / 180.0
    end subroutine degrees_to_radians

end module math_utils

Using a Module

program use_module_demo
    use math_utils    ! Import the module
    implicit none
    real :: r, angle_deg, angle_rad
    
    r = 5.0
    print '(A, F10.6)', "PI = ", PI    ! Access module constant
    print '(A, F10.4)', "Area of circle with r=5: ", circle_area(r)
    
    angle_deg = 90.0
    call degrees_to_radians(angle_deg, angle_rad)
    print '(A, F6.1, A, F10.6)', "90 degrees = ", angle_rad, " radians"
    
end program use_module_demo

Selective Import with only

You can import only specific items from a module:

program selective_import
    use math_utils, only: PI, circle_area   ! Import only what we need
    implicit none
    
    print *, "PI =", PI
    print *, "Area =", circle_area(3.0)
    ! print *, "E =", E   ! ERROR: E was not imported
    
end program selective_import

Renaming Imported Items

program rename_import
    use math_utils, only: PI, area => circle_area   ! Rename circle_area to area
    implicit none
    
    print *, "Area =", area(3.0)    ! Use the renamed function
    
end program rename_import

Public and Private Access

Control what users of your module can access:

module controlled_access
    implicit none
    
    private                      ! Default: everything is private
    public :: public_function    ! Explicitly make public
    
    integer :: internal_counter = 0   ! Private - cannot be accessed outside
    
contains

    function public_function(x) result(y)
        real, intent(in) :: x
        real :: y
        call internal_helper()
        y = x * 2.0
    end function public_function
    
    subroutine internal_helper()
        ! Private subroutine - only callable within this module
        internal_counter = internal_counter + 1
    end subroutine internal_helper

end module controlled_access

Complete Module Example: Statistics Library

module statistics
    implicit none
    private
    public :: mean, variance, std_dev, describe_array
    
contains

    real function mean(arr)
        real, intent(in) :: arr(:)
        mean = sum(arr) / real(size(arr))
    end function mean
    
    real function variance(arr)
        real, intent(in) :: arr(:)
        real :: avg, sum_sq
        integer :: i
        
        avg = mean(arr)
        sum_sq = 0.0
        do i = 1, size(arr)
            sum_sq = sum_sq + (arr(i) - avg)**2
        end do
        variance = sum_sq / real(size(arr))
    end function variance
    
    real function std_dev(arr)
        real, intent(in) :: arr(:)
        std_dev = sqrt(variance(arr))
    end function std_dev
    
    subroutine describe_array(arr)
        real, intent(in) :: arr(:)
        
        print *, "=== Array Statistics ==="
        print '(A, I0)',    "  Count:    ", size(arr)
        print '(A, F12.4)', "  Sum:      ", sum(arr)
        print '(A, F12.4)', "  Mean:     ", mean(arr)
        print '(A, F12.4)', "  Variance: ", variance(arr)
        print '(A, F12.4)', "  Std Dev:  ", std_dev(arr)
        print '(A, F12.4)', "  Minimum:  ", minval(arr)
        print '(A, F12.4)', "  Maximum:  ", maxval(arr)
    end subroutine describe_array

end module statistics

program stats_demo
    use statistics
    implicit none
    real :: data(10) = [23.5, 45.2, 12.8, 67.3, 34.1, &
                        89.4, 21.7, 56.9, 43.2, 78.6]
    
    call describe_array(data)
    
end program stats_demo

7️⃣ Interface Blocks: Explicit Interfaces for External Procedures

When a subprogram is defined outside of your program (not in contains or a module), Fortran doesn't automatically know its interface (argument types, return type, etc.). This can lead to subtle bugs.

When Do You Need an Interface Block?

Situation Interface Needed?
Internal subprogram (in contains) No - automatic
Subprogram in a module (use module) No - automatic
External subprogram in separate file Yes
Assumed-shape array arguments Yes
Pointer or allocatable arguments Yes
Optional arguments Yes
Keyword arguments Yes

Interface Block Syntax

interface
    function function_name(args) result(res)
        type, intent(in) :: args
        type :: res
    end function function_name
    
    subroutine subroutine_name(args)
        type, intent(...) :: args
    end subroutine subroutine_name
end interface

Example: External Function Without Interface (Dangerous!)

File: external_func.f90

real function square(x)
    implicit none
    real, intent(in) :: x
    square = x * x
end function square

File: main_no_interface.f90 (Incorrect — works by luck)

program no_interface_demo
    implicit none
    real :: result, square   ! Declaring square as real is NOT an interface!
    
    result = square(5.0)     ! May work, but no type checking!
    print *, "Result:", result
    
end program no_interface_demo

Example: External Function With Interface (Safe!)

File: main_with_interface.f90 (Correct)

program with_interface_demo
    implicit none
    real :: result
    
    ! Explicit interface tells compiler about the function
    interface
        real function square(x)
            real, intent(in) :: x
        end function square
    end interface
    
    result = square(5.0)     ! Compiler can now check types
    print *, "Result:", result
    
end program with_interface_demo

Interface Required for Assumed-Shape Arrays

If a subprogram takes an assumed-shape array (arr(:)), an explicit interface is required:

File: array_ops.f90

subroutine double_array(arr)
    implicit none
    real, intent(inout) :: arr(:)   ! Assumed-shape array
    arr = arr * 2.0
end subroutine double_array

File: main_array.f90

program array_interface_demo
    implicit none
    real :: data(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
    
    ! Interface REQUIRED for assumed-shape arrays
    interface
        subroutine double_array(arr)
            real, intent(inout) :: arr(:)
        end subroutine double_array
    end interface
    
    print *, "Before:", data
    call double_array(data)
    print *, "After: ", data
    
end program array_interface_demo

The Best Solution: Use Modules Instead!

Modules automatically provide interfaces, making your code safer and cleaner:

File: array_module.f90

module array_operations
    implicit none
contains
    subroutine double_array(arr)
        real, intent(inout) :: arr(:)
        arr = arr * 2.0
    end subroutine double_array
end module array_operations

File: main_module.f90

program module_demo
    use array_operations    ! Interface is automatic!
    implicit none
    real :: data(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
    
    print *, "Before:", data
    call double_array(data)
    print *, "After: ", data
    
end program module_demo
💡 Best Practice: Prefer modules over external procedures with interface blocks. Modules are safer, cleaner, and the modern Fortran way.

8️⃣ Separate Compilation: Multi-File Programs

Real-world Fortran projects typically span multiple files. Here's how to organize and compile them.

Project Structure Example

my_project/
├── math_module.f90       # Module with math functions
├── io_module.f90         # Module with I/O routines
├── main.f90               # Main program
└── Makefile               # Build instructions

File 1: math_module.f90

module math_module
    implicit none
    private
    public :: factorial, fibonacci, is_prime, gcd
    
contains

    integer function factorial(n)
        integer, intent(in) :: n
        integer :: i
        
        factorial = 1
        do i = 2, n
            factorial = factorial * i
        end do
    end function factorial
    
    integer function fibonacci(n)
        integer, intent(in) :: n
        integer :: i, prev, curr, next
        
        if (n <= 1) then
            fibonacci = n
            return
        end if
        
        prev = 0
        curr = 1
        do i = 2, n
            next = prev + curr
            prev = curr
            curr = next
        end do
        fibonacci = curr
    end function fibonacci
    
    logical function is_prime(n)
        integer, intent(in) :: n
        integer :: i
        
        if (n < 2) then
            is_prime = .false.
            return
        end if
        
        if (n == 2) then
            is_prime = .true.
            return
        end if
        
        if (mod(n, 2) == 0) then
            is_prime = .false.
            return
        end if
        
        do i = 3, int(sqrt(real(n))), 2
            if (mod(n, i) == 0) then
                is_prime = .false.
                return
            end if
        end do
        
        is_prime = .true.
    end function is_prime
    
    integer function gcd(a, b)
        integer, intent(in) :: a, b
        integer :: x, y, temp
        
        x = abs(a)
        y = abs(b)
        
        do while (y /= 0)
            temp = y
            y = mod(x, y)
            x = temp
        end do
        
        gcd = x
    end function gcd

end module math_module

File 2: io_module.f90

module io_module
    implicit none
    private
    public :: print_header, print_separator, get_integer_input
    
contains

    subroutine print_header(title)
        character(len=*), intent(in) :: title
        
        print *, ""
        call print_separator()
        print '(A)', "  " // trim(title)
        call print_separator()
    end subroutine print_header
    
    subroutine print_separator()
        print '(A)', "=========================================="
    end subroutine print_separator
    
    subroutine get_integer_input(prompt, value, min_val, max_val)
        character(len=*), intent(in) :: prompt
        integer, intent(out) :: value
        integer, intent(in), optional :: min_val, max_val
        integer :: ios
        
        do
            print '(A)', prompt
            read(*, *, iostat=ios) value
            
            if (ios /= 0) then
                print *, "Invalid input. Please enter an integer."
                cycle
            end if
            
            if (present(min_val)) then
                if (value < min_val) then
                    print '(A, I0)', "Value must be at least ", min_val
                    cycle
                end if
            end if
            
            if (present(max_val)) then
                if (value > max_val) then
                    print '(A, I0)', "Value must be at most ", max_val
                    cycle
                end if
            end if
            
            exit
        end do
    end subroutine get_integer_input

end module io_module

File 3: main.f90

program multi_file_demo
    use math_module
    use io_module
    implicit none
    
    integer :: choice, n, a, b
    
    call print_header("Math Functions Demo")
    
    print *, "Choose a function:"
    print *, "  1. Factorial"
    print *, "  2. Fibonacci"
    print *, "  3. Prime Check"
    print *, "  4. GCD"
    print *, ""
    
    call get_integer_input("Enter choice (1-4): ", choice, 1, 4)
    
    select case (choice)
    case (1)
        call get_integer_input("Enter n: ", n, 0, 12)
        print '(A, I0, A, I0)', "Factorial(", n, ") = ", factorial(n)
        
    case (2)
        call get_integer_input("Enter n: ", n, 0, 40)
        print '(A, I0, A, I0)', "Fibonacci(", n, ") = ", fibonacci(n)
        
    case (3)
        call get_integer_input("Enter n: ", n, 1)
        if (is_prime(n)) then
            print '(I0, A)', n, " is prime"
        else
            print '(I0, A)', n, " is not prime"
        end if
        
    case (4)
        call get_integer_input("Enter first number: ", a)
        call get_integer_input("Enter second number: ", b)
        print '(A, I0, A, I0, A, I0)', "GCD(", a, ", ", b, ") = ", gcd(a, b)
        
    end select
    
    call print_separator()
    
end program multi_file_demo

Compiling Multi-File Programs

Option 1: All at once

gfortran math_module.f90 io_module.f90 main.f90 -o math_demo

Option 2: Separate compilation (better for large projects)

# Compile modules first (creates .mod files)
gfortran -c math_module.f90
gfortran -c io_module.f90

# Compile main program
gfortran -c main.f90

# Link everything together
gfortran math_module.o io_module.o main.o -o math_demo
⚠️ Important: When compiling separately, modules must be compiled before any file that uses them. The compiler creates .mod files that contain interface information.

Simple Makefile

FC = gfortran
FFLAGS = -Wall -g

OBJS = math_module.o io_module.o main.o

math_demo: $(OBJS)
	$(FC) $(FFLAGS) -o $@ $(OBJS)

math_module.o: math_module.f90
	$(FC) $(FFLAGS) -c __CODE_BLOCK_0__lt;

io_module.o: io_module.f90
	$(FC) $(FFLAGS) -c __CODE_BLOCK_0__lt;

main.o: main.f90 math_module.o io_module.o
	$(FC) $(FFLAGS) -c __CODE_BLOCK_0__lt;

clean:
	rm -f *.o *.mod math_demo

9️⃣ Complete Examples

Example A: All-in-One File

This example puts everything in a single file using contains:

program all_in_one
    implicit none
    real, allocatable :: data(:)
    real :: mean_val, std_val, min_val, max_val
    integer :: n, i
    
    ! Get array size from user
    print *, "Enter number of data points: "
    read *, n
    
    if (n < 1) then
        print *, "Error: Need at least 1 data point"
        stop
    end if
    
    ! Allocate and fill array
    allocate(data(n))
    
    print *, "Enter ", n, " values:"
    do i = 1, n
        print '(A, I0, A)', "  Value ", i, ": "
        read *, data(i)
    end do
    
    ! Calculate statistics using internal subprograms
    mean_val = calculate_mean(data)
    std_val = calculate_std_dev(data, mean_val)
    call find_min_max(data, min_val, max_val)
    
    ! Display results
    call print_results(data, mean_val, std_val, min_val, max_val)
    
    ! Clean up
    deallocate(data)
    
contains

    ! FUNCTION: Calculate mean of an array
    real function calculate_mean(arr)
        real, intent(in) :: arr(:)
        calculate_mean = sum(arr) / real(size(arr))
    end function calculate_mean
    
    ! FUNCTION: Calculate standard deviation
    real function calculate_std_dev(arr, mean_val)
        real, intent(in) :: arr(:)
        real, intent(in) :: mean_val
        real :: sum_sq
        integer :: i
        
        sum_sq = 0.0
        do i = 1, size(arr)
            sum_sq = sum_sq + (arr(i) - mean_val)**2
        end do
        calculate_std_dev = sqrt(sum_sq / real(size(arr)))
    end function calculate_std_dev
    
    ! SUBROUTINE: Find minimum and maximum values
    subroutine find_min_max(arr, min_val, max_val)
        real, intent(in) :: arr(:)
        real, intent(out) :: min_val, max_val
        
        min_val = minval(arr)
        max_val = maxval(arr)
    end subroutine find_min_max
    
    ! SUBROUTINE: Print formatted results
    subroutine print_results(arr, mean_val, std_val, min_val, max_val)
        real, intent(in) :: arr(:)
        real, intent(in) :: mean_val, std_val, min_val, max_val
        
        print *, ""
        print *, "========== RESULTS =========="
        print '(A, I0)', "  Data points: ", size(arr)
        print '(A, F12.4)', "  Mean:        ", mean_val
        print '(A, F12.4)', "  Std Dev:     ", std_val
        print '(A, F12.4)', "  Minimum:     ", min_val
        print '(A, F12.4)', "  Maximum:     ", max_val
        print '(A, F12.4)', "  Range:       ", max_val - min_val
        print *, "============================="
    end subroutine print_results

end program all_in_one

Example B: Separate Files with Module

File: stats_module.f90

module stats_module
    implicit none
    private
    public :: calculate_mean, calculate_std_dev, find_min_max, print_results
    
contains

    real function calculate_mean(arr)
        real, intent(in) :: arr(:)
        calculate_mean = sum(arr) / real(size(arr))
    end function calculate_mean
    
    real function calculate_std_dev(arr, mean_val)
        real, intent(in) :: arr(:)
        real, intent(in) :: mean_val
        real :: sum_sq
        integer :: i
        
        sum_sq = 0.0
        do i = 1, size(arr)
            sum_sq = sum_sq + (arr(i) - mean_val)**2
        end do
        calculate_std_dev = sqrt(sum_sq / real(size(arr)))
    end function calculate_std_dev
    
    subroutine find_min_max(arr, min_val, max_val)
        real, intent(in) :: arr(:)
        real, intent(out) :: min_val, max_val
        
        min_val = minval(arr)
        max_val = maxval(arr)
    end subroutine find_min_max
    
    subroutine print_results(arr, mean_val, std_val, min_val, max_val)
        real, intent(in) :: arr(:)
        real, intent(in) :: mean_val, std_val, min_val, max_val
        
        print *, ""
        print *, "========== RESULTS =========="
        print '(A, I0)', "  Data points: ", size(arr)
        print '(A, F12.4)', "  Mean:        ", mean_val
        print '(A, F12.4)', "  Std Dev:     ", std_val
        print '(A, F12.4)', "  Minimum:     ", min_val
        print '(A, F12.4)', "  Maximum:     ", max_val
        print '(A, F12.4)', "  Range:       ", max_val - min_val
        print *, "============================="
    end subroutine print_results

end module stats_module

File: main_separate.f90

program main_separate
    use stats_module
    implicit none
    real, allocatable :: data(:)
    real :: mean_val, std_val, min_val, max_val
    integer :: n, i
    
    print *, "Enter number of data points: "
    read *, n
    
    if (n < 1) then
        print *, "Error: Need at least 1 data point"
        stop
    end if
    
    allocate(data(n))
    
    print *, "Enter ", n, " values:"
    do i = 1, n
        print '(A, I0, A)', "  Value ", i, ": "
        read *, data(i)
    end do
    
    ! Call module functions and subroutines
    mean_val = calculate_mean(data)
    std_val = calculate_std_dev(data, mean_val)
    call find_min_max(data, min_val, max_val)
    call print_results(data, mean_val, std_val, min_val, max_val)
    
    deallocate(data)
    
end program main_separate

Compile with:

gfortran stats_module.f90 main_separate.f90 -o stats_program

📋 Quick Reference

Task Syntax
Define a function real function name(args) or function name(args) result(res)
Define a subroutine subroutine name(args)
Call a subroutine call subroutine_name(args)
Input-only argument type, intent(in) :: arg
Output-only argument type, intent(out) :: arg
Input/output argument type, intent(inout) :: arg
Assumed-shape array real, intent(in) :: arr(:)
Define a module module name ... contains ... end module name
Use a module use module_name
Selective import use module_name, only: item1, item2
Rename on import use module_name, only: new_name => old_name
Interface block interface ... end interface
Internal subprograms After contains in program
Make private private or private :: item
Make public public or public :: item

📝 Programming Exercise: Vector Operations Library

Write a Fortran program that implements a vector mathematics library using both functions and subroutines. Your implementation should demonstrate modular design and proper use of interfaces.

Requirements

Part 1: Create a Module (vector_module.f90)

Create a module named vector_ops that provides the following:

Functions (return a single value):

  1. vector_magnitude(v) — Returns the magnitude (length) of a vector
  2. dot_product_custom(v1, v2) — Returns the dot product of two vectors (name it dot_product_custom to avoid conflict with Fortran's intrinsic)
  3. vector_angle(v1, v2) — Returns the angle between two vectors in degrees

Subroutines (modify arguments):

  1. vector_add(v1, v2, result) — Adds two vectors, stores result in third argument
  2. vector_scale(v, scalar, result) — Multiplies vector by scalar, stores result
  3. vector_normalize(v, result) — Creates a unit vector in the same direction
  4. vector_cross_product(v1, v2, result) — Computes cross product (for 3D vectors only)

Part 2: Create the Main Program (vector_main.f90)

Write a main program that:

  1. Uses the vector_ops module
  2. Demonstrates each function and subroutine
  3. Uses allocatable arrays for vectors
  4. Properly deallocates memory before exiting

Part 3: Add an Interface Block Example

Create an additional file (external_func.f90) containing a single external function:

real function vector_sum(v, n)
    ! Returns the sum of all elements in vector v

Modify your main program to also call this external function, using a proper interface block to declare it.

Sample Output

=== Vector Operations Demo ===

Vector A: [ 3.00,  4.00,  0.00]
Vector B: [ 1.00,  0.00,  0.00]

FUNCTION RESULTS:
  Magnitude of A:    5.0000
  Magnitude of B:    1.0000
  Dot product A·B:   3.0000
  Angle between A and B: 53.1301 degrees

SUBROUTINE RESULTS:
  A + B = [ 4.00,  4.00,  0.00]
  A × 2 = [ 6.00,  8.00,  0.00]
  Normalized A = [ 0.60,  0.80,  0.00]

3D Cross Product Demo:
  Vector C: [ 1.00,  0.00,  0.00]
  Vector D: [ 0.00,  1.00,  0.00]
  C × D = [ 0.00,  0.00,  1.00]

EXTERNAL FUNCTION (with interface):
  Sum of A elements: 7.0000

Memory deallocated successfully.

Formulas

Magnitude:

\[ |\vec{v}| = \sqrt{v_1^2 + v_2^2 + v_3^2} \]

Dot Product:

\[ \vec{v_1} \cdot \vec{v_2} = v_{1x}v_{2x} + v_{1y}v_{2y} + v_{1z}v_{2z} \]

Angle Between Vectors:

\[ \theta = \arccos\left(\frac{\vec{v_1} \cdot \vec{v_2}}{|\vec{v_1}| \cdot |\vec{v_2}|}\right) \]

Cross Product:

\[ \vec{v_1} \times \vec{v_2} = \begin{pmatrix} v_{1y}v_{2z} - v_{1z}v_{2y} \\ v_{1z}v_{2x} - v_{1x}v_{2z} \\ v_{1x}v_{2y} - v_{1y}v_{2x} \end{pmatrix} \]

Hints

File Structure

submission/
├── vector_module.f90      # Module with vector operations
├── external_func.f90      # External function with vector_sum
├── vector_main.f90        # Main program
└── Makefile                # Optional: build script

Compilation

# Compile the module first
gfortran -c vector_module.f90

# Compile the external function
gfortran -c external_func.f90

# Compile and link main program
gfortran vector_module.o external_func.o vector_main.f90 -o vector_demo

# Run
./vector_demo