Fortran1
Fortran1: The Basics
We'll forge our path through the verdant garden of Fortran90 using a number of examples. To get your copy of these examples, from the version control repository, login to your favourite linux machine (perhaps dylan), and type:
svn co http://source.ggy.bris.ac.uk/subversion/fortran1/trunk --username=guest ./fortran1
hello, world
Without further ado, and in-keeping with the most venerable of traditions, let's meet our first example--"hello, world":
cd fortran1/examples/example1
compile it by typing:
make
and run it by typing:
./hello_world.exe
Bingo! You've just compiled and run, perhaps your first, Fortran90 program. Hurrah! we're on our way:) Everybody whoop! Yeehah!
OK, OK...you'd better reign in your excitement. This is serious you know!:)
Enough of the magic, let's take a look inside the source code file. Open-up hello_world.f90, using cat, less, more or your favourite text editor, and you'll see:
! ! This is a comment line. ! Below is a simple 'hello, world' program written in Fortran90. ! It illustrates creating a main 'program' unit together ! with good habits, such as using 'implicit none' and comments. ! program hello_world implicit none write(*,*) "hello, world" end program hello_world
We have:
- some comment lines, giving us a helpful narrative
- the start of the main program unit
- the implicit none statement (more of that in the next section, but suffice to say, every well dressed Fortran program should have one)
- a write statement, printing our greeting to the screen
- and last, but not least, the end of the main program.
We'll be using make to compile all our example programs, so you won't have to worry about that side of things. (If you'd like to know more about make, you can take a look at our course on make, presented in a very similar style to this here excursion into Fortran90.)
This is all pretty straight forward, right? Open-up your text editor and try changing the greeting, just for the heck of it. Retype make and re-run it. We'll adopt a similar strategy for all the other examples we'll meet. If you ever want to get back to the original version of a program, just type:
svn revert hello_world.f90
Although this has all been fairly painless, we have made a very significant step--we are now editing, compiling and running Fortran programs. All the rest is basically just details!:)
Containers and the Types of Things
As fun as "hello, world" was, let's spice things up a little. For instance, let's introduce some variables. We'll need to move to the next example:
cd ../example2
Fortran90 has several types of built-in, or intrinsic, variables. Take a look in intrinsic_types.f90:
character :: sex ! a letter e.g. 'm' or 'f' character(len=12) :: name ! a string logical :: wed ! married? integer :: numBooks ! must be a whole number real :: height ! e.g. 1.83 m (good include units in comment) complex :: z ! real and imaginary parts
This set of types suffice for a great many programs. The above are all single entities. We'll meet arrays of things in a couple of examples time. In Fortran2, we'll also meet user-defined types. These allow us to group instances of intrinsic types together forming new kinds of thing--new types. User-defined types are the bees knees and can make programs much easier to work with. We'll leave the details to that later course, however.
The above snippet shows some variable declarations, along with a helpful comments. It's good practice to comment your declarations, as a programmer new to your code (or even yourself in a couple of months time) can have a hard time figuring out what is supposed to be stored in such-and-such a variable. While we're on the topic, it's also good practice to give your variables meaningful names, even if they are long. Trust me, a bit more typing now, perhaps, but a lot less head-scratching later on over what is stored in the inspiringly namedxbNew, or z2!
It's often a good idea to give variables an initial value when we declare them (working with unitialised variables in another common source of bugs):
character :: nucleotide = 'A' ! DNA has A,C,G & T character(len=50) :: infile = 'yourData.nc', outfile = "myData.nc" logical :: initialised = .true. ! or .false. real :: solConst = 1.37 ! Solar 'constant' in kW/m^2 complex :: sqrtMinusOne = (-1.0, 0.0) ! sqrt(-1)
Fortran90 also allows us to gives variables certain attributes, for example:
real,parameter :: pi = 3.14159 ! a fixed constant real(kind=8) :: totPrecip ! this is preferred to 'double precision'
The parameter attribute tells you, me and Fortran that pi is a constant. It's fixed and it's a compile-time error if we try to change it. This is a good thing, since we can catch nasty bugs that can creep in that way. We never want pi to be anything other than pi, right?! Assigning parameter attributes to quantities we know are constant is an example of defensive programming, or bug avoidance!
By default reals in Fortran are represented using 4 bytes of memory. The addition of (kind=8) gives us an 8-byte real, often referred to a double precision real. Fortran does have a double precision type, but the kind attribute is preferred. (Many compilers also support the promotion of all default, 4-byte reals and integers in your program through flags, typically named -r8 and -i8, respectively.) 8-byte reals can be useful as accumulators, since they can help to avoid rounding errors.
The remaining part of the program illustrates some pitfalls--beware!:
- integer division and it's truncation
- casting as a solution to mismatched types
- (integer) overflow
- (real number) underflow
Have a play with the program. Compile it and run it by typing:
make ./types.exe
Now modify the program (remembering "svn revert intrinsic_types.f90" if you make a mess). Try giving values to various types and also using operators such as:
- arithmetic: +, -, /, ** (exponentiation)
- functions: sin, cos, floor (rounding down)
- logic: .and., .or., .not., .eqv., .neqv.
- and you'll meet many more in the future..
About that mysterious intent none which we keep seeing at the start of our programs. Let me tell you a story: Once upon a time, the kings and queens of the garden of Fortran, being a generous and well meaning bunch, decided to save the programmers the bother of specifying the type of their variables. "Don't bother!", they said, "just be sure to give them appropriate names, and well sort out the rest." "Thank you. Thank you very much", said the programmers, and it was decreed that the names of integers should start with the letters i, j, k, l, m, or n, and the names of reals would start with the other letters. Anyhow, this all seemed like a great wheeze and everybody was very happy. This lasted for a while, but after time, the programmers got complacent and forgot how to name things and it all got rather messy. Integers became reals, reals became integers and before they knew it, the programmers had bugs all over the place! Boo. The kings and queens conferred on the matter and they realised that they had made a grave error in their gift of implicit typing. However, they couldn't undo what they had done. Instead, they had to persuade the programmers to give it up voluntarily. "Anything, anything!", they pleaded "to get rid of all these bugs!", and so it passed that every good programmer agreed to put implicit none at the top of every program they wrote, and they all lived happily ever after.
If, Do, Select and Other Ways to Control the Flow
Programs are like cooking recipes. We've covered the how much of this and how much of that part. However, we also need to cover the doing bit--do this and then do that, and for how long etc. This is generically termed control flow. Fortran gives us a fairly rich language with which to describe how we would like things done. Next example:
cd ../example3
Take a look inside control.f90. We have some variable declarations and then we encounter our first conditional:
if (initialised .eqv. .true.) then write (*,*) "The variable 'area' is initialised and has the value:", area else write (*,*) "The variable 'area' is NOT initialised and has the value:", area end if
This is fairly self eplanatory--if..something is the case..then..else.. You can also have an elseif. In fact you can have as many of those as you like. You can also have as many statements inside each clause as you like. Talk about spoiled!
Our first do loop is of the form:
do ii=1,5 write (*,*) "Do loop counter ii is:", ii end do
Again, this is fairly readable. ii is first given the value of 1, the body of the loop is evaluated and then we go back to the top again. Except this time we increment the counter (ii) by the default amount, which is 1. When we're at the top and we take ii past 5, we stop the loop and move on to the next statement passed the end do. You're allowed as many statements inside the loop as you like. Indeed, you're allowed more loops, conditions, loops in loops, just about anything you can think of! Beware, however, debugging a huge construct of nested this that and the other can be beyond the limits of human patience. Keep our programs simple and you will be happier for it.
The other loop examples show variations in the stopping condition and stride (i.e. how much we increment by), including counting backwards, and stopping before we've even started!
Select is another control structure. This is a neat way of saying, "if..then..elsif..else.." The default clause at the bottom is important. Dropping this off can lead to fall-through, where none of the cases triggered. This is rarely what you want and can lead to nasty bugs.
As before, compile it, run it and generally muck about. These are only a few of the control structures provided to us by Fortran. You'll find that you can do most things with these three, however.
Not one, Many!
That was fun. Back to thinking about variables for a moment:
cd ../example4
Last time we declared just one thing of a given type. Sometimes we're greedy! Sometimes we want more! To be fair, some things are naturally represented by a vector or a matrix. Think of values on a grid, solutions to linear systems, points in space, transformations such as scaling or rotation of vectors. For these sorts of things the kings and queens of Fortran gave us programmers arrays. Take a look inside static_array.f90:
real, dimension(4) :: tinyGrid = (/1.0, 2.0, 3.0, 4.0/) real, dimension(2,2) :: square ! 2 rows, 2 columns real, dimension(3,2) :: rectangle ! 3 rows, 2 columns
The syntax here reads, "we'll have an one-dimensional array (i.e. vector) of 4 reals called tinyGrid, please, and we'll set the initial values of the cells in that array to be 1.0, 2.0, 3.0 and 4.0, respectively.
For the second and third declarations, we're asking for two-dimensional arrays. One with two rows and two colums, called square, and one with three rows and two colums. We're calling that rectangle.
The program then goes on to print out the contents of tinyGrid.
Fortran90 provides a couple of handy intrinsic routines for determining the size (how many cells in total) and the shape (number of dimensions and the extent of each dimension) of an array. Fortran90 also allows us to reshape an array on-the-fly. Using this intrinsic, we can copy the values from tinyGrid into square. Neat.
Fortran also provides us with a rather rich set of operators (+, -, *, / etc.) for array-valued variables. Have a go at playing with these. If you know some linear algebra, you're going to have a great time with this example!
The static malarky is because Fortran90 also allows us to say we want an array, but we don't know how big we want it to be yet. "We'll decide that at run-time", we programmers say. This can be handy if you're reading in some data, say a pay-roll, and you don't know how many employees you'll have from one year to the next. Fortran90 calls these allocatable arrays and we'll meet them in Fortran2.
If Things get Hectic, Outsource!
cd ../example5
Now, as we get more ambitious, the size of our program grows. Before long, it can get unwieldy. Also we may find that we repeat ourselves. We do the same thing twice, three times. Heck, many times! Now is the time to start breaking your program into chunks, re-using some from time-to-time, making it more manageable. Fortran gives us two routes to chunk-ification, functions and subroutines. Let's deal with subroutines first. In procedures.f90, scroll down a bit and ou can see:
subroutine mirror(len,inArray,outArray) implicit none ! dummy variables integer, intent(in) :: len character,dimension(len),intent(in) :: inArray character,dimension(len),intent(out) :: outArray ! local variables integer :: ii integer :: lucky = 3 ! notice scope of this identically named variable do ii=1,len outArray(len+1-ii) = inArray(ii) end do write (*,*) "'lucky' _inside_ subroutine is:", lucky end subroutine mirror
Now, note that this is outside of the main program unit. (In principle, we could hive this off into another source code file, but we'll leave that discussion until Fortran2.) Notice also that we have a shiney implicit none resplendent at the top of the subroutine. Overall, it looks pretty similar to how a main program unit might look, but with the addition of arguments. Those fellows in the parentheses after the subroutine name. The declaration part also lists those arguments and we've commented that this are so-called dummy variables. We also see an attribute that we've not seen before, called intent. This is a very handy tool for defensive programming (remember aka bug avoidance). Using intent we can say that the integer len is an input and as such we're not going to try to change it. Likewise for the caharcter array inArray. It would be a compile-error if we did. We also state that the character array outArray is an output and we're going to give it a new value come what may! We also have some variables that we note are local. Interestingly enough, one of our local variables, the integer called lucky, has exactly the same name as a variable in the main program unit. When we run the program, however, we will see that the two do not interfere with each other. This is down to their scope. The scope of lucky in the main program is all and only the main program unit and the scope of lucky in the subroutine is all and only the subroutine. We say that the main program unit and the subroutine units have different name spaces.
Well, we've seen a lot of new syntax and concepts in all that. Useful ones though. This program is small and artificial, so it's hard to see the benefits just yet. You will, however, as your programs grow. The subroutine is called from the main program , funnily enough by a call statement. Notice how the arguments passed to the subroutine in the call statement also have different names in the main program and the subroutine. That's scope again.
Functions are a bit different to subroutines. Notice that we don't call them in the same way. We still pass arguments, but the functions returns and value, and so we could place a function on the right-hand side (RHS) of an assignment. Looking at the body of the function:
function isPrime(num) implicit none ! return and dummy variables logical :: isPrime integer, intent(in) :: num select case (num) case (2,3,5,7,11,13,17,19,23,29) isPrime = .true. case default isPrime = .false. end select end function isPrime
we can see that we have a declaration for a variable with the same name as the function. The type of this variable is the return type of the function. Indeed the value of this variable when we reach the bottom of the function is the value passed by to the calling routine. Note that we can call functions and subroutines from other functions and subroutines etc. in a nested fashion. This particular function is a fairly lame prime number spotter and returns a logical (.true. or .false.) depending upon whether its single input integer is prime.
The last thing of note is the funky interface structure at the top of the main program:
interface function isPrime(num) logical :: isPrime integer, intent(in) :: num end function isPrime end interface
Our function definition is outside of the main program and so is said to be external. The main program unit needs to know about it, however, and an interface structure is a good way to do this as it prompts Fortran to check that all the arguments match up between the call and the definition. It's the way to do it.
Try running the program and writing some functions and subroutines of your own.
You can Have That, and I'll Have This
File input/output
cd ../example6
In this example we se how we can write output to a file and also read input from a file. before you can use a file, it needs to be opened. This is done with the open statement:
open(unit=19,file="output.txt",form=formatted,status=old,iostat=ios)
- the unit is just a number given to the file so that we can refer to it later. It needs to be between 1 and 99 and numbers 5 and 6 are reserved (5 for keyboard entry and 6 for screen output)
- the file is simply the name of the file to open
- the <form is used to give the file format. It can be formaatted (i.e. a text file) or unformatted (i.e. binary).
- he status specifies the behaviour if the file exists:
- old means the file must exists
- new means the file cannot exists pror to being opened
- replace is used when an old file will be overwritten
- iostat is a keyword which returns an error in case of an error, i.e. the file cannot be opened for instance.
Writing to a character string
cd ../example7
Namelist
And all of a sudden, we're at our last example:
cd ../example8
In this one, we'll look at file-based input and output. Fortran provides a way of grouping variables together into a set, called a namelist, that are input or output from our program en masse. This is a common situation. The statement:
namelist /example_nml/numBooks,initialised,name,vec
sets it up.
Fortran further provides us with built-in mechanisms for reading or writing a namelist to or from a file.
First, we must open the file:
open(unit=56,file='input.nml',status='old',iostat=ios) if (ios /= 0) then print*,'ERROR: could not open namelist file' stop end if
In this snippet, we've associated the file called input.nml (you can find this in the directory already) with a so-called Fortran unit. Unit number 56 to be precise, although we could have picked any unit number we liked, so long as it wasn't associated with a different file. We also made sure to tell Fortran that this is an old file, i.e. that it already exists, and to check the error code, in case the open operation failed. If that were to happen, we've asked the program to halt with an error.
Now, assuming that we've opened the file OK, we proceed to read its contents. Fortran makes this rather easy for us, given that the information is contained in a namelist:
read(UNIT=56,NML=example_nml,IOSTAT=ios) if (ios /= 0) then print*,'ERROR: could not read example namelist' stop else close(56) end if
In the read statement, we told fortran that we wanted to read a namelist and that the group of variables should be labelled in the file as example_nml. Again we've checked the error status and decided to halt with an error should the read statement fail for any reason. Take a look at the contents of input.nml. See that it is ascii text and that the variables (with their values) do not need to be listed in the same order in the file as they are in the program. If the data is read successfully, we close the file unit.
The program procedes to print the values to screen, demonstrating that they have indeed come from the file, to assign new values to the variables and then to write the modified values to a new file, called modified.nml. Compare the two ascii text files. Try running the program a second time and you will recieve an error, telling you that the output could not be written since a file called modified.nml exists and that we expressly stated that is was to be a new file. Delete modified.nml, try again and it will succeed. Try changing the namelist, values, filenames etc. Go for it, make a mess! You'll learn a lot from it:)