Fortran - Functional Programming - List type
Matthias Noback
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
:
public :: list
public :: integer_list_t
type :: integer_list_t
integer, dimension(:), allocatable :: values
end type integer_list_t
interface list
procedure :: create_integer_list
end interface list
contains
pure function create_integer_list(values) result(res)
integer, dimension(:), intent(in) :: values
type(integer_list_t) :: res
res%values = values
end function create_integer_list
Now we can write this:
type(integer_list_t), allocatable :: integers
integers = list([1, 3, 5, 7])
The next step is to add a type-bound procedure filter
to integer_list_t
. Knowing that we want to support both regular procedure arguments and closures in the form of a derived type, it’s likely that we go down the same path as we did before when we designed the filter
function. We’ll have to duplicate a lot of code if we want to support both the original filter
function and the filter
type-bound procedure…
Assuming that we want to use a list type from now on, we start by upgrading the existing filter_*
functions to type-bound procedures. They will need a first argument that is an integer_list_t
(self
) instead of an array of integers. We can find the actual integers in the values
data component of self
. Also, the result should be an integer_list_t
, so we can call the next function directly on it:
-pure function filter_integers(values, filter_func) result(res)
- integer, dimension(:), intent(in) :: values
- integer, dimension(:), allocatable :: res
+pure function filter_integers(self, filter_func) result(res)
+ class(integer_list_t), intent(in) :: self
+ type(integer_list_t) :: res
interface
pure function filter_func(value) result(keep)
implicit none(type, external)
integer, intent(in) :: value
logical :: keep
end function filter_func
end interface
integer :: i
- values = pack(values, &
- [(filter_func(values(i)), i=1, size(values))])
+ res%values = pack(self%values, &
+ [(filter_func(self%values(i)), i=1, size(self%values))])
end function filter_integers
After making the same changes to filter_integers_with_dt
we can bind both these functions to integer_list_t
. Similar to how we exposed these specific functions as a single filter
interface
, we can expose both these type-bound procedures as a generic
type-bound procedure filter
:
type :: integer_list_t
integer, dimension(:), allocatable :: values
contains
procedure, private :: filter_integers
procedure, private :: filter_integers_with_dt
generic :: filter => filter_integers, filter_integers_with_dt
end type integer_list_t
Note that the specific filter_integers
and filter_integers_with_dt
procedures are marked as private
. They don’t need to be exposed to users.
Now that we have this highly flexible filter
function available on integer_list_t
, the next line of our sample code works too:
type(integer_list_t), allocatable :: integers
integers = list([1, 3, 5, 7])
integers = integers%filter(divisible_by(3))
We can do the same thing for map
. We can upgrade the existing map_*
functions to become type-bound procedures. In the end we can expose a generic type-bound procedure map
on integer_list_t
. One thing to consider is that map_*
functions should return list types as well. What about the map_integer_to_real
function we created earlier? It should return a real_list_t
, not an integer_list_t
. We don’t have that list type yet, but it’s easy to create.
Although it takes a few extra lines, this gives us endless possibilities for composing functions into smart “pipelines”, building up the end result of a calculation from smaller, intermediate results.
There is one major transformation operation that we haven’t covered yet, which is the reduction function. We’ll discuss it in the next post.