Fortran - Functional Programming Concepts - Map
Matthias Noback
We considered the problem of filtering elements in an array, keeping only the ones we want. A decision about that is made by a function we pass as an argument.
A similar problem, but with its own challenges, is when we have an array of values and want to create a new array, with an equal number of elements, but each element has been transformed in some way. For instance, say we have an array of integers and want to square each integer. A procedural approach looks as follows:
pure function square_all(integers) result(squared)
integer, dimension(:), intent(in) :: integers
integer, dimension(size(integers)) :: squared
integer :: i
do i = 1, size(integers)
squared(i) = integers(i)**2
end do
end function square_all
It’s used like this:
square_all([1, 2, 3, 4])
Some experienced Fortran programmers may be quick to point out that if you want to do something as simple as this, you don’t need a do loop…
Elemental functions
First, we need a function that transforms a single value from the array:
pure function square(number) result(squared)
integer, intent(in) :: number
integer :: squared
squared = number**2
end function square
We could use this function in our original do loop:
do i = 1, size(integers)
- squared(i) = integers(i)**2
+ squared(i) = square(integers(i))
end do
But now that we have the function square, we can rewrite the do loop as a single statement:
squared = square(integers)
This looks funny; squared is an integer array, but the return type of square() is just a single integer. We can make it work using a powerful Fortran feature, one that I haven’t come across in other languages (but I guess Fortran will not be the only language to have it): elemental procedures. By adding the elemental keyword to the procedure declaration, we can indicate that the function may be used on arrays of the argument and return types:
-pure function square(number) result(squared)
+pure elemental function square(number) result(squared)
integer, intent(in) :: number
integer :: squared
squared = number**2
end function square
Now the function can take an array of integers as an argument, and in that case it will return an array of integers, so this now works:
squared = square(integers)
It means we don’t even need a separate square_all function like we had before. Current users of square_all can just start using square.
A map function
What the elemental square function is, in functional terms, is a map function. It takes every element of an array and transforms it into some other value. The resulting array will have the same number of items, but different values.
What if we want to make the map operation explicit in our code, similar to how we created a generic filter function? We would also have a map function that can be used like this:
map([1, 2, 3, 4], square)
We already have the square function, so we can create a map function that will accept a function like that as an argument. This function itself can be elemental, so it will do the work for just one value, but can also be used on arrays of this value.
pure elemental function map(value, map_func) result(res)
integer, intent(in) :: value
integer :: res
interface
pure function map_func(old_value) result(new_value)
implicit none(type, external)
integer, intent(in) :: old_value
integer :: new_value
end function map_func
end interface
res = map_func(value)
end function map
This doesn’t compile:
error #7434: A dum-arg of a procedure with the ELEMENTAL prefix-spec
shall not be a dummy procedure. [MAP_FUNC]
For some reason we can’t pass a procedure to an elemental function. So we have to make map non-elemental, meaning it should accept an integer array as input:
-pure elemental function map(value, map_func) result(res)
- integer, intent(in) :: value
- integer :: res
+pure function map(values, map_func) result(res)
+ integer, dimension(:), intent(in) :: values
+ integer, dimension(size(values)) :: res
Should we re-introduce the do loop now? Actually, we can specify in the map_func that the passed procedure needs to be elemental instead:
interface
- pure function map_func(old_value) result(new_value)
+ pure elemental function map_func(old_value) result(new_value)
Knowing that map_func works on arrays as well as single values, the last line of the map function can simply be:
res = map_func(values)
This works now:
map([1, 2, 3, 4], square)
Mapping to other types of values
In the example of squaring integers, we map from integer to integer. Let’s consider what’s needed to map to another type of value, for instance from integer to real. An example is the elemental function half, that we’d like to use with map as well:
pure elemental function half(number) result(res)
integer, intent(in) :: number
real :: res
res = real(number)/2
end function half
Of course, this doesn’t work, because the map_func requires an integer argument and an integer return value, so we get a compiler error:
error #6633: The type of the actual argument differs from
the type of the dummy argument. [HALF]
Again, we don’t have support for generics, so we can’t write something like this:
pure function map_integer_to_integer(values, map_func) result(res)
[input_type], dimension(:), intent(in) :: values
[output_type], dimension(size(values)) :: res
interface
pure elemental function map_func(old_value) result(new_value)
implicit none(type, external)
[input_type], intent(in) :: old_value
[output_type] :: new_value
end function map_func
end interface
res = map_func(values)
end function map_integer_to_integer
Instead, for every type variation that we have in mind, we’d have to redefine this same function. For a real return type, the map function looks as follows:
pure function map_integer_to_real(values, map_func) result(res)
integer, dimension(:), intent(in) :: values
real, dimension(size(values)) :: res
interface
pure elemental function map_func(old_value) result(new_value)
implicit none(type, external)
integer, intent(in) :: old_value
real :: new_value
end function map_func
end interface
res = map_func(values)
end function map_integer_to_real
We already discussed generic interfaces and we can use one here as well. After renaming the existing map function to map_integer_to_integer, we can define a new generic interface map that points to the specific map_* we already have. We expose map publicly, so other modules and programs can use it:
public :: map
interface map
procedure :: map_integer_to_integer, map_integer_to_real
end interface map
contains
pure function map_integer_to_integer(values, map_func) result(res)
! ...
end function map_integer_to_integer
pure function map_integer_to_real(values, map_func) result(res)
! ...
end function map_integer_to_real
That’s great, now we can do:
map([1, 2, 3, 4], square)
map([1, 2, 3, 4], half)
The compiler will pick the right procedure for us, depending on how map is used.
Similarly, we could define map_real_to_*, with corresponding map_func interfaces defined inside those functions. We can even create a more advanced map that supports partially bound functions for map_func, in the same way that we added a filter procedure that accepts a derived type in the previous post.
Looking ahead
Using a dedicated map function requires more code, and even duplicate code, but it’s a very flexible setup, which comes with a great user experience. We can imagine how this approach gives us building blocks for more “complicated” transformations, like:
map(filter([1, 3, 5, 7]), divisible_by(3)), square)
Something we’re not able to do is write these function calls as a pipeline:
list([1, 3, 5, 7]) % filter(divisible_by(3)) % map(square)
That would be quite cool. It won’t be possible, because Fortran does not support “call chains” like this; it requires every intermediate result to be stored in a local variable. Accepting this fact, we can still replicate this syntax to some extent by introducing a list type. We’ll do that in the next post.