Fortran

Guide To Learn

Let’s jump straight into it and write our first custom function. We’ll take our cold front program from listing 3.2 and rewrite it to delegate the temperature calculation to an external function. This will allow us to easily compute the solution for a series of different input values. Specifically, we’ll iterate over several values of time interval dt, ranging from 6 to 48 hours at 6-hourly increments, while holding the other input parameters constant. This will tell us how the temperature in Miami drops over time as the cold front moves through:

Temperature after    6.00000000      hours is    22.5000000      degrees.
Temperature after    12.0000000      hours is    21.0000000      degrees.
Temperature after    18.0000000      hours is    19.5000000      degrees.
Temperature after    24.0000000      hours is    18.0000000      degrees.
Temperature after    30.0000000      hours is    16.5000000      degrees.
Temperature after    36.0000000      hours is    15.0000000      degrees.
Temperature after    42.0000000      hours is    13.5000000      degrees.
Temperature after    48.0000000      hours is    12.0000000      degrees.

I’ll first go over the complete program, as shown in the following listing, and then go more in-depth into the function definition syntax and its rules.

Listing 3.3 Calculating the cold front temperature using an external function

program cold_front
 
  implicit none                                    ❶
  integer :: n
  real :: nhours                                   ❷
 
  do n = 6, 48, 6                                  ❸
    nhours = real(n)                               ❹
    print *, 'Temperature after ', &               ❺
      nhours, ' hours is ', &
      cold_front_temperature(12., 24., 20., 960., nhours), ' degrees.'
  end do
 
contains                                           ❻
 
  real function cold_front_temperature( &          ❼
    temp1, temp2, c, dx, dt) result(res)           ❼
    real, intent(in) :: temp1, temp2, c, dx, dt    ❽
    res = temp2 - c * (temp2 - temp1) / dx * dt    ❾
  end function cold_front_temperature              ❿
 
end program cold_front

❶ Explicit declarations apply to the whole program scope, including the contained function.

❷ Time interval that we pass to the function as a real number

❸ Loops from 6 to 48 hours with a 6-hourly increment

❹ Converts the integer counter to a real number of hours

❺ Prints the function result

❻ Separates the program code and the function definition

❼ Specifies function type, name, and arguments

❽ Inputs arguments

❾ Computes the function result

❿ Closes the function scope

In this program, we loop over several values of time interval in hours. Inside the loop, we invoke the cold_front_temperature function, using four input arguments that have fixed values, with the fifth input argument being the time interval that varies. The function is invoked on the right side of the print statement, so the result is broadcast directly to the screen. Finally, the function is defined in a special section at the end of the program, marked by the contains statement. In summary, we have three new language elements in this program: how the function is defined, where it’s defined, and how it’s called from the main program. I’ll explain how each element works, one at a time.

Defining a function

For brevity, I’ll go over the function definition by using a simpler example, such as calculating the sum of two integers, as shown in the following listing.

Listing 3.4 A function that returns a sum of two integers

function sum(a, b)             ❶
  integer, intent(in) :: a     ❷
  integer, intent(in) :: b     ❷
  integer :: sum               ❸
  sum = a + b                  ❹
end function sum               ❺

❶ Specifies the name of the function and input arguments

❷ Declares input arguments and specifies intent

❸ Declares the function result

❹ Computes the function result

❺ Closes the function scope

Let’s break this down. We open the function body with a function statement and specify its name. This is analogous to defining a main program, except for one important difference. With a function, we also list all the arguments in parentheses, immediately following the function name. Like the program statement, the function statement must have a matching end function statement.

Next, we declare the arguments much like we did for the main program, except that here we also have an additional attributeintent(in). This attribute indicates to the compiler–and to the programmer reading the code–what the intent of the argument is. Here, intent(in) means that the variables a and b are to be provided by the calling program or procedure, and their values won’t change inside this function.

Like when declaring variables in the main program, you can specify input arguments of the same data type on the same line. Furthermore, you can specify the data type of the function result immediately in front of the word function, as shown in the following listing. Notice that we use both of these features in the cold front program as well.

Listing 3.5 Specifying the data type of the function result in the function statement

integer function sum(a, b)         ❶
  integer, intent(in) :: a, b      ❷
  sum = a + b
end function sum

❶ We specified the data type of the function result here.

❷ You can put multiple arguments of the same type and intent on the same line.

It’s also possible, for convenience, to specify a different name for the function result, other than the name of the function, using the result attribute, as the following listing demonstrates.

Listing 3.6 Specifying the function result as different from the function name

integer function sum(a, b) result(res)     ❶
  integer, intent(in) :: a, b
  res = a + b                              ❷
end function sum

❶ Specifies a different name for the function result

❷ The function result is now res.

The advantage to using the result attribute may not be obvious from this example because the name of the function (sum) is already quite short, but it comes in handy for longer function names. Note that Fortran comes with an intrinsic (built-in) function sum that returns the sum of all elements in an input array. Because of this, some compilers may warn you if you compile this function, and that’s okay. I used the same name for the example in this section only for convenience.

In listing 3.6, the function returns a single scalar as a result. In general, functions can return a scalar, an array, or a more complex data structure, but it’s always a single entity.

You may be wondering why I omitted the implicit none statement in the declaration section in listing 3.6. In this case, I did it for brevity, and it wouldn’t do much here because we use only the input arguments and no other variables in the calculation of the result. However, I omitted it in the cold_front_temperature function definition (listing 3.3) as well because the function is defined in the scope of the main program, and implicit none then propagates into all procedures defined therein.

As functions always return a single result and can only be invoked from expressions, they’re best suited for minimal bits of functionality. A function that does more than one thing is harder to understand. What happens when you start chaining multiple function calls in a single expression, as I’ll show you in the next subsection? Well, you should be able to tell what a function does simply based on its name. You can see that it’d be difficult to do so if the function was doing many things. When defining a function, consider the result and the smallest set of inputs required to calculate it. If your function does only that and no more, congratulations–you’re on a good track toward clean and maintainable code.

Tip A function should do one and only one thing.

Invoking the function

A Fortran function is invoked in the same way as in C, Python, or JavaScript. To call the function sum defined in listing 3.6 and print the result to the screen, you’d simply say

print *, sum(3, 5)

You can also use a function in expressions or output statements, or pass the function result as an argument to another function. All of the statements in the following listing are valid.

Listing 3.7 Examples of invoking an external function

six = 2 * sum(1, 2)                     ❶
print *, '2 plus 4 equals', sum(2, 4)   ❷
six = sum(sum(1, 2), 3)                 ❸

❶ Invokes a function in an arithmetic expression

❷ Invokes a function as part of an output statement

❸ Passes a function result as an argument to another function call

You can thus chain functions into more complex expressions, which you can use to write concise and elegant code if used with moderation. In the cold front program in listing 3.3, we invoked the cold_front_temperature function directly on the print statement.

Actual and dummy arguments

The Fortran Standard uses specific terminology to differentiate between arguments defined inside the procedure and those that are passed in the call. Actual arguments are the ones that you pass when invoking the procedure. Dummy arguments are the ones declared in the procedure definition. In the previous example of sum(3, 5), the integer literals 3 and 5 are the actual arguments, and integers a and b in the function definition are dummy arguments. Being aware of this distinction and terminology will prove to be useful later when we tease out more advanced procedure concepts, as well as if you read Fortran Standard documents or other Fortran books.

Specifying the intent of the arguments

If you look closely at the declaration statements for arguments a and b in listings 3.4 to 3.6, you’ll notice the intent attribute–something that we haven’t used in our programs so far. This attribute informs the compiler about the semantic purpose of the arguments, and it can take three different values:

  • intent(in)–The argument is an input argument. It will be provided to the procedure by the calling program or procedure, and its value won’t change inside the procedure.
  • intent(out)–The argument is an output argument. Its value is assigned inside the procedure and returned back to the calling program or procedure.
  • intent(in out)–The argument is an input and output argument. It’s provided to the procedure by the calling program or procedure, its value can be modified inside the procedure, and its value is returned to the calling program or procedure.

Like implicit none, specifying the intent is optional but strongly recommended. First, an intent specification clearly indicates to the programmer (especially if they’re not the original author of the code) what the role of each argument is, which helps with both understanding and debugging the code. Second, specifying intent can help the compiler raise errors if the actual code is in violation of the intent specification. For example, if you declare an argument as an intent(in) variable, the compiler won’t let you use it on the left side of an assignment. Being explicit regarding the intent of all arguments will help you write transparent and correct programs.

Tip Always specify intent for all arguments.

I mentioned earlier that functions are best suited for calculations that don’t cause side effects, whereas subroutines are more appropriate when we need to modify variables in-place. These are best practices, rather than hard rules: Fortran allows intent(in out) and intent(out) arguments for functions as well as subroutines, which means that functions could be used to modify variables in-place.

Where to define a function

Before modules were introduced by the Fortran 90 standard, it was common for functions to be defined in their own file. State-of-the-art linear algebra libraries like BLAS (Basic Linear Algebra Subprograms, https://www.openblas.net) or LAPACK (Linear Algebra PACKage, http://www.netlib.org/lapack) are still organized in the one-procedure-per-file model. For larger programs and libraries, it’s best practice to define functions in a module and have one module per source file. For short and simple programs, you can place the function definition within the scope of the main program. As we won’t go into more details on modules until the next chapter, we’ll define all our procedures in the main program for now.

To define a function in the main program, place it near the end of the program, immediately following the contains statement and before the end program statement. The contains statement separates the main program code above it from the procedure definitions beneath it, as the following listing demonstrates.

Listing 3.8 Defining a function inside the program scope

program cold_front
  ...                    ❶
contains                 ❷
  ...                    ❸
end program cold_front

❶ Program code goes here

❷ Marks the end of the program executable code, and the beginning of procedure definitions

❸ Put any procedure definitions here.

This rule will also apply to defining functions in a module, as you’ll learn in the next chapter.

Your first function

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top