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 reals 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 kinds. 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.