To: J3 J3/23-164
From: Aleksandar Donev, Courant Institute, New York University
Subject: Packaging long argument lists of templates
Date: 2023-June-09
1. Background
===============
This paper relates to one of the unresolved technical issues (UTIs) in the
current generic programming proposal.
We are all familiar with Fortran 77 procedures with very long lists of
arguments, and how error prone and tedious in typing it is to call them
correctly with the right number of arguments and in the right order. Cut
and paste are commonly used, but errors are very hard to catch since a
code may compile even if there are swapped arguments, for example, and the
error can then be cut and paste many times. Fortran 90 solves this problem
by allowing one to package multiple parameters into a derived type, which
acts as a container that can store multiple related variables/parameters.
Only a single scalar argument of that derived type needs to be passed by
the user. Default values for many parameters can be specified as
initializations of variables inside the derived type definition. This
further avoids using long lists of optional arguments.
2. The Problem
===============
The same situation that happens for procedure argument lists happens for
template arguments in INSTANTIATE statements in the current proposal for
generic programming. There is no mechanism to package a set of types and a
set of operations acting on those types into a unit that can then be
passed as an argument when instantiating templates. Such a unit has been
referred to by others as a signature (taken from Ada's rationale/textbook)
or witness.
I believe the lack of signatures/witnesses is a serious deficiency in the
current design, but one that is difficult to resolve. This paper proposes
to learn from Ada 2005/2012 and mimic what Ada does.
3. Rejected Solution: TBPs
===============
It is worth noting that the object-oriented features of F2003 allow a type
to be used as a mechanism to package a type along with operations as
type-bound procedures. This mechanism is not available in templates in the
current proposal since there is no way to use any OOP with deferred types.
However, there are some deficiencies with using TBPs to package procedures
acting on objects of a given set of types. Most importantly, every use of
a given template that relies on TBPs will require extending a specific
type, which will require writing wrapper types, but can be hard or
impossible to combine with existing type hierarchies since we do not have
multiple inheritance in Fortran. The solution I propose below can in some
sense be seen as an alternative to multiple inheritance, and is similar to
the way interfaces are used in Java or other languages as an alternative
to C++'s multiple inheritance.
4. Signatures in Ada 2005
===============
The Ada community was aware of this issue when they added generic
programming features to Ada 2005, and has already developed a solution. I
find this solution elegant and a good basis for developing our own
solution for Fortran. It is therefore worthwhile to first briefly discuss
signatures in Ada as per the Ada rationale.
In Ada, generic programming is done via generic packages, which can be
thought of as generic (parameterized) modules. In Ada, requirements,
templates, and signatures are all just (generic) packages, instead of
being three different features. This is very flexible but IMO makes the
code hard to read and understand, and I prefer how we made a distinction
between TEMPLATEs, REQUIREMENTs, and INSTANTIATIONs. I propose below to
add SATISFACTIONs to this list.
One of the examples in the Ada rationale is taken from scientific
computing and is very relevant for Fortran, so I will use it in this
paper. The problem is to construct a type corresponding to a complex
number, along with operations on arrays/vectors of complex numbers,
starting from existing implementations of real numbers (here I use the
intrinsic REAL types) and existing implementations of arrays/vectors of
real numbers.
The basic idea in Ada is that packages can be parameters of packages. This
way, one can pass to a generic complex vector package a package that
implements a generic complex type and a package that implements generic
real arrays, to produce an implementation of a generic complex array. In
the proposal below, this means that TEMPLATEs can have SATISFACTIONs as
arguments. Key to safety is to have a mechanism that ensures that the
signatures for the complex type and the real array are based on the same
real type. I mimic Ada's approach below to accomplish this.
5. Proposed Solution: SATISFACTIONs
===============
I propose that a new concept be introduced in Fortran 202X (instead of
waiting until Fortran 202Y) that mimics signatures/witnesses. Since
REQUIREMENTs are our key mechanism for specifying "concepts," i.e., for
specifying a set of relations between a set of deferred types and a set of
deferred constants and procedures, I propose we name the new unit a
SATISFACTION (of a requirement).
Instead of trying to write a detailed specification or syntax, I simply
provide an illustration inspired by the example in the Ada rationale.
There are some places where I was not completely certain of what the
current design allows. Writing this was a useful exercise to show some
limitations of the current design such as requiring to write a wrapper
around the intrinsic sum function, and the fact that we want requirements
to export/import specific procedures into generic interfaces in addition
to defining explicit interfaces for deferred procedures.
Let's begin by defining a REQUIREMENT specifying a complex type along with
some basic operations on it:
requirement ComplexNumber(R,C,I,construct,re,im,plus,...)
type, deferred :: R ! Type corresponding to a real number
type, deferred :: C ! Type corresponding to a complex number
! In Ada, one can add a lot of restrictions on the sort of type
! that can be passed at instantiation, for example:
! type R is digits <>;
! which obviates the need to write wrappers around
! intrinisic types corresponding to real numbers
! Imaginary unit
! In Fortran, as currently proposed,
! we cannot have deferred constants of type(R)
! Therefore, we have to make the imaginary unit a function:
function I()
type(R) :: I
end function
! In Ada one can write
! I:constant Complex:=(0.0,1.0);
! and the constants zero and one will be converted to
! the appropriate Digits type
! Create a complex number from real and imaginary pieces:
elemental function construct(re,im) result(cmplx)
type(R), intent(in) :: re,im
type(C) :: cmplx
end function
! Split a complex number into real and imaginary pieces:
elemental function re(cmplx)
type(C), intent(in) :: cmplx
type(R) :: re
end function
elemental function im(cmplx)
type(C), intent(in) :: cmplx
type(R) :: im
end function
! Operations on complex numbers, such as addition:
function plus(first,second)
type(C), intent(in) :: first,second
type(C) :: plus
end function
...
! New proposed feature
! It is important for a signature to be able to package generic
! interfaces along with types and procedures
interface operator(+)
procedure plus
end interface
end requirement
Now let us also create another REQUIREMENT specifying vectors of reals
along with operations on them:
requirement RealVector(R,add,...)
type, deferred :: R ! Type corresponding to a real number
...
function add(vec) result(sum)
type(R), dimension(..), intent(in) :: vec
type(R) :: sum
end function
end requirement
Now we want to create generic code for a vector of complex numbers, along
with operations such as add, by combining a signature for a complex number
with a signature for a real vector:
template ComplexVector(C,Complex,Vector)
type, deferred :: C ! Basic type corresponding to a complex number
! New proposed feature: satisfactions
satisfaction :: Complex=ComplexNumber(C=C)
! Syntax can use either = or =>
! The instantiation argument must be a satisfaction of the requirement
ComplexNumber where the type C is the same type
! as the type passed for the deferred type C when the template
ComplexVector is instantiated.
! New proposed feature: importing into namespace from satisfactions
import Complex
! The import means that the names R, C, construct, re, im, plus
! become available in the current scoping unit
! This has the same semantics as if we instead wrote:
! requires ComplexNumber(R, C, construct, re, im, plus, ...)
! Additionally, the function plus gets added to the generic
! operator(+) so we can use it in this template
! One can restrict what gets imported, for example
! import Complex, only : operator(.plus.)=>operator(+)
! will make + available for complex numbers as
! .plus. inside this template
! Note that the syntax could be
! use Complex
! which is how it is in Ada
! Note that this feature is key, without it,
! nested signatures would require
! typing long selection lists like
! ComplexVector%ComplexNumber%RealType etc.
! How horrendous this can be was made especially clear
! to me as I was rewriting
! the block matrix example to use signatures
satisfaction :: Vector=RealVector(R=R)
! A satisfaction of the requirement RealVector
! No import here so the name add is not available
! in the current scoping unit,
! and one must use Vector%add instead.
! The instantiation argument must be a satisfaction of the requirement
! RealVector where the type R is the same type
! as the type passed for the deferred type R
! when the satisfaction Complex is declared
! We could also have written:
! satisfaction :: Vector=>RealVector(R=Complex%R)
! If there were no import for Complex we would be required to write this
contains
function add(vec) result(sum)
type(C), dimension(..), intent(in) :: vec
type(C) :: sum
! This code is certainly not optimal but it is very simple to write:
sum = construct(re=Vector%sum(re(vec)), im=Vector%sum(im(vec)))
end function
end template
Now let me illustrate how to use this template. Unfortunately, the currend
design forces us to first write some boiler plate code...
module Complex_Vectors
use ... ! Assume the requirements and templates above are available
! Complex number based on modulus/phase representation
template PolarComplex(wp)
integer, constant :: wp
type :: PolarComplex(wp)
integer, kind :: wp
real(wp) :: r,theta
end type
contains
function I()
type(PolarComplex(wp)) :: I
I%r=1.0_wp
I%theta=asin(1.0_wp)
end function
! Create a complex number from real and imaginary pieces:
elemental function construct(re,im) result(cmplx)
real(wp), intent(in) :: re,im
type(PolarComplex(wp)) :: cmplx
cmplx%r=sqrt(re**2+im**2)
cmplx%theta=atan(im,re)
end function
! I omit the functions re, im, plus, etc. for brevity
end template
! The current design of the template feature forces us to write a
! wraper around the intrinsic function sum
template Wrappers(wp)
integer, constant :: wp
contains
function add(vec)
real(wp), dimension(..), intent(in) :: vec
real(wp) :: add
add = sum(vec) ! I hope this is legal in the current design?
end function
end template
integer, parameter :: sp=kind(0.0), dp=kind(0.0d0)
integer, paramater :: wp=dp ! Choose what precision we want to use
instantiate PolarComplex(wp)
instantiate Wrappers(wp)
satisfaction :: Complex=&
ComplexNumber(real(wp), PolarComplex, I, plus, ...)
satisfaction :: Vector=RealVector(real(wp), add, ...)
! Key to new feature: combine two packages together into a new one
instantiate :: CVector=ComplexVector(real(wp), Complex, Vector)
type(PolarComplex) :: array(10)=I()
write(*,*) add(array)
end program