🔬 Object-Oriented Programming in Modern Fortran

Created by: Andrew J. Pounds, Ph.D.

Welcome to a comprehensive guide on Object-Oriented Programming (OOP) in Modern Fortran. While Fortran is often associated with its procedural heritage, Fortran 2003 and later standards introduced powerful OOP features that rival those found in Java, C++, and D. This document explores how Fortran approaches OOP concepts and provides practical examples using the GNU Fortran Compiler (gfortran). By understanding Fortran's OOP capabilities, you'll be able to write more modular, maintainable, and reusable scientific and engineering code.

📚 Learning Objectives

After completing this exercise, you will be able to:

❓ What is Object-Oriented Programming?

Object-Oriented Programming is a paradigm that organizes code around "objects" that contain both data (variables) and functions (methods). Think of an object like a blueprint: a Drink type defines what properties all drinks have and what operations can be performed on them.

The core principles of OOP are:

Modern Fortran (2003 and later) supports all of these principles through derived types, type-bound procedures, type extension, and polymorphic variables.

📊 Language Feature Comparison Table

Feature Java C++ D Modern Fortran
Class/Type Definition class ClassName class ClassName class ClassName type :: TypeName
Constructor ClassName() ClassName() this() Interface + function
Destructor Automatic (GC) ~ClassName() Automatic or ~this() final :: cleanup
Inheritance extends : : extends(ParentType)
Access Modifiers public, private, protected public, private, protected public, private, protected public, private
Method Binding Automatic virtual keyword Automatic procedure :: in contains
Polymorphism Virtual by default Must use virtual Virtual by default class() + select type
Memory Management Automatic (GC) Manual (new/delete) Automatic or Manual allocatable + auto dealloc
Abstract Types abstract class Pure virtual classes abstract keyword abstract + deferred
Constants final keyword const keyword immutable keyword parameter attribute
This/Self Reference this this-> this Passed argument (e.g., self)
Compilation javac g++ gdc gfortran

✏️ Example 1: Basic Type Definition

Let's compare how to define a basic class/type in all four languages:

☕ Java

public class Drink {
    final int MAXIMUM = 10;
    private String name;
    
    public Drink(String brandName) {
        name = brandName;
    }
    
    public String getName() {
        return name;
    }
}

⚙️ C++

class Drink {
private:
    static const int MAXIMUM = 10;
    string name;
    
public:
    Drink(string brandName) {
        name = brandName;
    }
    
    string getName() {
        return name;
    }
};

🐲 D

class Drink {
    immutable int MAXIMUM = 10;
    private string name;
    
    this(string brandName) {
        name = brandName;
    }
    
    string getName() {
        return name;
    }
}

🔬 Modern Fortran

type :: Drink
    integer, parameter :: MAXIMUM = 10
    private
    character(len=50) :: name
contains
    procedure :: get_name
end type Drink

interface Drink
    module procedure create_drink
end interface

Key Observations for Fortran:

✏️ Example 2: Constructor Patterns

Fortran uses a different approach to constructors than other OOP languages. Instead of a special constructor syntax, Fortran uses interface blocks to create constructor-like behavior:

☕ Java

public Drink(String name) {
    this.name = name;
    this.cost = 1.75;
}

public Drink(String name, 
             double cost) {
    this.name = name;
    this.cost = cost;
}

🔬 Modern Fortran

! Generic interface acts as constructor
interface Drink
    module procedure create_drink
    module procedure create_drink_cost
end interface

! Constructor function 1
function create_drink(name) result(d)
    character(len=*), intent(in) :: name
    type(Drink) :: d
    d%name = name
    d%cost = 1.75
end function

! Constructor function 2
function create_drink_cost(name, cost) &
         result(d)
    character(len=*), intent(in) :: name
    real, intent(in) :: cost
    type(Drink) :: d
    d%name = name
    d%cost = cost
end function
📝 Note: In Fortran, the interface block with the same name as the type creates a generic constructor. When you write Drink("Coke"), Fortran automatically selects the appropriate constructor function based on the arguments provided. This is called generic resolution.

✏️ Example 3: Methods and Type-Bound Procedures

Here's how methods work in Fortran compared to other languages, using the vending machine example:

☕ Java

public void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        System.out.println(
            name + " Purchased.");
    }
}

public double profit() {
    return moneyMade;
}

🔬 Modern Fortran

subroutine vend(self)
    class(Drink), intent(inout) :: self
    if (self%available > 0) then
        self%available = self%available - 1
        self%consumed = self%consumed + 1
        self%money_made = self%money_made &
                        + self%cost
        print *, trim(self%name), &
                 " Purchased."
    end if
end subroutine vend

real function profit(self)
    class(Drink), intent(in) :: self
    profit = self%money_made
end function profit

Key Observations for Fortran Methods:

🥤 The Complete Vending Machine Example

Now let's look at a complete, working example using Modern Fortran and the GNU Fortran Compiler (gfortran). This program implements a simple vending machine that tracks drink sales and profits using separate module and program files.

⚠️ Important: Module Organization in Fortran

When using Fortran with separate files, modules must be compiled before any program unit that uses them. The compiler creates .mod files that contain interface information needed by the use statement.

✅ The Correct Approach: Place your derived type and all its methods in a module file, then use that module in your main program. This ensures proper encapsulation and automatic interface generation.

Why Modules Are Essential for Fortran OOP

Using modules for OOP in Fortran is crucial because:


🔬 drink_module.f90 - The Drink Type Module
!===============================================================================
! drink_module.f90
!
! A module defining the Drink derived type with type-bound procedures.
! This demonstrates Object-Oriented Programming in Modern Fortran.
!
! OOP Features Demonstrated:
!   - Derived type definition (similar to a class)
!   - Type-bound procedures (methods)
!   - Encapsulation with private components
!   - Constructor using generic interface
!   - Getter methods for controlled access
!
! Author: Student Name
! Course: Fortran Programming
!===============================================================================

module drink_module
    implicit none
    
    !---------------------------------------------------------------------------
    ! Access Control
    ! By default, make everything private, then explicitly export public items
    !---------------------------------------------------------------------------
    private
    public :: Drink      ! Export the type AND the constructor interface
    
    !---------------------------------------------------------------------------
    ! Module-level constants (like static final in Java)
    !---------------------------------------------------------------------------
    integer, parameter :: MAXIMUM_CAPACITY = 10
    real, parameter :: DEFAULT_DRINK_COST = 1.75
    
    !---------------------------------------------------------------------------
    ! Derived Type Definition (similar to a class)
    !
    ! The 'private' statement makes all components private by default.
    ! This enforces encapsulation - users must use methods to access data.
    !---------------------------------------------------------------------------
    type :: Drink
        private                              ! All components are private
        character(len=50) :: name            ! Name of the drink
        integer :: available = 0             ! Drinks currently in stock
        integer :: consumed = 0              ! Total drinks sold
        real :: drink_cost = DEFAULT_DRINK_COST  ! Price per drink
        real :: money_made = 0.0             ! Total revenue
    contains
        !-----------------------------------------------------------------------
        ! Type-Bound Procedures (methods)
        ! These are bound to the type and called using the % operator
        !-----------------------------------------------------------------------
        procedure :: vend            ! Dispense a drink
        procedure :: refill          ! Restock to maximum capacity
        procedure :: profit          ! Get total revenue (rounded)
        procedure :: drinks_sold     ! Get number of drinks sold
        procedure :: get_drink_name  ! Get the drink name
        procedure :: restock_amount  ! Get number needed to restock
    end type Drink
    
    !---------------------------------------------------------------------------
    ! Generic Interface for Constructor
    !
    ! This creates a "constructor" that can be called as Drink(name) or
    ! Drink(name, cost). The compiler selects the appropriate function
    ! based on the arguments provided.
    !---------------------------------------------------------------------------
    interface Drink
        module procedure create_drink
        module procedure create_drink_with_cost
    end interface Drink

contains

    !===========================================================================
    ! CONSTRUCTOR FUNCTIONS
    !===========================================================================
    
    !---------------------------------------------------------------------------
    ! create_drink: Constructor with name only (uses default cost)
    !
    ! Arguments:
    !   brand_name - Name of the drink
    !
    ! Returns:
    !   A new Drink object initialized with default values
    !---------------------------------------------------------------------------
    function create_drink(brand_name) result(d)
        character(len=*), intent(in) :: brand_name
        type(Drink) :: d
        
        d%name = brand_name
        d%consumed = 0
        d%available = 0
        d%money_made = 0.0
        d%drink_cost = DEFAULT_DRINK_COST
        
    end function create_drink
    
    !---------------------------------------------------------------------------
    ! create_drink_with_cost: Constructor with name and custom cost
    !
    ! Arguments:
    !   brand_name - Name of the drink
    !   cost       - Custom price for this drink
    !
    ! Returns:
    !   A new Drink object initialized with the specified cost
    !---------------------------------------------------------------------------
    function create_drink_with_cost(brand_name, cost) result(d)
        character(len=*), intent(in) :: brand_name
        real, intent(in) :: cost
        type(Drink) :: d
        
        d%name = brand_name
        d%consumed = 0
        d%available = 0
        d%money_made = 0.0
        d%drink_cost = cost
        
    end function create_drink_with_cost
    
    !===========================================================================
    ! TYPE-BOUND PROCEDURES (METHODS)
    !===========================================================================
    
    !---------------------------------------------------------------------------
    ! vend: Dispense a drink if available
    !
    ! This is a subroutine (not a function) because it modifies the object
    ! and produces output as side effects.
    !
    ! The 'self' argument:
    !   - Uses 'class(Drink)' for polymorphism support
    !   - Uses 'intent(inout)' because we modify the object
    !---------------------------------------------------------------------------
    subroutine vend(self)
        class(Drink), intent(inout) :: self
        
        if (self%available > 0) then
            self%available = self%available - 1
            self%consumed = self%consumed + 1
            self%money_made = self%money_made + self%drink_cost
            print '(A, A)', trim(self%name), " Purchased."
        else
            print *, ""
            print *, " \\\\\\\\\\\\"
            print *, " \ SOLD OUT \"
            print *, " \\\\\\\\\\\\"
            print *, ""
        end if
        
    end subroutine vend
    
    !---------------------------------------------------------------------------
    ! refill: Restock drinks to maximum capacity
    !---------------------------------------------------------------------------
    subroutine refill(self)
        class(Drink), intent(inout) :: self
        
        self%available = MAXIMUM_CAPACITY
        
    end subroutine refill
    
    !---------------------------------------------------------------------------
    ! profit: Calculate and return the total revenue (rounded to cents)
    !
    ! This is a function because it returns a value without side effects.
    ! Uses 'intent(in)' because we only read from the object.
    !---------------------------------------------------------------------------
    real function profit(self)
        class(Drink), intent(in) :: self
        
        ! Round to 2 decimal places (cents)
        profit = nint(self%money_made * 100.0) / 100.0
        
    end function profit
    
    !---------------------------------------------------------------------------
    ! drinks_sold: Return the number of drinks sold
    !---------------------------------------------------------------------------
    integer function drinks_sold(self)
        class(Drink), intent(in) :: self
        
        drinks_sold = self%consumed
        
    end function drinks_sold
    
    !---------------------------------------------------------------------------
    ! get_drink_name: Return the drink name
    !
    ! This is a "getter" method - provides controlled access to private data
    !---------------------------------------------------------------------------
    function get_drink_name(self) result(name)
        class(Drink), intent(in) :: self
        character(len=50) :: name
        
        name = self%name
        
    end function get_drink_name
    
    !---------------------------------------------------------------------------
    ! restock_amount: Return the number of drinks needed to reach full capacity
    !---------------------------------------------------------------------------
    integer function restock_amount(self)
        class(Drink), intent(in) :: self
        
        restock_amount = MAXIMUM_CAPACITY - self%available
        
    end function restock_amount

end module drink_module

🔬 machine.f90 - The Driver Program
!===============================================================================
! machine.f90
!
! Main program demonstrating the use of the Drink derived type.
! This is the "driver" program that creates objects and calls methods.
!
! Demonstrates:
!   - Using a module with 'use' statement
!   - Creating objects using the constructor
!   - Calling type-bound procedures (methods) with %
!   - Working with multiple objects
!
! Author: Student Name
! Course: Fortran Programming
!===============================================================================

program machine
    !---------------------------------------------------------------------------
    ! Use the drink_module
    ! This imports the Drink type and its constructor interface
    !---------------------------------------------------------------------------
    use drink_module
    implicit none
    
    !---------------------------------------------------------------------------
    ! Declare variables
    ! Note: We use type(Drink) to declare objects of our derived type
    !---------------------------------------------------------------------------
    type(Drink) :: drink1, drink2, drink3, drink4
    integer :: total_sold
    real :: total_profit
    
    !---------------------------------------------------------------------------
    ! Create drink objects using the constructor
    !
    ! The generic interface allows two forms:
    !   - Drink(name)        -> uses default cost of $1.75
    !   - Drink(name, cost)  -> uses specified cost
    !
    ! Note the syntax: this looks like a function call, but it's actually
    ! invoking our constructor through the generic interface.
    !---------------------------------------------------------------------------
    drink1 = Drink("Coke")
    drink2 = Drink("Sprite")
    drink3 = Drink("Doctor Pepper")
    drink4 = Drink("V8 Splash", 4.25)    ! Custom price
    
    !---------------------------------------------------------------------------
    ! Call the refill method on each drink
    !
    ! Syntax: object%method()
    ! This is equivalent to: call refill(drink1) but more OOP-like
    !---------------------------------------------------------------------------
    call drink1%refill()
    call drink2%refill()
    call drink3%refill()
    call drink4%refill()
    
    !---------------------------------------------------------------------------
    ! Simulate purchases by calling vend() multiple times
    !---------------------------------------------------------------------------
    
    ! Purchase 8 Cokes
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    call drink1%vend()
    
    ! Purchase 6 Sprites
    call drink2%vend()
    call drink2%vend()
    call drink2%vend()
    call drink2%vend()
    call drink2%vend()
    call drink2%vend()
    
    ! Purchase 2 Doctor Peppers
    call drink3%vend()
    call drink3%vend()
    
    ! Attempt to purchase 11 V8 Splash (only 10 available - will show SOLD OUT)
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()
    call drink4%vend()    ! This one will fail - SOLD OUT
    
    !---------------------------------------------------------------------------
    ! Calculate totals using getter methods
    !
    ! Note: For functions, we can use them in expressions directly
    !       drink1%drinks_sold() returns an integer
    !       drink1%profit() returns a real
    !---------------------------------------------------------------------------
    total_sold = drink1%drinks_sold() + drink2%drinks_sold() + &
                 drink3%drinks_sold() + drink4%drinks_sold()
    
    total_profit = drink1%profit() + drink2%profit() + &
                   drink3%profit() + drink4%profit()
    
    !---------------------------------------------------------------------------
    ! Display results
    !---------------------------------------------------------------------------
    print *, ""
    print '(A, I0, A, F0.2)', "A Total of ", total_sold, &
          " drinks were sold for a profit of $", total_profit
    print *, ""
    
    ! Display restock information
    ! Note: trim() removes trailing spaces from character variables
    print '(I0, A, A, A)', drink1%restock_amount(), " ", &
          trim(drink1%get_drink_name()), " needed to restock machine"
    print '(I0, A, A, A)', drink2%restock_amount(), " ", &
          trim(drink2%get_drink_name()), " needed to restock machine"
    print '(I0, A, A, A)', drink3%restock_amount(), " ", &
          trim(drink3%get_drink_name()), " needed to restock machine"
    print '(I0, A, A, A)', drink4%restock_amount(), " ", &
          trim(drink4%get_drink_name()), " needed to restock machine"

end program machine

⚡ Compilation and Execution Instructions

Step-by-Step: Two-File Compilation with gfortran

  1. Create drink_module.f90 with the Drink module code (shown above)
  2. Create machine.f90 with the main program code (shown above)
  3. Save both files in the same directory
  4. Open a terminal and navigate to that directory
  5. Compile the module first, then the main program:
    gfortran -c drink_module.f90
    gfortran -c machine.f90
    gfortran drink_module.o machine.o -o machine
    Or compile everything in one command (module must come first!):
    gfortran drink_module.f90 machine.f90 -o machine
  6. Run the program:
    ./machine
✅ The Key to Success: Always compile the module file before the program file. The compiler creates a .mod file (e.g., drink_module.mod) that the main program needs when it sees use drink_module.

The Difference Between Compilation Approaches

Approach Command When to Use
All at once gfortran module.f90 main.f90 -o prog Small projects, quick testing
Separate compilation gfortran -c module.f90
gfortran -c main.f90
gfortran *.o -o prog
Larger projects, when only some files change
Using Makefile make Professional projects, automated builds

Sample Output

Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Doctor Pepper Purchased.
Doctor Pepper Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.

 \\\\\\
 \ SOLD OUT \
 \\\\\\

A Total of 26 drinks were sold for a profit of $70.50

2 Coke needed to restock machine
4 Sprite needed to restock machine
8 Doctor Pepper needed to restock machine
0 V8 Splash needed to restock machine

📖 Understanding the Code

Module Organization

In Fortran, modules serve as the container for derived types and their associated procedures:

File Declaration Purpose
drink_module.f90 module drink_module Contains the Drink type and all its methods
drink_module.f90 public :: Drink Exports the type and constructor for external use
machine.f90 use drink_module Imports the Drink type into the main program

Type Components vs. Type-Bound Procedures

Aspect Type Components (Data) Type-Bound Procedures (Methods)
Declaration character(len=50) :: name procedure :: vend
Location Before contains After contains in type definition
Access self%name call self%vend()
Encapsulation Usually private Usually public

Encapsulation in Action

Notice that in the Drink type, all data members are declared private. This means they cannot be accessed directly from outside the module. Instead, users must use the public methods:

✅ This is encapsulation: The internal details (how money is tracked, how inventory works) are hidden, and users interact through well-defined methods. If we later change the internal implementation, users of the type won't need to modify their code.

❌ Common Mistakes and Solutions

Mistake 1: Compiling in the Wrong Order

❌ Wrong:

gfortran machine.f90 drink_module.f90 -o machine
! Error: Module 'drink_module' not found

✅ Correct:

gfortran drink_module.f90 machine.f90 -o machine
! Module compiled first, then program

Mistake 2: Using type() Instead of class() in Methods

❌ Wrong (prevents polymorphism):

subroutine vend(self)
    type(Drink), intent(inout) :: self    ! type() - not polymorphic
    ! ...
end subroutine

✅ Correct (supports polymorphism):

subroutine vend(self)
    class(Drink), intent(inout) :: self   ! class() - polymorphic
    ! ...
end subroutine

Mistake 3: Forgetting to Declare Procedures in Type Definition

❌ Wrong:

type :: Drink
    private
    character(len=50) :: name
    ! Missing 'contains' section!
end type Drink

subroutine vend(self)   ! This is NOT bound to the type!
    class(Drink), intent(inout) :: self
end subroutine

✅ Correct:

type :: Drink
    private
    character(len=50) :: name
contains
    procedure :: vend    ! Bind the procedure to the type
end type Drink

subroutine vend(self)   ! Now this IS bound to Drink
    class(Drink), intent(inout) :: self
end subroutine

Mistake 4: Calling Methods Without call for Subroutines

❌ Wrong:

drink1%vend()      ! Error: vend is a subroutine, needs 'call'

✅ Correct:

call drink1%vend()  ! Correct for subroutines

! But functions can be used directly:
x = drink1%profit() ! No 'call' needed for functions

Mistake 5: Using . Instead of % for Component Access

❌ Wrong (this is Java/C++/D syntax):

self.name = "Coke"       ! Wrong operator
call drink1.vend()       ! Wrong operator

✅ Correct (Fortran uses %):

self%name = "Coke"       ! Correct: use %
call drink1%vend()       ! Correct: use %

Mistake 6: Forgetting intent for the Self Argument

❌ Wrong:

subroutine vend(self)
    class(Drink) :: self    ! Missing intent - unclear behavior
end subroutine

✅ Correct:

subroutine vend(self)
    class(Drink), intent(inout) :: self    ! Clear: we read AND modify
end subroutine

real function profit(self)
    class(Drink), intent(in) :: self       ! Clear: we only read
end function

Mistake 7: Module Name Doesn't Match Use Statement

❌ Wrong (drink_module.f90):

module drink_class    ! Module name
    ! ...
end module drink_class

! In machine.f90:
use drink_module      ! ERROR: module drink_module not found

✅ Correct:

module drink_module   ! Module name matches 'use' statement
    ! ...
end module drink_module

! In machine.f90:
use drink_module      ! Correct: names match

✏️ Your Programming Exercise

Objective

Modify the vending machine program to replace the V8 Splash drink with a new beverage of your choice.

Assignment

The current program includes V8 Splash at $4.25 as the fourth drink. Your task is to:

  1. Replace V8 Splash: Change the drink name to something different (e.g., "Orange Juice", "Apple Cider", "Iced Tea", "Lemonade", etc.) and choose a new price (different from $4.25)
  2. Modify the vending operations: Change how many times each drink is purchased by modifying the number of vend() method calls for each drink
  3. Update the output display: Ensure all the drink names and restock amounts are printed correctly
  4. Test your program: Compile with gfortran and run it to ensure it works properly

Specific Changes to Make

In machine.f90, change the line:

drink4 = Drink("V8 Splash", 4.25)

to your new drink with a new price, for example:

drink4 = Drink("Orange Juice", 3.50)

Testing Your Solution

  1. Make your changes to machine.f90 (the module file doesn't need changes)
  2. Save the file
  3. Compile both files together:
    gfortran drink_module.f90 machine.f90 -o machine
  4. Run:
    ./machine
  5. Verify the output shows your new drink name and the correct profit calculations

🎯 Challenge

Before running your program, calculate what the total profit should be. Does your output match your prediction?

Hint: Total profit = (# of Cokes × $1.75) + (# of Sprites × $1.75) + (# of Dr Peppers × $1.75) + (# of your drink × your price)

🚀 Advanced Topics: Inheritance and Polymorphism

Modern Fortran supports inheritance and polymorphism, though we haven't used them in the vending machine example. Here's a brief overview:

Inheritance with extends

! Base type
type :: Beverage
    character(len=50) :: name
    real :: cost
contains
    procedure :: display
end type Beverage

! Extended type (inherits from Beverage)
type, extends(Beverage) :: CaffeinatedDrink
    real :: caffeine_mg    ! Additional component
contains
    procedure :: display => display_caffeinated  ! Override parent method
end type CaffeinatedDrink

Polymorphism with class and select type

subroutine process_any_beverage(b)
    class(Beverage), intent(in) :: b    ! Accepts Beverage or any child type
    
    ! Use select type for type-specific behavior
    select type(b)
    type is (Beverage)
        print *, "This is a regular beverage"
    type is (CaffeinatedDrink)
        print *, "This is caffeinated with", b%caffeine_mg, "mg"
    class default
        print *, "Unknown beverage type"
    end select
end subroutine
📝 Note: These advanced OOP features are powerful but add complexity. For many scientific computing tasks, the simpler derived type with type-bound procedures (as shown in the vending machine example) is sufficient and easier to understand.

⭐ Key Takeaways About OOP in Modern Fortran

🏆 Language Strengths Summary

Language Strengths Best For
Java Write once, run anywhere; Strong ecosystem; Great for large projects Enterprise applications, web backends, Android apps
C++ High performance; Maximum control; Close to hardware System software, games, performance-critical apps
D Combines best of Java and C++; Modern features; Fast compilation Systems programming, learning OOP, rapid development
Modern Fortran Excellent array handling; Mature numerical libraries; Legacy code compatibility Scientific computing, numerical simulation, HPC