aradi / fypp

Python powered Fortran preprocessor
http://fypp.readthedocs.io
BSD 2-Clause "Simplified" License
180 stars 30 forks source link

Variadic templates with fypp #21

Closed ivan-pi closed 2 years ago

ivan-pi commented 2 years ago

I attended a C++ course recently, where I learned about variadic templates. Here's an example of using C++17 to compute the maximum value:

template< typename T1, typename T2, typename... Ts >
constexpr auto max( T1 const& a, T2 const& b, Ts const&... args )
{
   const auto result = ( a < b ) ? b : a;

   if constexpr( sizeof...(Ts) > 0 ) {
      return max( result, args... );
   }
   else {
      return result;
   }
}

Today, I finally had time to try this with fypp:

#:def base_max(a,b)
max(${a}$,${b}$)
#:enddef

#:def varmax(a,b,*pos)
  #:set res = base_max(a,b)
  #:if len(pos) > 0
      #:set res = varmax(res,*pos)
      $:res
  #:else
      $:res
  #:endif
#:enddef

The input

@:varmax(a,b)
@:varmax(a,b,c)
@:varmax(a,b,c,d)

gets expanded as

max(a,b)
max(max(a,b),c)
max(max(max(a,b),c),d)

I was really impressed when this just worked straight out the box! Now of course the Fortran max intrinsic is already variadic and can be used directly as max(a,b,c,d), so this example makes only little sense.

The website fypp.readthedocs.io doesn't appear to contain the keywords recursive or variadic anywhere, so I thought I'd open an issue.

ivan-pi commented 2 years ago

Here's an example of adding multiple elements:

#:def base_add(a,b)
(${a}$ + ${b}$)
#:enddef

#:def add(a,b,*pos)
  #:set res = base_add(a,b)
  #:if len(pos) > 0
      #:set res = add(res,*pos)
      $:res
  #:else
      $:res
  #:endif
#:enddef

res = @{add(1,2,3,4)}@
aradi commented 2 years ago

@ivan-pi The variable number of arguments was actually documented, but the word variadic was not used, indeed. And the recursive call was not mentioned at all in the docs. I've fixed it now (cd222e2), thanks a lot for making me aware of it, and also for the nice example with the Horner scheme, which is now included (in a simplified form) in the docs.

ivan-pi commented 2 years ago

Thanks for documenting this. I must say the recursion can be tricky to get right in some cases. Inspired by the functional-fortran package I attempted to produce left and right "fold" macros that would work for arbitrary binary operators:

#:def binary_op(op,a,b)
(${a}$ ${op}$ ${b}$)
#:enddef

#:def foldl(op,start,*pos)
  #:if len(pos) > 0
    #:set res = foldl(op,binary_op(op,start,pos[0]),*pos[1:])
    $:res
  #:else
    $:start
  #:endif
#:enddef

#:def foldr(op,start,*pos)
  #:if len(pos) > 0
    #:set res = binary_op(op,start,&
      foldr(op,pos[0],*pos[1:]))
    $:res
  #:else
    $:start
  #:endif
#:enddef

@{foldl(.and.,a)}@
@{foldl(.and.,a,b)}@
@{foldl(.and.,a,b,c)}@
@{foldl(.and.,a,b,c,d)}@
@{foldl(.and.,a,b,c,d,e)}@

@{foldr(+,a)}@
@{foldr(+,a,b)}@
@{foldr(+,a,b,c)}@
@{foldr(+,a,b,c,d)}@
@{foldr(+,a,b,c,d,e)}@

For the right-fold I exceeded the maximal recursion depth dozens of times before finally succeeding by copying the code functional-fortran uses verbatim.

aradi commented 2 years ago

Cool example! However, one really has to decide, where to stop with preprocessing and let the compiler do the work instead. Unfortunately, fold-like operations are not necessary very efficient in imperative languages without lazy evaluation...

ivan-pi commented 2 years ago

Yes, I don't have actual use cases for these, but it's interesting to probe what can be done via preprocessing.