Fortran: Private Data Components and Custom constructors
Matthias Noback
In the previous post we have worked on the point_t
derived type, turning the module procedure distance
into a type-bound procedure:
module geometry
! ...
type :: point_t
real :: x
real :: y
contains
procedure :: distance => point_distance
end type point_t
contains
pure function point_distance(point_1, point_2) result(the_distance)
class(point_t), intent(in) :: point_1
! ...
end function point_distance
end module geometry
Private data components
Type-bound procedures can help us tie relevant behaviors (procedures) to derived types. It also allows us to let the derived type keep its data to itself. This is often called data hiding, or encapsulation of state. Nothing outside the module where the derived type is defined will have (read or write) access to its data components. This can be accomplished by adding the attribute private
to each data component:
type :: point_t
- real :: x
+ real, private :: x
- real :: y
+ real, private :: y
contains
procedure :: distance => point_distance
end type point_t
Or we can make all components private
at once:
type :: point_t
+ private
real :: x
real :: y
contains
procedure :: distance => point_distance
end type point_t
This has no effect on the type-bound procedures. That’s because private
means “private to the module where the derived type is defined”. So any procedure in the same module can still access the private data components.
Outside the module, i.e. in another module
or program
, we will get a compiler error if we try to access one of point_t
’s private data components.
type(point_t) :: some_point
! This is not allowed outside the module that contains `point_t`:
some_point%x = 3.0
The error we receive:
error #6292: The parent type of this component is use associated with
the PRIVATE attribute. [X]
some_point%x = 3.0
--------------^
We will also get a compiler error when we try to instantiate a point_t
:
error #6053: Structure constructor may not have components with the
PRIVATE attribute [0.0]
origin = point_t(0.0, 0.0)
--------------------^
The default structure constructor (the part that looks like we’re invoking point_t
as a function) only works outside the module when the data components are public, which they are by default. This makes sense, because the default structure constructor effectively behaves like this:
type(point_t), allocatable :: origin
! ...
allocate (origin)
origin%x = 0.0
origin%y = 0.0
Given that those data components are now private, this is no longer allowed. The conclusion should be that we can only instantiate a derived type inside its own module. But we want to enable other modules and programs to create fresh instances too…
Factory functions
The solution is to let the module provide a factory function. This function should return an instance of the derived type and assign values to its private data components. For instance, we can define a create_point
(or simply point
) function that accepts values for x
and y
:
module geometry
! ...
public :: create_point
contains
pure function create_point(x, y) result(point)
real, intent(in) :: x
real, intent(in) :: y
type(point_t) :: point
point%x = x
point%y = y
end function create_point
end module geometry
Outside the module we can import and call this function to get a new point_t
instance:
use geometry, only: point_t, create_point
type(point_t), allocatable :: some_point
some_point = create_point(3.0, 4.0)
Notice how in this case we replicate the data components and their declarations inside the factory function, where they become arguments. This replicates a common pattern in object-oriented languages where classes have a constructor function where the arguments are the same number and type as the class properties. The constructor then assigns these values to the properties. In PHP you could write this:
class Point
{
private float $x;
private float $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
}
Note that it’s not at all a requirement to repeat the complete list of data components as factory arguments. The signature of the factory function can be completely different from the default structure constructor. Actually, we can implement several alternative ways of creating the same type of thing. As an example, we can have a function that creates an origin
for us, with default values 0.0
and 0.0
for x
and y
(we’ll skip the theoretical discussion about the question if this makes sense):
pure function create_origin() result(point)
type(point_t) :: point
point%x = 0.0
point%y = 0.0
end function create_origin
Another useful factory might be one that accepts integer
values for x
and y
and casts them as real
values before assigning them:
pure function create_point_from_ints(x, y) result(point)
integer, intent(in) :: x
integer, intent(in) :: y
type(point_t) :: point
point%x = real(x)
point%y = real(y)
end function create_point_from_ints
Now we have different ways of creating point_t
instances:
use geometry, only: point_t, &
create_point, &
create_origin, &
create_point_from_ints
type(point_t), allocatable :: origin
type(point_t), allocatable :: some_point
! no arguments
origin = create_origin()
! `real` arguments
some_point = create_point(3.0, 4.0)
! same point, but created with `integer` arguments
some_point = create_point_from_ints(3, 4)
An interface for the factory functions
It’s great that as a user of point_t
we now have access to multiple factory functions, but this has a few downsides:
- The user has to remember the options, or look them up in the
module
itself. - The user has to select the right factory, based on the arguments they want or can provide.
- The module has to expose all the factory functions separately, leading to a long list of
public
elements.
We can fix all these problems by declaring an interface
. Please note: this has a completely different meaning than interface
in many other programming languages. What Fortran means with an interface
here is in other languages known as “function overloading”:
module geometry
! ...
interface point
procedure :: create_point
procedure :: create_origin
procedure :: create_point_from_ints
end interface point
contains
! ...
end module geometry
This allows us to define a single abstract function (in this case point
) with multiple concrete alternatives. Based on how the abstract function is called (how many arguments, which argument types, and which return type is expected), the compiler will select one of the available concrete functions (create_point
, create_origin
, create_point_from_ints
). Now we can write the following:
use geometry, only: point_t, point
! ...
origin = point()
some_point = point(3.0, 4.0)
some_point = point(3, 4)
An interface with the same name as the derived type
One thing we can do is to give the interface the same name as the derived type (point_t
).
interface point_t
procedure :: create_point
procedure :: create_origin
procedure :: create_point_from_ints
end interface point_t
This surprisingly works; for the compiler it doesn’t cause ambiguity. Now we only need one import, point_t
, which imports both the type and the interface:
-use geometry, only: point_t, point
+use geometry, only: point_t
Also, the module can keep most of its components to itself:
module geometry
! ...
public :: point_t
- public :: point
- public :: create_point
- public :: create_origin
- public :: create_point_from_ints
Finally, we’ve come full circle: instantiating point_t
looks like it did when we used the default structure constructor, except now we have full control over populating the private data components:
use geometry, only: point_t
type(point_t), allocatable :: some_point
some_point = point_t(3.0, 4.0)
Under the hood, this will actually invoke the create_point
factory function, because the compiler selects it from the point_t
interface based on the expected return type (point_t
) and the provided arguments (real
, real
).
In the next post we’ll consider a different category of derived types, namely service types.