Basics
Hello, World
cat("Hello, World!\n") program hello
implicit none
print *, "Hello, World!"
end program hello A Fortran program needs a
program name ... end program name wrapper, and implicit none is written by convention at the top of every one — R has neither ceremony, since a script is just a sequence of top-level expressions. print *, ... plays cat, with the leading * meaning “use default formatting.”<- becomes = (after a declaration)
sample_size <- 30
mean_height <- 170.5
cat(sample_size, mean_height, "\n") program assignment_demo
implicit none
integer :: sample_size
real :: mean_height
sample_size = 30
mean_height = 170.5
print *, sample_size, mean_height
end program assignment_demo Every variable is declared with its type before it can be assigned —
implicit none is what makes an undeclared variable a compile error instead of Fortran’s historical (and dangerous) default of guessing a type from the variable’s first letter. Once declared, assignment is a plain =, with no arrow.The Comfort Zone: Vectors & 1-Based
Arrays are STILL 1-based
heights <- c(160, 172, 181)
print(heights[1]) # the first element
print(heights[length(heights)]) # the last program one_based_demo
implicit none
real :: heights(3)
heights = [160.0, 172.0, 181.0]
print *, heights(1) ! the first element — no zero-shift
print *, heights(size(heights)) ! the last
end program one_based_demo A genuinely rare comfort: like R (and unlike almost every other migration target on this site), Fortran counts array elements from one by default —
heights(1) means what it has always meant. Note the parentheses instead of brackets, and size() playing length().Whole-array arithmetic needs no loop
doses <- c(1, 2, 3)
print(doses * 2) # vectorized, invisibly
print(sqrt(doses))
print(doses + c(10, 20, 30)) program whole_array_demo
implicit none
real :: doses(3)
doses = [1.0, 2.0, 3.0]
print *, doses * 2.0 ! vectorized natively, just like R
print *, sqrt(doses)
print *, doses + [10.0, 20.0, 30.0]
end program whole_array_demo The rarest kinship on this page: Fortran, like R, vectorizes elementwise arithmetic and intrinsic functions over whole arrays with no explicit loop — a feature most languages compared against R (Go, Java, C) have no answer for at all.
doses * 2.0 and sqrt(doses) read almost identically to their R counterparts.Recycling → Strict Conformance
🚨 Recycling is a hard error here
print(c(1, 2, 3, 4) + c(10, 20)) # silently recycled: 11 22 13 24
print(c(1, 2, 3) + c(10, 20)) # recycled with a warning: 11 22 13 program conformance_demo
implicit none
real :: four_values(4), two_values(2)
four_values = [1.0, 2.0, 3.0, 4.0]
two_values = [10.0, 20.0]
! four_values + two_values does not COMPILE — shapes must conform exactly.
! R's silent (or warned) recycling has no equivalent: mismatched shapes
! are caught before the program runs, whenever the sizes are known statically.
print *, four_values + [10.0, 20.0, 10.0, 20.0] ! write the recycle out by hand
end program conformance_demo This is the sharpest edge of an otherwise gentle migration: R’s array kinship comes with a real silent-bug risk — mismatched vector lengths recycle the shorter one, sometimes with a warning, sometimes (when lengths divide evenly) with none at all. Fortran refuses the question outright: shapes must conform exactly, and a mismatch is a compile-time error whenever sizes are known at compile time, or a runtime failure otherwise. Recycling has to be spelled out by hand if it is genuinely intended.
Dynamic → Static & Declared
Every variable has one fixed, declared type
measurement <- 5
print(class(measurement))
measurement <- "five" # R happily lets a variable change type
print(class(measurement)) program declared_types_demo
implicit none
integer :: measurement
measurement = 5
print *, measurement
! measurement = "five" does not compile —
! a variable's type is fixed forever at its declaration
end program declared_types_demo R lets any name hold any type at any time; Fortran fixes a variable’s type at its declaration —
integer, real, character, logical — and no later assignment may change it. This, combined with the compile step, is what catches an entire category of R runtime surprises (a function receiving the wrong type deep into a long-running job) before the program starts at all.Implicit coercion is gone
print(1 + TRUE) # TRUE coerces to 1 — silently
print(paste("count:", 5)) # 5 coerces to "5" — silently program conversion_demo
implicit none
integer :: count
character(len=20) :: message
count = 5
write(message, '(A, I0)') "count: ", count ! explicit formatted conversion
print *, trim(message)
! 1 + .true. does not compile — no automatic logical-to-integer coercion
end program conversion_demo R quietly converts between types wherever an operation needs it; Fortran converts nothing without being told — mixing a
logical into arithmetic is a compile error, and building a string from a number goes through an explicit formatted write statement (the I0 edit descriptor means “integer, minimum width”). More ceremony, but the conversion is always visible in the code.Array Intrinsics
sum/mean/max → intrinsic array functions
values <- c(2, 4, 4, 4, 5, 5, 7, 9)
print(sum(values))
print(mean(values))
print(max(values))
print(min(values)) program reductions_demo
implicit none
real :: values(8)
values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]
print *, sum(values)
print *, sum(values) / size(values) ! no built-in "mean" — assembled by hand
print *, maxval(values)
print *, minval(values)
end program reductions_demo sum, maxval, and minval are built-in array reductions, direct counterparts to R’s. The one gap: there is no built-in mean — Fortran has no general statistics library at all, base or otherwise, so even the average is sum(values) / size(values) written out.Slicing uses a colon, still inclusive
readings <- c(10, 20, 30, 40, 50)
print(readings[2:4]) # positions 2,3,4 — both ends included
print(readings[c(1, 3, 5)]) # arbitrary positions program slicing_demo
implicit none
real :: readings(5)
readings = [10.0, 20.0, 30.0, 40.0, 50.0]
print *, readings(2:4) ! positions 2,3,4 — both ends included, like R
print *, readings([1, 3, 5]) ! arbitrary positions via an index array
end program slicing_demo Slicing is another rare point of agreement:
readings(2:4) reads exactly like R’s readings[2:4], inclusive on both ends. Arbitrary-position selection also works the same way, indexing with an integer array instead of a single range.Logical masks → the WHERE construct
doses <- c(10, 20, 30)
print(doses > 15) # a logical vector
print(ifelse(doses > 15, doses, 0)) # vectorized conditional program where_demo
implicit none
real :: doses(3), result(3)
doses = [10.0, 20.0, 30.0]
where (doses > 15.0)
result = doses
elsewhere
result = 0.0
end where
print *, result
end program where_demo R’s
ifelse — a vectorized conditional applied elementwise — becomes the WHERE/ELSEWHERE construct: a mask expression, a branch for where it holds, a branch for where it does not, applied across the whole array with no explicit loop. It reads almost like an if/else block that happens to operate elementwise.Vectorization → ELEMENTAL
Your own functions need ELEMENTAL to vectorize
celsius_to_fahrenheit <- function(celsius) celsius * 9 / 5 + 32
temperatures <- c(0, 20, 37)
print(celsius_to_fahrenheit(temperatures)) # vectorizes for free module conversions
implicit none
contains
elemental function celsius_to_fahrenheit(celsius) result(fahrenheit)
real, intent(in) :: celsius
real :: fahrenheit
fahrenheit = celsius * 9.0 / 5.0 + 32.0
end function celsius_to_fahrenheit
end module conversions
program elemental_demo
use conversions
implicit none
real :: temperatures(3)
temperatures = [0.0, 20.0, 37.0]
print *, celsius_to_fahrenheit(temperatures) ! ELEMENTAL makes this legal
end program elemental_demo R’s function vectorizes for free because every operation inside it happens to vectorize on its own — write scalar logic and the free ride ends silently. Fortran is explicit about the same idea: mark a function
elemental and it may be called on a scalar OR an array of any shape, applied elementwise — the intent is declared, not assumed. Without the keyword, calling a scalar function on an array is a compile error.Control Flow
if is a statement, not an expression
sample_size <- 12
label <- if (sample_size >= 30) "large" else "small"
print(label) program if_demo
implicit none
integer :: sample_size
character(len=10) :: label
sample_size = 12
if (sample_size >= 30) then
label = "large"
else
label = "small"
end if
print *, trim(label)
end program if_demo Unlike R, Fortran’s
if has no value of its own — it cannot be assigned directly. The R idiom of “assign the result of a condition” becomes declare-then-branch-then-set, and every character-string variable must declare its maximum length (len=10) up front, since strings are fixed-width, padded fields rather than R’s flexible ones.for → do, still inclusive ranges
for (trial in 1:5) {
cat("trial", trial, "\n")
}
print(seq(0, 10, by = 2)) program do_loop_demo
implicit none
integer :: trial
do trial = 1, 5
print *, "trial", trial
end do
do trial = 0, 10, 2 ! start, stop, STEP — same shape as seq(by =)
print *, trial
end do
end program do_loop_demo do trial = 1, 5 plays for (trial in 1:5) almost verbatim — inclusive on both ends, exactly like R’s ranges. A third argument is the step, matching the argument order of seq(0, 10, by = 2) rather than requiring the middle-position trap some other target languages introduce.Functions & Subroutines
Functions declare every type, including the result
standardize <- function(values) {
(values - mean(values)) / sd(values)
}
print(standardize(c(10, 20, 30))) module statistics
implicit none
contains
function standardize(values) result(standardized)
real, intent(in) :: values(:)
real :: standardized(size(values))
real :: average, deviation
average = sum(values) / size(values)
deviation = sqrt(sum((values - average)**2) / (size(values) - 1))
standardized = (values - average) / deviation
end function standardize
end module statistics
program function_demo
use statistics
implicit none
print *, standardize([10.0, 20.0, 30.0])
end program function_demo Every parameter’s type, the result’s type, and — for an array parameter — whether it may be modified (
intent(in) declares read-only) are all stated up front. values(:) is an assumed-shape array: the size is not fixed at compile time, but is known and checked at the call site. Functions live inside a module, which also plays the role of R’s namespace-free top-level scripts.Subroutines: functions that modify their arguments
add_measurement <- function(values, new_value) {
c(values, new_value) # R never mutates the caller's data
}
readings <- c(1, 2)
extended <- add_measurement(readings, 3)
print(readings) # untouched
print(extended) module updater
implicit none
contains
subroutine add_reading(total, new_value)
real, intent(inout) :: total
real, intent(in) :: new_value
total = total + new_value ! genuinely modifies the CALLER's variable
end subroutine add_reading
end module updater
program subroutine_demo
use updater
implicit none
real :: running_total
running_total = 3.0
call add_reading(running_total, 4.0)
print *, running_total ! 7.0 — changed by the subroutine
end program subroutine_demo R never lets a function mutate the caller’s data — arguments are effectively copies. Fortran has an explicit tool for the opposite: a
subroutine (invoked with call, returning nothing) whose intent(inout) parameters genuinely modify the variable the caller passed in. The intent annotation is what makes this safe and legible — in, out, and inout declare the contract instead of leaving it to convention.NA Has No Equivalent
🚨 There is no NA, no exceptions, no missing marker
readings <- c(1, 2, NA, 4)
print(is.na(readings))
print(mean(readings, na.rm = TRUE)) program no_na_demo
implicit none
real :: readings(4)
real, parameter :: MISSING = -999.0 ! a sentinel value, agreed by convention
real :: total
integer :: count, index
readings = [1.0, 2.0, MISSING, 4.0]
total = 0.0
count = 0
do index = 1, size(readings)
if (readings(index) /= MISSING) then
total = total + readings(index)
count = count + 1
end if
end do
print *, total / count
end program no_na_demo This is the sharpest conceptual gap on the page: Fortran has no built-in missing-value marker at all — no
NA, no NaN-aware statistics, nothing checked by the compiler. Real Fortran code agrees on a sentinel value (often a suspicious constant like -999) and every computation must remember to check for it by hand, exactly the discipline na.rm = TRUE automates away in R.Named Lists → Derived Types
list(name =, age =) → a derived type
patient <- list(name = "Ada", age = 36)
print(patient$name)
patient$age <- 37
print(patient$age) module patient_module
implicit none
type :: patient_type
character(len=20) :: name
integer :: age
end type patient_type
contains
end module patient_module
program derived_type_demo
use patient_module
implicit none
type(patient_type) :: ada
ada%name = "Ada"
ada%age = 36
print *, trim(ada%name)
ada%age = 37
print *, ada%age
end program derived_type_demo R’s tagged named list becomes a declared
type — fields typed and fixed up front, accessed with % instead of $. Every field’s type (and, for strings, its maximum length) is part of the declaration, so misspelling a field name is caught at compile time instead of silently returning NULL.Strings
Strings are fixed-width, padded fields
name <- "Ada"
print(name)
print(nchar(name))
longer_name <- "Alexandria"
print(nchar(longer_name)) # R strings just grow program fixed_width_demo
implicit none
character(len=10) :: name
name = "Ada"
print *, name ! padded with trailing spaces to length 10
print *, len(name) ! ALWAYS 10 — the declared length
print *, len_trim(name) ! 3 — the length ignoring trailing padding
end program fixed_width_demo R strings are as long as their content and nothing more; a Fortran
character variable has a FIXED declared length, padded with trailing spaces to fill it. len() always reports the declared width, not the “real” content length — len_trim() is the one that behaves like R’s nchar(). Assigning a longer string than the declared length silently truncates it.paste & sprintf → formatted write
name <- "Ada"
completion <- 66.67
cat(paste("Subject", name, "is", completion, "percent done"), "\n")
cat(sprintf("%s: %.1f%% done\n", name, completion)) program formatting_demo
implicit none
character(len=10) :: name
real :: completion
character(len=50) :: message
name = "Ada"
completion = 66.67
write(message, '(A, A, A, F5.1, A)') "Subject ", trim(name), " is ", completion, " percent done"
print *, trim(message)
write(message, '(A, A, F5.1, A)') trim(name), ": ", completion, "% done"
print *, trim(message)
end program formatting_demo A formatted
write statement into a string variable replaces both paste and sprintf at once: the format string in parentheses (F5.1 means “fixed-point, width 5, one decimal place”) plays the role of R’s format specifiers, but ahead of the values rather than interleaved with them. trim() strips the padding before printing, undoing the fixed-width story above.Error Handling
tryCatch → IOSTAT and manual checks
result <- tryCatch({
stop("model failed to converge")
}, error = function(condition) {
cat("caught:", conditionMessage(condition), "\n")
NA
})
print(result) program error_demo
implicit none
real :: denominator, result
logical :: converged
denominator = 0.0
converged = denominator /= 0.0
if (.not. converged) then
print *, "caught: model failed to converge"
result = 0.0
else
result = 1.0 / denominator
end if
print *, result
end program error_demo There is no exception mechanism to catch: idiomatic Fortran checks conditions before they become failures (as here), or checks an
IOSTAT integer after an I/O operation — zero means success, nonzero identifies the failure. tryCatch’s job is spread across explicit conditionals at each risky point rather than wrapped once around a block.stop() → STOP, with an exit code
check_positive <- function(value) {
if (value < 0) stop("value must be non-negative")
sqrt(value)
}
print(check_positive(4)) program stop_demo
implicit none
real :: value
value = 4.0
if (value < 0.0) then
print *, "value must be non-negative"
stop 1
end if
print *, sqrt(value)
end program stop_demo The closest analog to
stop() is Fortran’s own STOP statement, which halts the program immediately and can carry an integer exit code the operating system sees — useful for scripts that need to detect failure downstream. Unlike R’s condition system, there is no handler to catch it: STOP ends the program, full stop.Why Bother: Performance
Why bother: the reason this pairing exists
# R calls out to compiled code for the heavy lifting all the time:
# base R's linear algebra runs on LAPACK/BLAS (often Fortran underneath)
# .Fortran() and .C() exist specifically to call compiled routines
# Rcpp/cppFunction are the modern equivalent, usually reaching for C++ ! Fortran IS the compiled code R and Python are calling out to:
! - LAPACK and BLAS, the numerical linear-algebra libraries underneath
! R's matrix operations, NumPy, and MATLAB, are written in Fortran
! - Decades of tuned, battle-tested numerical routines (ODE solvers,
! FFTs, optimization) exist ONLY in Fortran, with no modern port
! - Climate models, computational fluid dynamics, and weather forecasting
! still run on Fortran code first written in the 1970s-80s This is the honest answer to “why would an R user ever need Fortran”: R already depends on it, one layer down. The BLAS/LAPACK routines behind R’s matrix multiplication and linear algebra are themselves Fortran, and huge bodies of tuned scientific code (climate models, fluid dynamics solvers) exist only in Fortran, with no modern rewrite planned. Writing the hot inner loop of a slow R function in Fortran — called via
.Fortran() or a modern FFI — is a well-worn path when profiling shows R itself is the bottleneck. Both cells are illustrative, shown display-only.