Programming with Generics in LFortran¶
In this page we will explore how to write generics in LFortran.
Prerequisite¶
Variables inside of a generic function are typed with a deferred types. For example, we may want to define a function that calculate the sum of a generic array which can take any number type. Using a deferred type T
, we may want to declare the following generic function:
function array_sum(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: r
r = set_to_zero()
do i = 1, n
! uses a function instead of + because T
! can be any type, even a derived type
r = add_element(arr(i), r)
end do
end function
As we can see here, we need to define the functions associated with the deferred type T
. To do so in LFortran, we use requirements to define both deferred types and their associated functions.
requirement number_type(T, add_element, set_to_zero)
type, deferred :: T
function add_element(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
end function
function set_to_zero() result(z)
type(T) :: z
end function
end requirement
Defining and Using a Generic Function¶
Having defined the necessary requirement to use the deferred type T
, we can proceed to define our generic array sum. LFortran provides two different ways to write and instantiate generic functions:
With a template
Without a template (simpler syntax)
With Template¶
Defining a generic function
A template works as a closure for a generic function. A template takes as parameters symbols that would replace the generic symbols inside a generic function.
template array_t(T, add_element, set_to_zero)
...
public :: array_sum
contains
function array_sum(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: r
integer :: n, i
n = size(arr)
r = set_to_zero(0)
if (n > 0) then
r = arr(1)
do i = 2, n
res = add_element(r, arr(i))
end do
end if
end function
end template
Then to connect the parameters with the functions defined in the requirement, we use a require
statement.
template array_t(T, add_element, set_to_zero)
require :: number_type(T, add_element, set_to_zero)
public :: array_sum
contains
function array_sum(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: r
integer :: n, i
n = size(arr)
r = set_to_zero(0)
if (n > 0) then
r = arr(1)
do i = 2, n
res = add_element(r, arr(i))
end do
end if
end function
end template
require :: number_type(T, add_element, set_to_zero)
sets the type signature for the parameters within the scope of the template. This makes it possible for the LFortran compiler to type check the computations associated with the deferred type T
.
A template can also contains multiple functions that may depend on each other.
template array_t(T, add_element, set_to_zero)
require :: number_type(T, add_element, set_to_zero)
public :: array_sum
contains
function array_sum(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: r
integer :: n, i
n = size(arr)
r = set_to_zero(0)
if (n > 0) then
r = arr(1)
do i = 2, n
res = add_element(r, arr(i))
end do
end if
end function
function array_avg(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: s
s = array_sum(arr)
...
end function
end template
Using a generic function
To use a generic function we first need to instantiate (replace) the generic symbols inside a template with symbols with concrete types. The instantiation is done through the instantiate
statement. For example, if we want to instantiate “array_sum” with integer types, the instantiation would be as follows:
instantiate array_t(integer, add_element_integer, set_to_zero_integer), &
only: array_sum_integer => array_sum
First we pass the concrete symbols to the template in the form of a template call array_t(integer, add_element_integer, set_to_zero_integer)
. We are replacing the deferred type T
with a concrete type integer
, add_element
with a function that computes addition between two integers add_element_integer
, and set_to_zero
with a function that returns a zero integer value set_to_zero_integer
. These functions would have to be defined prior to the instantiation for them to be passed as symbol arguments to a template:
function add_element_integer(x, y) result(z)
integer, intent(in) :: x, y
integer :: z
z = x + y
end function
function set_to_zero_integer() result(z)
integer :: z
z = 0
end function
After only:
we decide which function inside in the template we want to instantiate, in this case array_sum
, and give the new instantiated function a new name array_sum_integer
.
After instantiation, we can then use the instantiated function similar to ordinary functions:
integer :: arr(10), sum
sum = array_sum_integer(arr)
The main benefit of generics is reuse. We can have different instantiations for different types using the same template. For example we can instantiate both integer and real array_sum
:
! instantiation with integer type
instantiate array_t(integer, add_element_integer, set_to_zero_integer), &
only: array_sum_integer => array_sum
! instantiation with real type
instantiate array_t(real, add_element_real, set_to_zero_real), &
only: array_sum_real => array_sum
Also, because a template may contain multiple generic functions, a single instantiation can be used to instantiate multiple functions:
instantiate array_t(integer, add_element_integer, set_to_zero_integer), &
only: array_sum_integer => array_sum, array_avg_integer => array_avg
instantiate array_t(real, add_element_real, set_to_zero_real), &
only: array_sum_real => array_sum, array_avg_real => array_avg
Without Template¶
The template notation can be cumbersome for defining a single generic function. To alleviate this, LFortran also supports a simpler syntax for declaring generic functions without having to declare an enclosing template. For example, our running array_sum
example can be written as follows in the simpler syntax:
function generic_sum {T, add_element, set_to_zero} (arr) result(r)
require :: number_type(T, add_element, set_to_zero)
type(T), intent(in) :: arr(:)
type(T) :: r
integer :: n, i
n = size(arr)
r = set_to_zero(0)
if (n > 0) then
r = arr(1)
do i = 2, n
res = add_element(r, arr(i))
end do
end if
end function
The first difference is that the template parameters are now included as the function’s generic symbol parameters enclosed by braces {T, add_element, set_to_zero}
. Since we still need to identify these generic symbols a requirement, the require statement is moved into the generic function itself. The rest of the function is the same as the generic function inside the template.
This is merely a syntax sugar for the original templated function. Inside the compiler this generic function is treated as the following template:
template generic_sum(T, add_element, set_to_zero)
require :: number_type(T, add_element, set_to_zero)
public :: generic_sum
contains
function generic_sum(arr) result(r)
type(T), intent(in) :: arr(:)
type(T) :: r
integer :: n, i
n = size(arr)
r = set_to_zero(0)
if (n > 0) then
r = arr(1)
do i = 2, n
res = add_element(r, arr(i))
end do
end if
end function
end template
The instantiation is also made simpler by having the instantiation and function call together:
integer :: arr(10), sum
sum = array_sum{integer, add_element_integer, set_to_zero_integer}(arr)
Further Simplifying Instantiations¶
Passing operator instead of functions
So far to replace the generic addition add_element
we have used a concrete function add_element_integer
. To simplify this, it is possible to just pass operator(+)
without having to define a function separately:
instantiate array_t(integer, operator(+), set_to_zero_integer), &
only: array_sum_integer => array_sum
Skipping function instantiation names
Generic functions can also be instantiated without having to rename each function one-by-one. Suppose we want to instantiate every generic functions inside the template array_t
, we can shorten the instantiation into:
instantiate array_t(integer, operator(+), set_to_zero_integer)
Doing so would generate the function array_sum
and array_avg
without any renaming.
Generic Derived Types¶
LFortran also supports generic derived types. Let’s say we want a generic tuple. We can define a derived type for tuples as usual inside a template:
template derived_type_t(T)
! for brevity we have the deferred type
! declared directly inside the template
type, deferred :: T
public :: tuple
type :: tuple
type(T) :: fst
type(T) :: snd
end type
end template
We can also define generic functions accessing this generic tuple as:
template derived_type_t(T)
type, deferred :: T
public :: tuple
type :: tuple
type(T) :: fst
type(T) :: snd
end type
contains
function get_fst(p) result(r)
type(tuple), intent(in) :: p
type(t) :: r
r = p%fst
end function
function get_snd(p) result(r)
type(tuple), intent(in) :: p
type(t) :: r
r = p%snd
end function
end template
The instantiation for derived types are also similar to generic functions. If we want an integer tuple type and its functions, then we can instantiate derived_type_t
as:
instantiate derived_type_t(integer), only: &
tuple_int => tuple, get_fst_int => get_fst, get_snd_int => get_snd
See Also¶
Generics, for details about the generics implementation and its related ASR.