Thank you, in my attempt, I made several errors, especially with the instantiate
statements or forgot to include them altogether. So the above is most useful. A lot to digest.
While playing with the example of the original post, I got wrong results. I don’t know if it’s a bug of a wrong usage of the template:
module template_add_m
implicit none
private
public :: add_t
requirement R(T)
type :: T; end type
end requirement
template add_t(T)
requires R(T)
private
public :: add_generic
contains
function add_generic(x, y, z) result(s)
type(T), intent(in) :: x, y, z
type(T) :: s
print*, "x, y, z, x+y+z =", x, y, z, x+y+z
s = x + y + z
end function
end template
contains
subroutine test_template()
real :: a
integer :: n
instantiate add_t(real), only: add_real => add_generic
a = add_real(5.1, 7.2, 10.0)
print*, "The result is ", a
instantiate add_t(integer), only: add_integer => add_generic
n = add_integer(5, 9, 10)
print*, "The result is ", n
end subroutine
end module
program template_add
use template_add_m
implicit none
call test_template()
end program template_add
And output from the online lfortran compiler:
x, y, z, x+y+z = 5.09999990 7.19999980 10.000000000 10.000000000
The result is 10.000000000
x, y, z, x+y+z = 5 9 10 10
The result is 10
The sum is always equal to the last variable that appears in the sum
I would expect something similar to what is available for polymorphic objects:
template algorithm_tmpl(T)
requires R(T)
contains
subroutine algorithm(arg1, arg2, ...)
type(T) :: arg1, ...
...
...
select type (arg1)
type is (real)
...
type is (complex)
...
end select
...
...
end subroutine
end template
If you want a generic mean
function of a 1-D array argument to return the same type as the argument for a real argument of any kind and real(kind=kind(1.0d0)) for an integer argument of any kind, is that something a generics feature should be able to handle, or will you need separate mean functions for real and integer arguments?
It should be a basic design requirement for Generics in Fortran i.e.,what is really enhanced Generics.
That is because Fortran has long had the facilities for generic interfaces and the idea for Generics must be build on it by offering the same PLUS offer additional facilities for generic subprograms and generic (derived) types.
So with the existing generic interfaces, it is readily viable, the problem being code duplication - see below. Thus the goal of the Generics feature must be to minimize this code duplication.
module mean_m
integer, parameter :: SP = kind(1.0)
integer, parameter :: DP = kind(1D0)
generic :: mean => mean_sp, mean_dp, mean_int
contains
function mean_sp( a ) result(r)
real(SP), intent(in) :: a(:)
real(SP) :: r
r = real(sum(a),kind=kind(r)) / size(a)
end function
function mean_dp( a ) result(r)
real(DP), intent(in) :: a(:)
real(DP) :: r
r = real(sum(a),kind=kind(r)) / size(a)
end function
function mean_int( a ) result(r)
integer, intent(in) :: a(:)
real(DP) :: r
r = real(sum(a),kind=kind(r)) / size(a)
end function
end module
use mean_m
print *, mean( [ 1.0_sp, 2.0_sp, 3.0_sp ] )
print *, mean( [ 1.0_dp, 2.0_dp, 3.0_dp ] )
print *, mean( [ 1, 2, 3 ] )
end
C:\temp>ifort /standard-semantics /free p.f
Intel(R) Fortran Intel(R) 64 Compiler Classic for applications running on Intel(R) 64, Version 2021.9.0 Build 20230302_000000
Copyright (C) 1985-2023 Intel Corporation. All rights reserved.
Microsoft (R) Incremental Linker Version 14.34.31937.0
Copyright (C) Microsoft Corporation. All rights reserved.
-out:p.exe
-subsystem:console
p.obj
C:\temp>p.exe
2.000000
2.00000000000000
2.00000000000000
Thanks @PierU! I reported it here: Generics, wrong sum · Issue #1823 · lfortran/lfortran · GitHub. We need to fix it.
The template is invalid. You have not provided a function that can add two variables of type(T)
together. LFortran just isn’t doing that level of type-checking on the template (yet). The correct template would be
requirement R(T, F,as_string)
type :: T; end type
function F(X, Y) result(Z)
type(T), intent(in) :: X, Y
type(T) :: Z
end function
function as_string(x) result(string)
type(T), intent(in) :: x
character(len=:), allocatable :: string
end function
end requirement
template add_t(T, F, as_string)
requires R(T, F, as_string)
private
public :: add_generic
contains
function add_generic(x, y, z) result(s)
type(T), intent(in) :: x, y, z
type(T) :: s
s = F(F(x,y),z)
print*, "x, y, z, F(F(x,y),z) =", as_string(x), as_string(y), as_string(z), as_string(s)
end function
end template
Is it just a limitation of the current implementation in LFortran? I hope that the template proposal does not require providing a function for such a simple case…
The templates must be verifiable on their own, without consideration for particular instantiations. I.e. what happens if I tried to do?
type :: my_t
end type
instantiate add_t(T)
One of the explicit design goals was avoiding the terrible error messages of C++ templates. This is done by using “strong concepts” (i.e. type checking of the template itself). The good news is that (aside from the as_string function), you can probably reuse a lot of intrinsics. I.e.
instantiate add_t(integer, operator(+))
I’m a bit scared about what this is implying… If I want to template a long routine with many computations, which I want it to work with all possible real kinds, I have to provide in the template “arguments” all the operations and functions used in the routine?
Thanks @everythingfunctional. Yes, LFortran is not great at the error messages yet, first we are focusing on valid code. Once we are in beta and most valid code works, we’ll get all the compiler error messages in place.
Internally in the compiler, yes. I think this follows from the “strong concepts” idea.
In the Fortran source code (at the “surface level”), the compiler can in principle do all kinds of automatic inference and “syntactic sugar” (currently you specify it by hand, but many of these things could be relaxed). The reason we prototyped it is exactly so that you can test it out, provide feedback (as you did), and think about it, and then let’s iterate on the design to improve it.
I think the compiler must be able to infer all the operations and functions used, so that it can “strongly” check the templated code. Assuming you agree with this, then the only question left is what things the user provides and what things get inferred.
Ref: the current paper on syntax for Generics viz. https://j3-fortran.org/doc/year/23/23-155r1.txt
Consider the example provided:
MODULE A
REQUIREMENT R(T,F)
TYPE, DEFERRED :: T
FUNCTION F(x, i) RESULT(y)
TYPE(T) :: y
TYPE(T), INTENT(IN) :: x
INTEGER, INTENT(IN) :: i
END FUNCTION F
END REQUIREMENT R
TEMPLATE B(T,F,C)
REQUIRES R(T,F) ! provides interface for deferred F
TYPE, DEFERRED :: T ! redundant decl of deferred type T
INTEGER, CONSTANT :: C(..) ! deferred rank constant
CONTAINS
SUBROUTINE SUB1(x)
TYPE(T), INTENT(INOUT) :: x
x = F(x, SUM(C))
END SUBROUTINE SUB1
SUBROUTINE SUB2(x)
TYPE(T), INTENT(INOUT) :: x
x = F(x, MAXVAL(C))
END SUBROUTINE SUB2
END TEMPLATE B
END MODULE A
MODULE B
USE MODULE A
INSTANTIATE B(REAL, OPERATOR(*), [3,4]), ONLY: &
& tot_sub1 => sub1
INSTANTIATE B(REAL, OPERATOR(+), [3,4]), ONLY: & ! different instance
& max_sub1 => sub2
CONTAINS
SUBROUTINE DO_SOMETHING(x)
REAL, INTENT(INOUT) :: x
x = 2.
CALL tot_sub(x)
PRINT*,'TOT: ', x ! expect 2. * (3+4) = 14.
x = 3.
CALL max_sub(x)
PRINT*,'MAX: ', x ! expect 3. + max(3,4) = 7.
END SUBROUTINE DO_SOMETHING
END MODULE B
This really feels strong concepts
gone haywire. And a complete departure from the initial premise laid for the design which was semantics via substitution in this paper.
Say one has already authored or has to author a subroutine in Fortran to compute a quantity
y = x*{\sum}_{i=1}^nc, why reinvent anything other than y = x*sum(c)
and end up at x = F(x, SUM(C))
. And when the standard states the multiplication operator *
is stipulated for the intrinsic types of REAL
and INTEGER
, to obfuscate the operation with a generic F
makes no sense.
For the example shown, an option with the current standard is
module ops_m
generic :: tot_sub => tot_sub_real ! and other specific implementations
generic :: max_sub => max_sub_real ! and other specific implementations
contains
subroutine tot_sub_real( x, c )
real, intent(inout) :: x
real, intent(in) :: c(:)
x = x * sum(c)
end subroutine
subroutine max_sub_real( x, c )
real, intent(inout) :: x
real, intent(in) :: c(:)
x = x + maxval(c, dim=1)
end subroutine
end module
For such an example, using the semantics via substitution to minimize code duplication while adhering to strong concepts, one simply needs to inform the processor the template involves a generic type that supports the three operations of addition, multiplication, and comparison. That is it. Anything more than that is an absolute overkill. Notionally, one might illustrate this pseudosyntax like so:
module ops_m
template, object :: T1
type => numeric_type !<-- look in the standard for SUM intrinsic
kind => * !<-- notional syntax to convey any kind for the stated types
end template
template, object :: T2
type => < real, integer > !<-- look in the standard for MAXVAL intrinsic
kind => * !<-- notional syntax to convey any kind for the stated types
end template
contains
subroutine tot_sub<T1>( x, c )
<T1>, intent(inout) :: x
<T1>, intent(in) :: c(:)
x = x * sum( c )
end subroutine
subroutine max_sub<T2>( x, c )
<T2>, intent(inout) :: x
<T2>, intent(in) :: c(:)
x = x + maxval( c, dim=1 )
end subroutine
end module
Now something like along such lines will follow the semantics via substitution principle and allow a practitioner for take existing programs that work for one or a few types and genericize them easily while cutting down on verbosity by reducing code duplication and avoiding unnecessary complications by first writing too broad a template which then needs to be specialized later for actual use.
Yes, I think your last example has all the information for the compiler to use “strong concepts” and fits the current design (in my mind at least). I think we need something like “numeric_type” requirement, and then you could use *
and +
.
Thanks @FortranFan, I would like to explore such simpler syntax for each example / test case.
No, because then the only template parameter would be a kind. I.e.
template axpy_tmpl(K)
integer, constant :: K ! we're considering `requires valid_real_kind(K)`
generics axpy => axpy_ ! We're still considering if this is necessary
contains
subroutine axpy_(a, x, y)
real(K), intent(in) :: a, x(:)
real(K), intent(inout) :: y(:)
y = a*x + y
end subroutine
end template
OK, good… And what if I want it to work both real and complex (i.e. a template for saxpy/daxpy/caxpy/zaxpy)? Looks like in this case all operations/functions must be template arguments, is that correct?
Correct. The axpy
example ends up looking like
requirement bin_op(T, op)
type, deferred :: T
elemental function op(a, b)
type(T), intent(in) :: a, b
type(T) :: op
end function
end requirement
template axpy_tmpl(T, plus, times)
private
public :: axpy
requires bin_op(T, plus)
requires bin_op(T, times)
interface axpy
procedure axpy_
end interface
contains
subroutine axpy_(a, x, y)
type(T), intent(in) :: a
type(T), intent(in) :: x(:)
type(T), intent(inout) :: y(:)
y = plus(times(a, x), y)
end subroutine
end template
I’ve given a presentation on this example that you can watch here
This is near exactly how I wish generics to be implemented. Why is there so much effort being spent to make the first iteration more than simple substitution, which would serve the majority of Fortran programmer needs?
Because simple substitution is not backwards compatible with “strong concepts”. C++ is the poster child for why just doing “simple substitution” ends up being a bad thing.
The examples of “current proposal” are beyond verbose, and seem to require multiple definitions linked together by the programmer, in different places. What is wrong with the suggestion by @FortranFan to define template types, indicating variable type and kind requirements, then writing sub programs as normal using those template types? You are saying this somehow lacks “strong concepts,” what information is missing for the compiler to correctly compile, or provide useful error messages, in this case?