Fortran from the ground up

Functions and subroutines

Naming a piece of work: functions that return a value, subroutines that report through their arguments, intent declarations that say who may change what, and arrays passed without their size.

Part 6 of 12

Intermediate18 min readBack to Courses

Two kinds of procedure

A function returns one value, and it returns it through its own name: integer function add_up(values) assigns to add_up, and a caller writes total = add_up(xs) exactly as if it were a built-in. A subroutine does work rather than produce a value, is called with call, and reports back through arguments declared intent(out).

Use a function when there is one answer and the name can be part of an expression. Use a subroutine when there are several answers, or none — a routine that reads data, prints a report, or updates two arrays in place has nothing to return.

A function that calls itself must be declared recursive, and the compiler needs to see that keyword in the definition. Without it the compiler rejects the self-call rather than guessing that you meant recursion. Recursion is natural for a tree or a factorial, but the declaration is not optional.

intent says who may change what

Every dummy argument gets an intent. intent(in) means the procedure may read it and the compiler will reject any attempt to assign to it. intent(out) means the procedure is expected to fill it in, and the caller's value is gone. intent(inout) means both.

This is more than documentation. An intent(in) argument cannot be silently overwritten halfway through a long routine, and a reader of the signature knows at a glance which names come back changed. It is the cheapest correctness check in the language.

Where the procedures live

The contains statement sits at the end of the program, and the procedures follow it, with end program after the last one. This puts them in the same file and the same scope, so they are visible to the program without any further declaration.

Internal procedures do see the program's variables, which is convenient and worth resisting: a procedure that reads a global is harder to test and harder to move. Passing what it needs as arguments costs a line and buys a procedure that works anywhere.

An array argument is declared with values(:), an assumed shape. The colon means "the size comes from whatever the caller passed", so the same function works on an array of three or three million, and size(values) gives the number the caller used.

A procedure defined outside the program, in its own file, has no explicit interface unless you write one. An interface block repeats the argument types and intents so the compiler can check the call. Modules avoid the repetition: the interface is generated from the procedure itself, which is one of the reasons chapter 10 puts procedures in a module.

A function and a subroutine, side by side

program demo
  implicit none
  integer :: xs(3), total
  real :: avg

  xs = [2, 4, 6]
  total = add_up(xs)
  call mean_of(total, size(xs), avg)
  print *, total, avg

contains
  integer function add_up(values)
    integer, intent(in) :: values(:)
    add_up = sum(values)
  end function add_up

  subroutine mean_of(total_in, count, result)
    integer, intent(in) :: total_in, count
    real, intent(out) :: result
    result = real(total_in) / real(count)
  end subroutine mean_of
end program demo

Arguments are shared, not copied

Fortran passes arguments by reference in effect: the procedure works on the caller’s storage, so an assignment to an intent(out) or intent(inout) argument is visible after the call returns. intent does not change that mechanism — it tells the compiler which direction the data may flow, so an assignment that would surprise the caller is rejected.

A function’s result is a variable with the function’s name, and it has to be assigned before the function returns. If a branch leaves it untouched, the value is undefined; it may be zero, or whatever was in that memory. Modern Fortran also lets you name the result — integer function span(values) result(width) — which keeps the body readable and stops the function name from being both the routine and a variable.

The value attribute passes a copy rather than a reference, which is useful when a procedure should not be able to change the caller’s variable even by accident. It is not the default, and adding it changes the interface, so the declaration has to match on both sides.

Three procedure mistakes to know: calling a function as if it were a subroutine, or forgetting call before a subroutine, produces an error that reads like a type error; assigning to an intent(in) argument is rejected for the same reason; and a function that assigns its result on only one branch returns undefined data on the other. The last one compiles cleanly, which is what makes it dangerous.

Try these next

  • Add an intent(inout) argument that counts how many values are above the mean.
  • Write a function that returns the larger of two integers, and call it inside a print.
  • Change swap to swap three values and check the result.

One value out, one value back

Run it, then change the array to [3, 5, 8, 10, 12] and let size decide the count. Then make add_up take a second argument and add it to the total.

Output
Click "Run" to execute your code

Swap two values, measure a range

Run it and note that `swap` changes the caller’s variables. Then add a value to the array, and change `span` to return the mean instead, keeping the `result` name honest.

Output
Click "Run" to execute your code

If a procedure needs more than a handful of arguments, or its signature keeps growing as the program does, that is usually a record asking to exist. Chapter 11 gives one student three fields and a name.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does intent(in) promise about an argument?