Custom Types in Fortran
Types like INTEGER
and LOGICAL
are used by the compiler to make sure you’re
passing the correct arguments to functions. This is really helpful when
functions have lots of arguments, as it lets you know if you’ve missed one, or
entered them in the wrong order. You can create your own types to bundle
together related variables, or to provide more granularity to argument
checking.
Types collect one or more variables into a single bundle. They must be defined inside a module, a type definition looks like:
MODULE field_m
USE unit_m, ONLY: unit_t
IMPLICIT NONE
PRIVATE
PUBLIC :: field_t
TYPE field_t
REAL, ALLOCTABLE :: dat(:,:)
TYPE(unit_t) :: unit
END TYPE
END MODULE
This type combines a Fortran array with a unit
, allowing functions to check
to make sure you’re not doing something silly like adding a length to a
velocity. Combining the two variables in the same type means that they can’t
get separated as they get passed between functions - you always know that the
data in the array dat
has the unit given by unit
.
To create variables of the type you must first USE
the module, then declare
variables with ‘TYPE(
type_name)
’:
USE field_m, ONLY: field_t
TYPE(field_t) :: temperature
The variables contained inside the type are called ‘member variables’, and can
be accessed using the %
operator - ‘variable%
member’. A unit-checking
addition function might look like:
SUBROUTINE add_fields(b, a)
USE field_m, ONLY: field_t
IMPLICIT NONE
TYPE(field_t), INTENT(IN) :: a
TYPE(field_t), INTENT(INOUT) :: b
IF (a%unit /= b%unit) THEN
! Incompatible units, abort
CALL EXIT()
END IF
b%dat = b%dat + a%dat
END SUBROUTINE
By default the values of member variables will be undefined. It’s common to create a constructor function with the same name as the type, this should return a new variable of that type. As with all interfaces you can add multiple functions to allow the type to be constructed from different arguments, for instance by specifying the field size or by initialising the field from an existing array:
MODULE field_m
! ...
INTERFACE field_t
PROCEDURE new_field_xy
PROCEDURE new_field_array
END INTERFACE
CONTAINS
FUNCTION new_field_xy(nx, ny, unit)
USE constants_m, ONLY: NAN
IMPLICIT NONE
INTEGER, INTENT(IN) :: nx
INTEGER, INTENT(IN) :: ny
TYPE(unit_t), INTENT(IN) :: unit
TYPE(field_t) :: new_field
ALLOCATE(new_field%dat(nx, ny))
new_field%dat = NAN
new_field%unit = unit
END FUNCTION
FUNCTION new_field_array(dat, unit)
IMPLICIT NONE
REAL, INTENT(IN) :: dat(:,:)
TYPE(unit_t), INTENT(IN) :: unit
TYPE(field_t) :: new_field
ALLOCATE(new_field%dat, SOURCE=dat)
new_field%unit = unit
END FUNCTION
END MODULE
You can then initialise variables by calling the constructor:
temperature = field_t(10, 15, kelvin)
From here it’s also possible to add operators to the type, so that you can add
two fields with +
, scale a field by a constant with *
, or even add entirely
new operators. You can also extend a type, creating new types which contain the
same data but that you can control how it’s used - say a temperature_field_t
and a pressure_field_t
so that you don’t confuse the two fields in function
arguments.