Fortran - Errors and error handling - Part 6 - Guarantees
Matthias Noback
Parsing an array of strings to a polyline should fail if one of the strings could not be parsed to a string. But it should also fail if the resulting array of points is empty. Or at least, if that makes sense in the application’s domain. You could say that a polyline with no points is still a polyline, just like an empty set is still a set. Similarly, in some scenarios it may be okay for a polyline to have just a single point. For now, let’s skip the mathematical discussion and take this as a practical rule that we want to enforce: a polyline has at least 2 points. The type we currently have can’t give us such a guarantee:
type :: polyline_t
type(point_t), dimension(:), allocatable :: points
end type polyline_t
Anyone can just create a polyline_t
instance, and assign any number of points to points
, or even allocate it to have 0
elements.
The first step to prevent bad things from happening to a derived type is to make the data components private
. This gives us some insurance: outside the module where polyline_t
is defined, no user can populate or allocate a data component manually, bypassing any rules we have in mind for it. Because users may still want to instantiate a polyline_t
, we have to offer a factory function, e.g. create_polyline
. In the case of polyline_t
, the user has to provide an array of point_t
instances. However, we only want to return a polyline_t
from the factory if the provided array has at least 2 elements. If the user provides 0 or 1 point_t
instances, we want to return an error with a helpful message. So the return type of the factory function shouldn’t be polyline_t
, but polyline_or_error_t
:
pure function create_polyline(points) result(res)
type(point_t), dimension(:), intent(in) :: points
type(polyline_or_error_t) :: res
if (size(points) >= 2) then
res%polyline = polyline_t(points)
else
res%error = error_t('At least 2 points are required for a polyline')
end if
end function create_polyline
Now we should use create_polyline
wherever we normally instantiate polyline_t
via its default structure constructor, e.g. in parse_polyline
. The return type of this function is also polyline_or_error_t
, so we can forward the result of calling create_polyline
as-is:
pure function parse_polyline(strings) result(res)
type(string_t), dimension(:), intent(in) :: strings
type(polyline_or_error_t) :: res
! ...
if (size(errors) == 0) then
! Filter the points
points = pack(points_or_errors, &
[(allocated(points_or_errors(i)%point), i=1, size(points_or_errors))])
- res%polyline = polyline_t([(points(i)%point, i=1, size(points))])
+ res = create_polyline([(points(i)%point, i=1, size(points))])
else
! ...
end if
end function parse_polyline
Once we know that every polyline_t
instance has been created via the create_polyline
factory function, we gain some important knowledge. Whenever a function returns a polyline_t
instance, or when we write a function that accepts a polyline_t
instance, we can be sure that the instance has been checked against the rules we enforce for polylines: any given polyline_t
instance will be valid. This is known as the Evidence Pattern; the type itself is evidence that the data it contains is valid. We never have to check this again.
Whenever a type comes with special rules, in other languages the default solution would be to have a constructor that throws exceptions to prevent the object from being created. In Fortran, the solution to this is to have a factory function which returns either a valid instance or an error, indicating the first thing that was wrong about the provided data.
Language limitations
It’s good to mention one caveat here. If we know that the whole application creates instances of polyline_t
only via create_polyline
, then indeed, we can be sure that it contains at least 2 points. However, and we saw this before, as a user you can also get an instance of polyline_t
by declaring a variable of type polyline_t
, as long as it’s not allocatable
:
type(polyline_t) :: broken_polyline
This variable will automatically hold a polyline_t
instance, and can be used as such. For instance, if it has a type-bound procedure length
, you can just call it:
print *, broken_polyline%length()
In this case, the program will crash, because that procedure likely doesn’t check whether the points
data component has been allocated, so we’ll see a memory access error. This problem may go unnoticed for some time if it happens in some if/else
branch (the compiler doesn’t warn about this kind of mistake). That’s why I consider it a best practice to always add allocatable
to the declaration of a derived type variable that is not a dummy argument:
type(polyline_t), allocatable :: broken_polyline
Before a user can do anything with this variable, they need to retrieve a polyline_t
instance in the official way, i.e. via the factory function.
We’ve seen several approaches to dealing with errors returned from functions. In the next post we’ll look at what needs to happen when it becomes clear that an error occurred that you can’t recover from. We want to quit the program, but what’s a nice way to do that?