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}
  deferred type :: T
  deferred interface
    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 interface
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:

  1. With a template

  2. 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 must be accessible in the instantiating scope. They may be defined later in that scope’s contains section; their completed interfaces are checked against the template’s requirements before lowering. A contained procedure also takes precedence over a host-associated procedure or intrinsic of the same name:

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

The actual procedure’s declarations use its complete host specification part, including declarations after the instantiate statement. Instantiation does not change host association in an ordinary call to that procedure. Types introduced by the instantiation remain available to subsequent declarations and to the contained procedure’s local variables. The contained specific procedure may share its name with a generic interface; completing that declaration preserves the procedure selected by instantiation. A bare templated function or subroutine is not a concrete procedure actual. A nearer templated declaration is diagnosed at the actual argument rather than bypassed in favor of a host procedure or intrinsic of the same name. With --continue-compilation, a failed requirement is diagnosed without constructing the rejected instantiation’s procedure bodies; independent code is still checked.

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.

Renaming a function in an instantiation does not rename its local result variable. Without a result(...) clause, assignments to the original function name inside its body still set the result of the instantiated function. An explicit result variable also keeps its declared name.

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:

template 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.

For both template function and template subroutine, every name in braces must have a deferred declaration in the subprogram’s specification, directly or through require. A deferred integer constant is declared with deferred integer, parameter :: n. A host-associated constant does not declare a deferred argument. A type declaration such as integer :: n or integer, parameter :: n = 7 does not either: it is reported as an error that names the deferred spelling, as in a template construct.

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
    deferred type :: 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}
    deferred type :: 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

也可以看看

  • Generics, for details about the generics implementation and its related ASR.