Fortran - Errors and error handling - Part 7 - Fatal errors
We’ve encountered several ways of designing functions in a way that allows them to fail for some reason, without stopping the program, or making it otherwise risky or awkward to use the function. We introduced the error_t
type which is very flexible. It can be used to provide some information to the caller, helping them understand what went wrong and how it can be fixed. By allowing errors to be wrapped inside others, we can create chains of errors that describe the problem at various abstraction levels. It gives back control to the user: how do they want to deal with an error? Would they like to try something else? Or, in the end, should we just stop trying and quit te program?
Fortran - Errors and error handling - Part 6 - Guarantees
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:
Fortran - Errors and error handling - Part 5 - Error propagation
Parsing a string to a point may not be successful, and we were able to communicate that to users of parse_point()
by returning a custom type called point_or_error_t
.
What if we want to parse not just one point but a set of points, forming a polyline, from a list of strings? The result of parsing each string separately is a point or an error. Only if the list of those point_or_error_t
values contains no error at all should we consider turning the points into a polyline. If any of the values is an error, we shouldn’t make a polyline of the other valid points, but instead return an error: “Could not create polyline from strings. Previous error: Could not extract two decimal numbers from …” With this error we communicate what we were trying to do at the highest conceptual level (create a polyline), and what lower-level error caused this to fail.
Fortran - Errors and error handling - Part 4 - Using an Either type
We looked at optionally returning a value from a function. In the case of average
it was an average value, or no value at all. In some more elaborate functions, the “no result” case needs some more explanation to the caller of the function. We may feel the need to communicate what went wrong, or for what reason. This gives the user the opportunity to pass better input next time, or fix some external issue that the program can’t fix itself (e.g. a file not being readable).
Fortran - Errors and error handling - Part 3 - Preventing edge cases with types
In the previous post we introduced an optional_real_t
type to be the return value of an average
function, because depending on the size of the array of numbers passed to it this function may or may not return a real
value.
What if we could just prevent people from calling the average
function when they don’t have any numbers? Maybe we’re lagging too far behind if we have to verify the size of the array inside the average
function.
Fortran - Errors and error handling - Part 2 - Optional results
In the previous post we’ve looked at several ways to communicate to the user of a function that something could go wrong, or that maybe the function couldn’t produce the result that the user was looking for. In the case of an average function, we’d want to communicate to the user that in some cases the function can’t produce a meaningful result, namely if the array of numbers is empty. This isn’t really an error, it’s just that we don’t have a way to answer the question “what’s the average of this array?”. To communicate that with the function signature, we tried adding a “success” return value, making the calculated average an intent(out)
argument. The function modifies the variable passed by the user to contain the average value, but only if the array is not empty:
Fortran - Errors and error handling - Part 1 - Exploration
Fortran doesn’t have exceptions. You can’t jump out of a function when something unexpected happens, then reflect on what happened outside the function, with the help of a try/catch block. As an alternative, intrinsic (built-in) procedures and constructs like write
, read
and open
allow you to pass an integer
variable as one of its arguments, to which it will assign a specific value indicating whether an error occurred. For example, when you try to open a file that doesn’t exist, you will get a non-zero value in the variable passed as the argument for iostat
:
Fortran - Functional Programming Concepts - Reduce
We’ve implemented filter and map operations. The results of both these operations are new arrays. Yet another common operation on arrays is the so-called reduction operation; we “reduce” multiple values to a single value. The most obvious example is calculating the sum of an array of integers: the input is an array (or list) and the output is a single integer
:
pure function sum_all(numbers) result(res)
integer, dimension(:), intent(in) :: numbers
integer :: res
integer :: i
res = 0
do i = 1, size(numbers)
res = res + numbers(i)
end do
end function sum_all
Just like with filter
and map
we can imagine a generalization of this process, which involves looping over the array elements and building up a single return value along the way. We start with the existing sum_all
function. The first step is to let the user decide what the return value should be, which is the logic that happens inside the do
loop. We extract a function for this:
Fortran - Functional Programming - List type
In the previous post we looked at this dream scenario:
list([1, 3, 5, 7]) % filter(divisible_by(3)) % map(square)
As mentioned, we can’t chain function calls like this. But we can put intermediate results in local variables and achieve something similar:
integers = list([1, 3, 5, 7])
divisible_by_3 = integers%filter(divisible_by(3))
squared = divisible_by_3%map(square)
If the intermediate values are all the same type, we can reuse the variable:
integers = list([1, 3, 5, 7])
integers = integers%filter(divisible_by(3))
integers = divisible_by_3%map(square)
To enable this syntax, it’s clear that we need to introduce a derived type for a list of integers that has type-bound procedures filter
and map
. These functions should each return a new list, so we can call any of these functions again on the new list. Let’s start with an integer_list_t
which contains an array of integers. We also provide a generic list
interface
with a specific procedure create_integer_list
for creating the integer_list_t
:
Fortran - Functional Programming Concepts - Map
We considered the problem of filtering elements in an array, keeping only the ones we want. A decision about that is made by a function we pass as an argument.
A similar problem, but with its own challenges, is when we have an array of values and want to create a new array, with an equal number of elements, but each element has been transformed in some way. For instance, say we have an array of integers and want to square each integer. A procedural approach looks as follows: