Skip to content

Variadic functions #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jacobwilliams opened this issue Nov 9, 2019 · 24 comments
Open

Variadic functions #76

jacobwilliams opened this issue Nov 9, 2019 · 24 comments
Labels
Clause 15 Standard Clause 15: Procedures

Comments

@jacobwilliams
Copy link

Has this even been discussed? Some other languages have it. Why not Fortran?

@milancurcic
Copy link
Member

Can you describe what this is? I've been programming since 2006 and never heard of these until now. I understand now because I Googled it, but we shouldn't have to Google stuff proposed here. An example would go a long way.

Now for my view on this idea: I use it occasionally in Python via *args and **kwargs, but can't say I ever wished for it in Fortran (not an argument against though). I suspect that this would significantly complicate compiler implementation, although I don't know as I never programmed compilers. Curious to hear more experienced people's opinions here.

@jacobwilliams
Copy link
Author

jacobwilliams commented Nov 9, 2019

If I'm using the term correctly, I just meant a function that has an unknown number of arguments. It's not entirely unknown to Fortran, since the intrinsic max or min routines behave like this. You can call it like:

a = max(1,2)
a = max(1,2,3)
a = max(1,2,3,4,.....etc)

But, we have no ability to write routines like this ourselves.

@milancurcic
Copy link
Member

Interesting, I had no idea I could use min and max with more than 2 arguments.

@certik
Copy link
Member

certik commented Nov 9, 2019

I didn't know that either.

@klausler
Copy link

The use of MIN/MAX with more than two arguments goes all the way back to the original Fortran compiler for the IBM 704, although the names of the functions were different.

@gronki
Copy link

gronki commented Nov 10, 2019

For example, function HYPOT(x,y) has just two arguments while it could have more. By the way, does any know what was the reason for introducing this as intrinsic function in F2008? Do some architectures provide hardware implementation for this?

As for variadic functions, Python handles such arguments to the function as list. For example:

def hypot(*p): 
    import math 
    return math.sqrt(sum([x**2 for x in p]))
print(hypot(1))
print(hypot(3,4))
print(hypot(1,4,8))

I am guessing in Fortran the issue would be how to implement that without the performance hit of unnecesary construction of a temporary array.

@gronki
Copy link

gronki commented Nov 11, 2019

Ok, a quick google search revealed the answer. Please ignore the first paragraph of my last post. :)

https://en.wikipedia.org/wiki/Hypot

@aradi
Copy link
Contributor

aradi commented Nov 11, 2019

Maybe useful, indeed, but the interaction of those arguments with the usual optional ones may be non-trivial. Does one allow for both, convention optional arguments and the variadic ones in the same subroutine? How does it work with named argument in subroutine calls, e.g. call nonvariadic(b=1, c=2, d=3, a=1) is completely valid for a normal subroutine with positional or optional dummy arguments, but I can't see, how something like this would work with variadic routines.

Some cases can be handled by using arrays instead, like hypot([1.,0, 2.0, 3.0, 4.0]). Actually, since the introduction of the norm2() function, it is already part the language.

Is there a showcase, where anyone can demonstrate the necessity of variadic routines and show, that it would be impossible or very inconvenient to implement the same functionality using arrays and/or optional arguments? Otherwise, it would be quite difficult to argue for a non-trivial language extension.

@LKedward
Copy link

There have been several times that this would have been useful for me, I have code at the moment that looks like:

class(*), intent(in), optional, target :: a1,a2,a3,a4,a5,a6,a7,a8,a9,a10
...
if (present(a1)) then
  call someFunction(i0+0,a1)
end if
if (present(a2)) then
  call someFunction(i0+1,a2)
end if
if (present(a3)) then
  call someFunction(i0+2,a3)
end if
et cetera...

Not impossible to implement, but inconvenient and limiting, since there is necessarily a finite number of optionals.
Since variadics generalise the concept of optional arguments, their use would presumably be mutually exclusive - it makes no sense to use both simultaneously so don't allow it - and there won't be any ambiguity in the interface.
Finally since the intrinsics support it, it would be nice to be able to use it in our own functions and subroutines.

@aradi
Copy link
Contributor

aradi commented Nov 11, 2019

OK, I see. Further question would be, how one ensures type safety? Does one request all variadic arguments to have the same type/rank? Or should a routine iterating over the arguments return type(*)/rank(..) objects? Latter would be more flexible, but then type and rank checking would be shifted to run-time instead of done at compile time. (And you would have to embed the evaluation of the arguments into select type and select rank constructs, which does not make it very convenient to use...)

@gronki
Copy link

gronki commented Nov 11, 2019 via email

@cmacmackin
Copy link

cmacmackin commented Nov 11, 2019

I've been thinking about this a bit and come up with an idea of how the syntax could work, loosely inspired by how C handles variadic arguments. A procedure without any optional arguments could have zero or one argument with the attribute variadic. There would be two additional intrinsic procedures defined, similar to those used for getting command line arguments: get_variadic_count and get_variadic_argument. The first of these would return the number of variadic arguments passed to the function. The second would make the argument specified as variadic point to the desired argument. Their interfaces would be as follows:

function get_variadic_count()
  integer :: get_variadic_count
end function

subroutine get_variadic_argument(number, value, status)
  integer, intent(in) :: number
  type(*), dimension(..), intent(out) :: value
  integer, intent(out), optional :: status
end subroutine

As an example, I'll show how these could be used for an implementation of max that works on rank-1 arrays:

function array_max(arrays)
  integer, dimension(:), intent(in), variadic :: arrays
  integer :: array_max
  integer :: i, j, n
  n = get_variadic_count()
  do i = 1, n
    call get_variadic_argument(i, arrays)
    if ( i == 1) array_max = arrays(1)
    do j = 1, size(arrays)
      if (arrays(j) > array_max) array_max = arrays(j)
    end do
  end do
end function array_max

The advantage of can see of this is that the variadic argument could support pass-by-reference, with the compiler implementing it as a pointer in the background. It would avoid the need to copy the individual arguments into a new array. It would allow enforcing all of the arguments to be of the same type/kind/rank but not necessarily the same size. There would also be nothing to stop the variadic argument being declared as unlimited polymorphic, assumed type, or assumed rank, in which case each argument would not necessarily need to be the same type/rank, but the usual type-guards would be needed before you could do anything interesting with the argument. This approach would also allow for all the usual attributes (intent, pointer, allocatable, etc.) to be applied to the variadic argument in a meaningful way.

@aradi
Copy link
Contributor

aradi commented Nov 11, 2019

@cmacmackin Just for clarification and make sure I understand your proposal correctly. Did you mean

if (arrays(j) > array_max) array_max = arrays(j)

on line 10 in the 2nd example?

@aradi
Copy link
Contributor

aradi commented Nov 11, 2019

@cmacmackin I like your approach a lot. Just a minor remark. The interface you specify won't work with your example, as the dummy argument type class(*) :: value won't match integer, dimension(:) :: arrays due to rank mismatch. It would have to be more general, being able to handle all different kind of ranks and types, including type(*) and class(*)

@cmacmackin
Copy link

Ah yes, thanks for catching those. I'll update the code fragments accordingly.

@milancurcic
Copy link
Member

The use of MIN/MAX with more than two arguments goes all the way back to the original Fortran compiler for the IBM 704, although the names of the functions were different.

62 years later, FORTRAN doesn't cease to impress me!

@aradi
Copy link
Contributor

aradi commented Nov 11, 2019

I think, the calling of such a routine with named arguments needs also some thoughts. Given the variadic routine

subroutine testvariadic(posarg1, posarg2, vararg)
  integer, intent(in) :: posarg1, posarg2
  integer, intent(in), variadic :: vararg
...
end subroutine testvariadic

which of the following calls should be valid:

call testvariadic(1, 2, 3, 4, 5, 6)   ! Trivially yes
call testvariadic(posarg2=2, posarg1=1, 3, 4, 5, 6)
call testvariadic(posarg1=1, 3, 4, 5, 6, posarg2=2)
call testvariadic(posarg1=1, 3, 4, 5, posarg2=2, 6)
call testvariadic(posarg2=2, posarg1=1, vararg=3, vararg=4, vararg=5, vararg=6)
etc.

@gronki
Copy link

gronki commented Nov 12, 2019

From what I understand, currently the keywords argument must come after all positional arguments. So I think vararg must be restricted that it always comes as a positional argument or things can get very very messy. I feel it might be actually quite difficult to design it in a good way that will not cause confusion.

@aradi
Copy link
Contributor

aradi commented Nov 12, 2019

Currently, having

  subroutine testoptional(a, b, c, d)
    integer, intent(in) :: a, b
    integer, optional, intent(in) :: c, d
  end subroutine testoptional

the calls

call testoptional(1, c=3, d=4, b=2)
call testoptional(b=2, c=3, d=4, a=1)

are valid. If varargs must be positional arguments, then basically this would invalidate keyword-like arguments in the call of such a routine. Unless, one allows for multiple occurancies of a given keyword if it belongs to the vararg:

subroutine testvariadic(posarg1, posarg2, vararg)
  integer, intent(in) :: posarg1, posarg2
  integer, intent(in), variadic :: vararg
end subroutine testvariadic

call testvariadic(1, vararg=3, vararg=4, vararg=5, posarg2=2)
call testvariadic(posarg2=2, vararg=3, vararg=4, vararg=5, posarg1=1)

I think, this would be the most compatible with the way how kewyord arguments work in Fortran now.

@cmacmackin
Copy link

Unless, one allows for multiple occurancies of a given keyword if it belongs to the vararg

I was thinking that would be the tidiest solution.

@FortranFan
Copy link
Member

FortranFan commented Nov 12, 2019

Keep in mind how the intrinsic procedures are set up:

From 16.9 Specifications of the standard intrinsic procedures of the standard:

27   16.9.125 MAX (A1, A2 [, A3, ...])                                                                              
28 1 Description. Maximum value.                                                                                    
29 2 Class. Elemental function.                                                                                     
30 3 Arguments. The arguments shall all have the same type which shall be integer, real, or character and they shall
31   all have the same kind type parameter.                                                                         
..                                                                                                                  

Note the following:

   print *, max( a1=1, a2=2, a42=3, a142=4 )
end

outputs 4.

Consistency with how intrinsic procedures are specified as being ELEMENTAL and being able to accept any number of dummy arguments of the same type such as MAX, MIN, .. work with argument names of A1, A2 [, A3, ...] might be the best option.

Now these intrinsic procedures only accept intrinsic types of course (usually integer, real, or character) but I think it'll be useful if such user procedures can accept dummy arguments which are derived types of the same declared type.

@ivan-pi
Copy link

ivan-pi commented Nov 15, 2019

This would have been useful for me in the past is when writing N different subroutines to print N different column vectors. With support for variadic arguments I imagine it would be possible to have a single subroutine call print_columns(filename,a,b,c,...) where a,b,c,... would be a variable number of arrays with the same dimension in rank one.

@aradi
Copy link
Contributor

aradi commented Nov 15, 2019

@FortranFan You are right, then we should allow for enumerating the varargs in the call to be similar to already existing intrinsic functions:

subroutine testvariadic(posarg1, posarg2, vararg)
  integer, intent(in) :: posarg1, posarg2
  integer, intent(in), variadic :: vararg
end subroutine testvariadic

call testvariadic(1, vararg1=3, vararg2=4, vararg129=5, posarg2=2)
call testvariadic(posarg2=2, vararg1=3, vararg2=4, vararg129=5, posarg1=1)

@jacobwilliams
Copy link
Author

FYI: here's another example where this would be handy: https://github.com/urbanjost/M_msg/blob/master/src/M_msg.f90

@certik certik added the Clause 15 Standard Clause 15: Procedures label Apr 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Clause 15 Standard Clause 15: Procedures
Projects
None yet
Development

No branches or pull requests

10 participants