Fortran - Functional Programming Concepts - Closures
Matthias Noback
We have a reusable filter
function to which we pass the name of a procedure that matches a given interface
. But can we also do something like this?
filter([1, 2, 3, 4], is_divisible_by(3))
Closures
This is a very common thing in functional programming. It involves something called higher-order functions. This means that a function may itself return a function. In this case, is_divisible_by
returns a function that can be passed to filter
. This returned function also needs to remember the value 3 that was passed to it. When it’s executed for each value in the integer array, it will check if that value is divisible by 3. In other languages such a function is called a closure; it’s a function that can be passed around as a value. Besides the function itself, it also has the data from the context where it was created - “bound” to it. In “ideal Fortran”, we would write something like this:
pure function is_divisible_by(divisor) result(res)
integer, intent(in) :: divisor
procedure(filter_func) :: res
res = pure function(value) result(keep)
integer, intent(in) :: value
logical :: keep
! We'd like to use `divisor` inside the created function
keep = mod(value, divisor) == 0
end function is_divisible_by
end function is_divisible_by
Unfortunately, Fortran doesn’t support this kind of closure or “function object”. Yes, we can return functions from a function using procedure pointers, but those functions are only statically “mentioned”; you can’t dynamically send data with a procedure pointer, pass a partial list of arguments, or something like that. This means that the divisor
will be lost, unless you store it somewhere outside the function, for instance in a module variable. But that’s bad design, and will definitely lead to strange application behavior.
What if the language doesn’t have built-in support for some cool feature we know from other languages? We have to build something ourselves. And that often involves extending the type system. In the case of closures, we can certainly do that in the same way we extended the type system before: using derived types. These naturally allow us to combine some state (in a data component) with a function (type-bound procedure). In the example of divisible_by
we would introduce a derived type divisble_by_t
:
public :: divisible_by_t
type :: divisible_by_t
! Store the divisor in a data component:
integer :: divisor
contains
procedure :: filter_func => divisible_by_filter_func
end type divisible_by_t
contains
pure function divisible_by_filter_func(self, value) result(keep)
class(divisible_by_t), intent(in) :: self
integer, intent(in) :: value
logical :: keep
! We have access to the divisor when filter_func() is invoked:
keep = mod(value, self%divisor) == 0
end function divisible_by_filter_func
When trying to use it like this:
filter([1, 2, 3, 4], divisible_by_t(3))
We get a compiler error:
error #6284: There is no matching specific function for this generic
function reference. [FILTER]
We are calling filter
, which is a generic function (defined with an interface
in the previous post). The compiler looked at the list of specific procedures (filter_integers
, filter_reals
) and couldn’t find a function that accepts a divisible_by_t
derived type as its second argument. That’s because we haven’t defined it yet. We have to add one more specific procedure to the filter
interface: filter_divisible_by
:
interface filter
procedure :: filter_integers, &
filter_reals, &
filter_divisible_by
end interface filter
contains
pure function filter_divisible_by(values, divisible_by) result(res)
integer, dimension(:), intent(in) :: values
class(divisible_by_t), intent(in) :: divisible_by
integer, dimension(:), allocatable :: res
integer :: i
res = pack(values, &
[(divisible_by%filter_func(values(i)), i=1, size(values))])
end function filter_divisible_by
The new function filter_divisible_by
accepts a divisible_by_t
instance and calls its filter_func
procedure, which has access to divisor
. This compiles, and now we can use it like this:
filter([1, 2, 3, 4], divisible_by_t(3))
This is very close to what we wanted, except for the _t
suffix (which is there to follow the previously suggested naming convention). If we are very annoyed by it, we can fix it with an interface
and a factory function, like we did before:
public :: divisible_by
interface divisible_by
procedure :: create_divisible_by
end interface
contains
pure function create_divisible_by(divisor) result(res)
integer, intent(in) :: divisor
type(divisible_by_t) :: res
res%divisor = divisor
end function create_divisible_by
At the cost of some duplication, we can now write:
filter([1, 2, 3, 4], divisible_by(3))
Actually, it’s not such a bad thing, because doing this allows us to keep divisible_by_t
private
to the module. Users only need the divisble_by
interface
, which we should make public
.
Making it generic, again
What if we want to make some other “closure” that remembers a value in order to perform the filtering, like is_greater_than
?
filter([3, 5, 7, 8], is_greater_than(6))
We can follow the exact same steps, and duplicate everything once more… introduce is_greater_than_t
, and a filter_is_greater_than
function, etc. We can save ourselves this trouble by introducing an abstraction for a derived type that can be used as a filter function:
type, abstract :: integer_filter_func_t
contains
procedure(integer_filter_func), deferred :: filter_func
end type integer_filter_func_t
interface
pure function integer_filter_func(self, value) result(keep)
import integer_filter_func_t
implicit none(type, external)
class(integer_filter_func_t), intent(in) :: self
integer, intent(in) :: value
logical :: keep
end function integer_filter_func
end interface
This really is “just” an abstraction of divisible_by_t
as we already have it. Now we can extend divisible_by_t
from this abstract type:
-type :: divisible_by_t
+type, extends(integer_filter_func_t) :: divisible_by_t
integer :: divisor
contains
procedure :: filter_func => divisible_by_filter_func
end type divisible_by_t
Finally, we can modify the filter function so it can be used with any integer_filter_func_t
subtype:
interface filter
procedure :: filter_integers, filter_reals, filter_integers_with_dt
end interface filter
contains
pure function filter_integers_with_dt(values, integer_filter) result(res)
integer, dimension(:), intent(in) :: values
class(integer_filter_func_t), intent(in) :: integer_filter
integer, dimension(:), allocatable :: res
integer :: i
res = pack(values, &
[(integer_filter%filter_func(values(i)), i=1, size(values))])
end function filter_integers_with_dt
Continuing with the previous example, we can now quickly implement it by defining a new type is_greater_than_t
, extending from the abstract integer_filter_func_t
.
type, extends(integer_filter_func_t) :: is_greater_than_t
integer :: compared_to
contains
procedure :: filter_func => is_greater_than_filter_func
end type is_greater_than_t
contains
pure function is_greater_than_filter_func(self, value) result(keep)
class(is_greater_than_t), intent(in) :: self
integer, intent(in) :: value
logical :: keep
keep = value > self%compared_to
end function is_greater_than_filter_func
We can use this new filter type as follows:
filter([3, 5, 7, 8], is_greater_than_t(6))
In the next post, we’ll look at another functional-style array transformation, called mapping.