Fortran - Functional Programming Concepts: Filter
Matthias Noback
Once introduced to Fortran, we are left wondering what kind of language it really is. Traditionally, it’s been used as a procedural language, or a “Do this, do that” language as I’d like to call it. You can do some Object-Oriented programming with it, but it’s somewhat clunky, verbose, and not very “idiomatic”. (That’s not to say that you shouldn’t do it!) The same goes for Functional programming. If we’d like to write Fortran code that follows this paradigm, it becomes even more awkward. You’ll need several advanced techniques to make it work, and then there are still many limitations. The inability to chain function calls, the lack of tail call optimization and generic type-support, and the obstacles you have to conquer to get some kind of delayed execution; all of these sound like good reasons for not-even-trying…
Anyway, I still want to look for ways in which we can make Fortran code more “functional”. The reason is that functional programming has some very relevant concepts for the type of programs that are written in Fortran. Knowing and applying these concepts will definitely make Fortran code easier to maintain, since functional code can be much more expressive than procedural code. It’s also easier to unit-test functional code than procedural code.
There are many different functional programming styles and languages. In this series on Functional concepts in Fortran, I’m aiming for a kind of Scala-like functional code, because I have some experience with that language. I find Scala a modern functional language, that’s not too far outside the comfort zone for an object-oriented programmer like myself.
A functional way of thinking often involves a strong focus on functions and their argument and return types. A functional approach to solving a problem often involves breaking up the problem into smaller problems, solved by smaller functions, that can be connected to each other because their argument and return types are compatible. This results in something like a pipeline of functions, that eventually yield the wanted results. We’ll see how this works later, when we have collected the building blocks for this programming style.
Transforming lists with a filter function
Consider an array of integers. If we want only the even numbers from this list, we would normally loop over this list, and check for each value if it’s even. If it is, we add it to a new list, the result:
integer, dimension(:), intent(in) :: integers
integer, dimension(:), allocatable :: res
integer :: i
integer :: res_index
allocate (res(size(integers)))
res_index = 0
do i = 1, size(integers)
if (mod(integers(i), 2) == 0) then
res_index = res_index + 1
res(res_index) = integers(i)
end if
end do
res = res(:res_index)
There are some tricks we could do to make this more memory-efficient, like to make the integers
argument intent(inout)
, allowing the function to modify the original array. However, that may be quite surprising for a user of this function. We will talk about (im)mutability of data in another post.
Using intrinsic function pack
A smarter way to write this same logic is to delegate the handling of array allocation and looping to an intrinsic (built-in) function called pack
. With pack
you can select elements from an array based on a mask
variable. For every index of the original array, it will check if the corresponding element in mask
is .true.
. If it is, then it will put the value in the result array. This example shows how it works with a hard-coded array of integers and logicals (booleans):
integer, dimension(:), allocatable :: integers
logical, dimension(:), allocatable :: mask
integer, dimension(:), allocatable :: res
integers = [1, 2, 3, 4]
mask = [.false., .true., .false., .true.]
! `res` will be `[2, 4]`
res = pack(integers, mask)
Of course, we normally don’t use a hard-coded array. The solution should work for any array of integers. This means we have to populate that mask
variable dynamically, with the help of intrinsic function mod()
(modulo). To make it reusable, we put the code in a function, and give it an integers
argument, which is an array of integers of assumed-size (:)
:
pure function filter_even(integers) result(res)
integer, dimension(:), intent(in) :: integers
integer, dimension(:), allocatable :: res
logical, dimension(:), allocatable :: mask
integer :: i
allocate (mask(size(integers)))
do i = 1, size(integers)
mask(i) = mod(integers(i), 2) == 0
end do
res = pack(integers, mask)
end function filter_even
One trick to make this more elegant is to use an array comprehension to build the mask
variable. An array comprehension is an implicit do
loop, which will automatically allocate the resulting array as well:
integer :: i
-allocate (mask(size(integers)))
-do i = 1, size(integers)
- mask(i) = mod(integers(i), 2) == 0
-end do
+mask = [(mod(integers(i), 2) == 0, i = 1, size(integers))]
We can even use the array comprehension as an expression. Then there’s no need for a local mask
variable anymore:
pure function filter_even(integers) result(res)
integer, dimension(:), intent(in) :: integers
- logical, dimension(:), allocatable :: mask
integer :: i
- mask = [(mod(integers(i), 2) == 0, i=1, size(integers))]
- res = pack(integers, mask)
+ res = pack(integers, [(mod(integers(i), 2) == 0, i=1, size(integers))])
end function filter_even
Passing a function as an argument
It’s a nice and short function, but it’s quite specific: this filter_even
function can only be used to get the even numbers. What if we also want a filter function for getting the odd numbers…
First, we need to extract the modulo logic to its own function and call that inside the array comprehension:
pure function filter_even(integers) result(res)
integer, dimension(:), intent(in) :: integers
integer, dimension(:), allocatable :: res
integer :: i
- res = pack(integers, [(mod(integers(i), 2) == 0, i=1, size(integers))])
+ res = pack(integers, [(is_even(integers(i)), i=1, size(integers))])
end function filter_even
And the is_even
function:
pure function is_even(number) result(res)
integer, intent(in) :: number
logical :: res
res = mod(number, 2) == 0
end function is_even
Now we can generalize: rename filter_even
to plain filter
and pass the is_even
function as an argument to specify how the algorithm should decide which values to copy to the result array. To allow a procedure to be passed as an argument we have to specify what the interface of that procedure should be. In other words, what signature we expect this procedure to have. This includes the number of arguments, their types, and the return type. In our case, generalizing from is_even
: the function should have an integer
intent(in)
argument, and its return value should be a logical
, indicating whether the result array should contain the given integer
. We can define this interface
in the function filter
itself, as follows:
pure function filter(integers, filter_func) result(res)
integer, dimension(:), intent(in) :: integers
interface
pure function filter_func(number) result(keep)
implicit none(type, external)
integer, intent(in) :: number
logical :: keep
end function filter_func
end interface
integer, dimension(:), allocatable :: res
integer :: i
res = pack(integers, [(filter_func(integers(i)), i=1, size(integers))])
end function filter
There’s no separate argument declaration for filter_func
, like there is for the integers
argument. We only need to declare an interface with the same name as the argument (filter_func
).
We can now call the function, passing the name of the function that should be used as the filter_func
argument:
filter([1, 2, 3, 4], is_even)
Now this might be any function that matches the specification from the filter_func
interface. This is very powerful, and very expressive, much more than the original do
loop with an if
inside. And it’s easy to add new functions, like is_odd
:
pure function is_odd(number) result(res)
integer, intent(in) :: number
logical :: res
res = mod(number, 2) == 1
end function is_odd
And such a function may live anywhere in your code base, as long as it’s accessible at the place where it’s used:
filter([1, 2, 3, 4], is_odd)
If everybody in the team knows what “filtering” an array means, they will effortlessly understand what’s going on in this line of code. Besides, once people know the concept of filtering they’ll notice all the places in the code where filtering happens. This provides nice opportunities for refactoring that code to call filter
, providing a dedicated function with a domain-specific name.
General characteristics of a filter operation are:
- The new list has the same type of values as the old list
- The number of values in the new list may be as much, or less than the number of items in the old list (and could even be 0, but never more)
Can we also have a filter
function for real
s? Let’s find that out in the next post.