I'm writing this page so that I can remember a number of details about f90 coding that always seem to slip my mind. I figured I'd put it up for folks that might be going through similar problems.
Useful string routines
Initialization
To initialize an array of strings, try
real, dimension(3), character*80 :: (/ 'a ', 'ab ', 'abc' /)
The issue at hand is that, at least in the NAGware f90 compiler that I use, there is no easy way to initialize
strings with an automatic length. For this reason, I declare the elements to be of type character*80.
Also, my compiler complains if the individual strings within the array are not all of the same length, even if they
are all smaller than the maximum length, so I add whitespace at the end to even them up. The slashes inside the
parentheses delimit a static array constructor.
To initialize a string passed to a subroutine, things are slightly more elegant.
subroutine mysub(str)
character*(len(str)) :: str
...
In this manner you can let the subroutine figure out how long the string is implicitly. You still have to worry about
any whitespace after the text of the string; use the len_trim and trim functions to
deal with that issue.
Individual characters
You can treat strings as arrays of characters and take slices of them just like you can with data. For instance,
character*10 :: str = 'abcdefg' print *,str(2:6)
prints 'bcdef'.
You can turn a single-digit integer i into string characters using achar(i+iachar("0")).
This is useful in do loops where it can be concatenated with other strings to form an output file name, e.g.
'myproject.'//achar(i+iachar('0'))//'.out'.
Passing functions as arguments
I find myself always wanting to pass functions as arguments to other routines, usually for math library routines
like integration/differentiation and root-finding. The problem comes in if you want to make your code look nice, with
the main program file being relatively short and showing just the program flow. Functions that are
declared in a program's contains section can't be passed as arguments. Functions declared within the
same source file must be declared before the main program and interface'd into the main program so
that the compiler can check the arguments and make sure they are right. Functions declared outside the program source
file have to be imported using the external statement. In short, it's quite a mess.
My current favorite solution to this problem is to break most projects up into three files. All of my functions are
containsed in a module called functions, defined in a file called functions.f90.
For example,
module functions
use datums
implicit none
interface x
module procedure x
end interface
contains
real function x(z)
real, intent(in) :: z
x = z/H
end function x
end module functions
In a separate file, datums.f90, I define a module called datums:
module datums
implicit none
real, parameter :: H = 120. ! inches
end module datums
There's quite a bit to this example. First off, the datums module is meant to hold common parameters and
variables. It doesn't use anything, so it can be used by any module without conflicts. It gets
used by both my program and my functions module. It's called 'datums' because 'data' is already
a keyword (albeit deprecated) in Fortran 77. In this example, it defines a height (say, of a reactor core).
There are a number of advantages to separating the code in this manner, from my perspective:
- Improved readability. Your program should be a page or two long, showing only the flow of the algorithm. Details of algorithm implementation are clearly defined in a separate file. Constants used are centrally defined and modifiable. This is good for future users of the program and for evaluation purposes.
-
Built-in interfacing. By
useing thefunctionsmodule in your main program, you already have all of the functions it defines in your namespace, and you can pass them to other procedures with no need for explicitinterfacestatements. This is performed by including themodule procedureline before thecontainssection in thefunctionsmodule. If you don't include this line, you can still call the function, but you can't pass it as an argument. -
Simplified building of the project. It may seem more complicated at first to have to build three files
instead of one, but the use of a generic project makefile makes things quite easy. To build my projects, I just type
makeon the command line and it does whatever needs to be done. I've included my makefile at the end of this document for reference. Also, as I find myself viewing the output of the program as just another thing to be built, I can just type inmake outormake psto run the program to generate the output files or to run gnuplot to generate the graphs. Too bad I didn't know this a few years ago. -
Clean function definitions. You don't have to worry about explicitly passing the global parameters to
each and every function, nor does the
datumsmodule have to beused in each function, as they inherit variables from the module. This cleans up and clarifies function definitions quite a bit.
Example makefile
This is a GNU makefile, and it doesn't work with other systems. On NC State's eos system, type add gnu to
get access to the GNU software, and agg nagf90 to get the NAGware compiler. Place the contents of this
code example in a file named 'Makefile' in your project directory. For a three-file setup, you'll want to set
libraries = datums.o functions.o.
libraries =
program = skeleton
otherfiles =
outfiles =
f90 = f90 -Qpath /ncsu/nagf90/lib -I/ncsu/nagf90/lib -L/ncsu/nagf90/lib \
-L$(HOME)/lib -I$(HOME)/lib/mod
$(program): $(libraries) $(program).f90 $(otherfiles)
$(f90) -u -o $(program) $(libraries) $(otherfiles) $(program).f90
$(libraries): %.o : %.f90
$(f90) -u -c $<
$(outfiles): %.out : $(program)
-./$(program)
ps : $(outfiles) $(program) $(program).gnuplot
gnuplot $(program).gnuplot > $(program).ps
clean:
rm -f *.mod *.out *.ps *.o $(program) *~
I use this makefile at NC State. If you are using it somewhere else, probably a better definition for the
f90 variable would be
f90 = f90 -L$(HOME)/lib -I$(HOME)/lib/mod
This assumes that you have a ~/lib directory that you keep the modules in and a mod directory beneath that the the .mod files (like header files in C, except not human-editable, or edible) are stored in. I have a makefile in my ~/lib directory that will build libraries from ~/lib/src into ~/lib, putting the .mod files in ~/lib/mod:
libraries = math.o datafile_type.o steamtables.o least_squares.o linear_least_squares.o
f90 = f90 -Qpath /ncsu/nagf90/lib -I/ncsu/nagf90/lib -L/ncsu/nagf90/lib \
-L$(HOME)/lib -mdir $(HOME)/lib/mod -I$(HOME)/lib/mod
libraries: $(libraries)
%.o : src/%.f90
$(f90) -u -c $<
clean:
rm -f mod/*.mod *.o *~
The libraries mentioned in the libraries variable are available at this site.