Getting past the installation hurdles

Most people who pick up Fortran 90 or 95 are coming from C, Python, or maybe they inherited a legacy codebase they didn't ask for. The language itself is not the problem. Getting a working compiler on your machine is where things fall apart. I use gfortran on Linux, and the command to compile a simple program is gfortran -O2 -o mycode mycode.f90. The -O2 flag gives you sensible optimization without making debugging impossible. If you skip it, your code will run at roughly one-tenth the speed it should on anything but trivial loops. That matters more than most beginners expect. On Windows, MinGW-w64 is the straightforward path. Install it through MSYS2, add the bin directory to your PATH, and test with gfortran --version. If it returns nothing, you forgot the PATH step and you will waste forty minutes debugging something that isn't broken. On macOS, Homebrew gets you there with brew install gcc. It installs gfortran alongside the GCC toolchain. Again, test immediately.

Fortran 90 95 For Scientists And Engineers

The reason Fortran survived this long isn't sentimentality. It's array handling. Modern languages have caught up somewhat, but Fortran still does contiguous array operations faster out of the box than most other compiled languages without extra libraries. When your code is doing dense linear algebra across three-dimensional grids, every temporary allocation you avoid is measurable runtime you get back. A Fortran 90 program that computes a matrix multiplication across a 500x500 grid using explicit DO loops and individual element assignments will run noticeably slower than the same logic expressed with array syntax. Not dramatically in some cases, but enough that if you are running this inside an iterative solver with thousands of steps, you will notice it by the end of the day.

The syntax looks almost like math because that was the design intent. x = a * b + c works exactly as written when x, a, b, and c are arrays of the same shape. No loops required. The compiler handles the iteration internally and can vectorize more aggressively because it knows the memory layout is contiguous by default.

Here is a realistic scenario from my own work. I was porting a 1990s thermal diffusion code from Fortran 77 to modern Fortran 95. The original used implicit double precision through the naming convention: variables starting with I through N were single precision integers by default, everything else was double precision. Simple enough to follow for decades. I rewrote the core solver using assumed-shape arrays and explicit interfaces, added use statements, and replaced the legacy COMMON blocks with modules. The new version compiled clean and ran faster. Then I noticed the results were wrong by about 0.3 percent on edge cases. The issue traced back to a single parameter declaration that had changed meaning between F77 and F90 semantics. In F77, a parameter passed to a subroutine through a COMMON block retained its precision context from the calling program. In my rewritten modular version, the interface block did not explicitly declare the argument as double precision, so it fell back to default real. A twenty-year-old precision bug resurfaced because the compiler was following the new rules correctly. The fix was adding an explicit interface in a module and declaring every argument with kind parameters. I used kind = 8 or, better yet, defined a named kind parameter like wp = selected_real_kind(15, 307) at the top of each module so the code would survive if someone changed compilers.

That experience taught me to never trust implicit typing again, even in newly written code. The line implicit none at the top of every program unit is not a suggestion. It is the single most important line you will type. Without it, a typo in a variable name becomes a silent source of wrong answers that will take you two or three days to find if you are unlucky.

Modules and the way they change everything

Fortran 77 had no modules. It had include files, COMMON blocks, and hope. Fortran 90 introduced modules, and that single feature is why the language is still viable today. A module is a container for variables, subroutines, functions, and type definitions that other programs can access with a use statement. The compiler enforces type checking across module boundaries. If you pass an integer where the subroutine expects a real, it catches it. In F77, the compiler would happily pass garbage and the program would either crash or produce silently incorrect results depending on your luck. Module design is mostly a matter of taste after you get past the basics. I put every derived type in its own module if it gets used in more than one file. Shared parameters like pi or physical constants go in a constants module. Solver routines go in a solver module. File I/O helpers in an io module. The tricky part is recursion and module procedures. If a subroutine needs to call itself, or if two subroutines need to call each other, you have to declare them as recursive or use separate interface blocks. The compiler will reject mutual recursion without explicit declarations. This catches people frequently when they try to convert old F77 code that relied on the old rule that any subroutine could call any other subroutine in the same file. ``` module physics_constants implicit none integer, parameter :: wp = selected_real_kind(15, 307) real(wp), parameter :: pi = 3.141592653589793238_wp real(wp), parameter :: grav = 9.80665_wp end module physics_constants ``` That is a module you will recognize in almost every codebase I have touched. It appears in structural mechanics codes, fluid dynamics packages, and heat transfer simulations. The wp kind parameter is what keeps your results stable when you switch between machines. Without it, a double precision calculation on one compiler becomes single precision on another and your convergence criteria break.

The parts beginners get wrong repeatedly

Array indexing starts at one by default. You can change it with the lower bound syntax, but ninety-five percent of the time you should leave it at one. Changing it causes more confusion than it solves unless you are interfacing with C code that expects zero-based indexing. String handling is simpler than C but more limiting. Fixed-length character arrays mean strings are always padded with spaces to their declared length. Comparing two strings with == works, but comparing a variable-length string to a literal requires padding or using len_trim to strip trailing spaces. I lost an afternoon to this once when comparing output filenames. The comparison failed because one string had trailing spaces from the file system and the other did not. len_trim solved it in thirty seconds. File I/O in Fortran is verbose by modern standards but extremely reliable. Open a unit number, read or write, close when done. The format statements look archaic to someone used to printf, but they are precise. If you need formatted output for publication or debugging, spend ten minutes learning the format specifiers instead of fighting with unformatted output. ``` open(unit=10, file='output.dat', status='replace') write(10, '(A, F12.6, E15.8)') 'Displacement:', disp, stress close(10) ``` That writes a label followed by a fixed-point number and a scientific notation number to a file. It will do the same thing identically on every compiler and every operating system. That consistency is worth more than you think when you are collaborating with a group spread across different institutions.

Dynamic memory and allocation

This is where Fortran 90 departs significantly from Fortran 77 and enters territory that C and C++ programmers expect. The allocate and deallocate statements let you create arrays whose size is not known at compile time. allocate(matrix(n, n), stat=errcode) checks the return code. If errcode is nonzero, the allocation failed and your program should handle it gracefully instead of continuing and crashing later. I skip this check in quick prototypes but never in production code. The cost of the check is negligible. The cost of a segfault in a parallel simulation is hours of wasted computation time. Deallocation is equally straightforward. deallocate(matrix). If you try to deallocate something already freed, the compiler typically warns you or the runtime detects it. Always set the pointer to null after deallocation to avoid dangling references. ``` real(wp), allocatable :: grid(:,:) allocate(grid(nx, ny)) ! ... use it ... deallocate(grid) grid => null() ```

Assumed-shape arrays are another Fortran 90 feature that changes how you write subroutines. Instead of passing explicit dimensions to every routine, you declare the dummy argument as a(:,:) and let the compiler figure out the bounds at runtime. The caller passes the actual array and the subroutine receives it with the correct shape automatically. This eliminates a whole class of index-out-of-bounds bugs that used to require manual dimension passing.

Get the Full Details

FORTRAN 90/95 for Scientists and Engineers by Stephen J. Chapman (1997, Trade Paperback) for ...
FORTRAN 90/95 for Scientists and Engineers by Stephen J. Chapman (1997, Trade Paperback) for ...
But assumed-shape arrays have a restriction: they require an explicit interface. The subroutine that receives the assumed-shape array must be in a module or have an explicit interface block. You cannot use them with old-style external subroutines that rely on implicit interfaces. This is not a limitation of the language. It is a safety feature. The compiler needs to know the array layout to generate correct code, and it cannot assume anything about an externally declared subroutine.

What Fortran 90 95 still does poorly

Parallelism is the main gap. Fortran 90 has no built-in threading model. If you want parallel execution, you use OpenMP directives inside your Fortran code. gfortran supports these with the -fopenmp flag. The syntax is straightforward and it works well for loop-level parallelism, which covers most scientific computing workloads. But if you are building something that requires fine-grained concurrency, task parallelism, or GPU acceleration, Fortran 95 has no answer. You are stuck with OpenMP or switching to Fortran 2008/2018 coarray Fortran, which most people have never used and almost no documentation covers adequately. The ecosystem is small. Package managers do not exist. There is no equivalent to pip or npm. If you need a numerical library, you are either downloading source code and compiling it yourself, hoping someone already compiled a version for your platform, or writing the routine yourself. LAPACK, BLAS, and netCDF are the exceptions that prove the rule. They exist as precompiled libraries on most systems, but finding and linking them can take more time than writing a basic implementation from scratch. Debugging tools are limited. gdb works. Print statements work. But modern IDE features like integrated debuggers with variable inspection, breakpoint management, and call stack navigation are rare. Most Fortran developers I know use vim or emacs with a separate terminal for compilation and another for gdb. It is functional but archaic compared to what a C++ or Python developer has available.

Memory management requires discipline. Unlike garbage-collected languages, Fortran gives you direct control over allocation and deallocation. That control is powerful but unforgiving. A single missing deallocate in a subroutine called inside a loop will leak memory proportional to the loop iteration count. In a loop that runs ten million times, that is ten million allocations sitting in memory until the program exits. On a cluster node with limited RAM, that is enough to trigger the OOM killer and terminate your simulation mid-run with no warning.

A practical workflow that actually works

Write your code in modules. Keep the main program thin. Put all the logic in submodules or modules. Compile with -O2 -g for development, strip the debug symbols later if you need to optimize for size. Use a Makefile. Even a simple one saves you from typing the same compile command fifty times. A basic Makefile for a Fortran project with multiple source files and modules takes about fifteen minutes to set up and then saves you five to ten minutes per compile cycle going forward. Over a project that lasts months, that is hours of time recovered. Test with small inputs first. Run your code on a 10x10 grid before you try it on a 1000x1000 grid. If it crashes on the small case, you will find the bug in seconds instead of waiting for the large case to fail at an arbitrary point in the simulation. If your code involves linear algebra, link against OpenBLAS or Intel MKL. The difference in performance between a naive implementation and a linked BLAS library on a matrix multiply of moderate size is roughly an order of magnitude. On repeated operations inside an iterative solver, that translates from hours of wall time to minutes.

The language feels archaic because it is old. But old does not mean obsolete in scientific computing. The compilers are mature. The optimizations are aggressive. The standards are stable and well-defined. If your work involves numerical computation, arrays, and mathematical modeling, Fortran 90 and 95 remain perfectly adequate tools. They are not flashy. They will not win any design awards. They do the work.

If you want to start, install gfortran, write a program that reads a data file, processes the numbers using array operations, and writes the results back out. That is the entire workflow. Everything else is detail.