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.