Prototype implementation of Fortran generics in LFortran

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.

1 Like

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

1 Like

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
1 Like

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.

2 Likes

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.

1 Like

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.

1 Like

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?

4 Likes