Fortran - Functional Programming Concepts - Generic Filtering
Matthias Noback
We have a working filter
function for integers. Now can we generalize it? It would be nice if we could filter other types of arrays, like real
arrays.
Generic filtering
As mentioned before, Fortran doesn’t have support for generics, which would allow you to use some kind of “placeholder” for types (e.g. [value_type]
):
pure function filter(values, filter_func) result(res)
[value_type], dimension(:), intent(in) :: values
interface
pure function filter_func(value) result(keep)
implicit none(type, external)
[value_type], intent(in) :: value
logical :: keep
end function filter_func
end interface
[value_type], dimension(:), allocatable :: res
integer :: i
res = pack(values, [(filter_func(values(i)), i=1, size(values))])
end function filter
You could then use filter
on an array with any type of value, and the filter_func
’s value
argument would have to be of that same type, and the return type would be of that type too.
If we want to replicate this behavior, we have to hard-code it. The solution is to redefine the same procedure for any type we want to support. For example, if we want to filter real
s with a filter function, we have to replicate the existing function, using real
instead of integer
where applicable:
pure function filter_reals(values, filter_func) result(res)
real, dimension(:), intent(in) :: values
interface
pure function filter_func(value) result(keep)
implicit none(type, external)
real, intent(in) :: value
logical :: keep
end function filter_func
end interface
real, dimension(:), allocatable :: res
integer :: i
res = pack(values, [(filter_func(values(i)), i=1, size(values))])
end function filter_reals
A function that matches the real
-based filter_func
interface
is the function would_be_rounded_up
:
pure function would_be_rounded_up(value) result(keep)
real, intent(in) :: value
logical :: keep
keep = value - int(value) >= 0.5
end function would_be_rounded_up
So once we have this filter_reals
function inplace, it can be used as follows:
filter_reals([1.3, 2.5, 3.7, 4.0], would_be_rounded_up)
Note that intrinsic types like integer
and real
have special kind
types. For numbers, the kind
of a variable indicates the number of bytes used to store their value, which also implies the minimum and maximum values you can store in them. Every combination of type and kind represents its own type. This means that if you want to support both real(kind=real64)
and real(kind=real32)
, you have to provide filter_*
functions for each of these types specifically. This is why libraries often have multiple copies of the same function, using different kind
s. It’s impossible to work around this by passing kind
as a function argument, because kind
needs to be a compile-time parameter and can’t be variable at runtime.
It’s not nice that we have to duplicate the code. Still, if it’s library code we are writing, this code will rarely be touched again, so it might not be that big a problem. Anyway, we can at least improve the situation for users who want to call these functions but don’t want to find the specific function they need to call. The solution is to define an interface
for them. We saw this kind of interface
before, when we had multiple alternative factory functions for point_t
. We can do a similar thing for type-specific filter_*
functions and subsume them all under a generic filter
function (we first have to rename the existing filter
function to filter_integers
, so the names don’t clash):
module filtering
implicit none(type, external)
private
public :: filter
! ...
interface filter
procedure :: filter_integers, filter_reals
end interface filter
contains
pure function filter_integers(values, filter_func) result(res)
! ...
end function filter_integers
pure function filter_reals(values, filter_func) result(res)
! ...
end function filter_reals
! ...
end module filtering
Note that we only have to export the interface
filter
. Whenever a user calls this function, the compiler will find the right module procedure (based on the argument and return types) and will call it:
! Will actually call `filter_integers`
filter([1, 2, 3, 4], is_even)
! Will actually call `filter_reals`
filter([1.3, 2.5, 3.7, 4.0], would_be_rounded_up)
We have successfully generalized filter
, without actual compiler support for generics. We just provide alternatives for the function filter
.
Now, what if we want to write something like this?
filter([1, 2, 3, 4], is_divisible_by(3))
The function is_divisible_by
should return another function, one that matches the filter_func
interface. Is this possible? Can we return a procedure from a procedure and pass it along as an argument?
In Fortran, not natively… But we’ll find a solution for this problem anyway, in the next post.