-
Notifications
You must be signed in to change notification settings - Fork 16
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
Proposal for general-use templates #29
Comments
Interesting. I was thinking of something similar to your generic types and contracts actually. I took inspiration from the experimental |
Nice proposal Arjen! One comment: this would require a separate module for every type, rank and attribute combination, one would like to generate. Imagine, you would like to use a list with 5 different types and 3 different ranks each in your project. You would have to create 15 modules... Then, I would still resort to pre-processors offering branches (e.g. Fypp 😉 ) in order to make it automatic, so we would be still stuck in the dark area of pre-processors. 😄 Would it be possible to generalize the syntax in a way, that a user can generate the template for several different types within one module? |
@cmacmackin : I knew of NIM, never had a look at it though. I have now printed the document (being old-fashioned I do prefer reading from paper ;)). |
The nice thing about this kind of language feature is that you can easily experiment with it (via preprocessing of course) but still. |
Taking another look at this, something which I think is missing is the ability to define procedures which operate on two distinct types which satisfy the generic type. I suppose one way to approach this would be to simply define multiple generic types with the same contract but different names. That would work fine for types without any required components or type-bound procedures but would become repetitive for more complex generic types. Another issue is that this approach only allows you to define the components and type-bound procedures required for the generic type. You can't require any non-type-bound procedures to take it as an argument. Finally, any (non-type-bound) procedures would have to be renames if they are to share a scope with another instantiation of the template. I'd suggest some means of automatically making this happen and creating a generic interface for the procedures. |
When I wrote it, it seemed fairly straightforward - apart from a few bits and pieces like the "implicit" statement maybe ending up in the wrong place and complications regarding "type(data_type)" and plain "data_type". But you have uncovered another issue indeed. I would like to avoid unnecessary verbosity, as that would hinder the use of such a feature. Thanks, I will definitely revise my proposal :). |
Another question: how would this proposal interact with parameterised derived types? While I don't particularly like how those work, they are a part of the standard and we have to deal with them. Can your generic types be parameterised? We'd have to think through how that could work. Another issue which I see is that you are treating |
I think, we need a syntax which immediately allows the compiler to generate unique names via name mangling, so that user should not have to care more about it, as with normal generic routines. What do you think about something along the following:
|
I agree, something like that seems sensible (although we could debate whether the compiler should automatically generate the |
Valid questions and useful suggestions. My, there is a lot to think about, isn't there? How about this to avoid repetition: type, generic :: data_type "copy(...)" would be akin to "extends(...)" in syntax, not in semantics |
@cmacmackin Yes, indeed. However, one could even carry it further and allow for "generics" on the type level (although, probably, that is much more complex on the compiler side):
|
I've been thinking a bit about the Fortran type system. In a language like C++, a variable's type encapsulates the sort of data it stores (int, uint, string, etc., with each of these corresponding to a unique kind), whether it's a pointer, whether it's an array, etc. In Fortran, there are really four different aspects to the type system, each of which is somewhat independent:
Parameterised derived types take care of Templates/generic programming in a language like C++ are conceptually fairly straightforward. While the code is written in such a way that it does not state the exact type (which, as we said, contains the information on the kind, rank, len) of the data it is operating on, all of this information will be available at compile-time. I don't think anyone has adequately considered how templates in Fortran should deal with the fact that there are really (up to) four pieces of information they need to know about, rather than one like in C++. Worse, the way these pieces of information are currently handled by the compiler are not really consistent, with some needing to be known at compile-time and others put off until run-time under certain circumstances. My feeling is that we're going to need to step back somewhat if we are to consider how best to make generic programming work within the type-system as a whole. Perhaps we should think about introducing "assumed-kind" variables and a Let's say that derived types could then be parameterised on rank and that len parameters could then be a 1-D array: type :: foo(r, s)
integer, rank :: r = 2
integer, dimension(r), len :: s = [3, 5]
real, dimension(s) :: bar
end type foo (Note that it remains unclear as to exactly how we'd handle allocatable/pointer arrays of parameterized rank, in that case.) With those changes we could eliminate some of the inconsistency within the Fortran type system. At that point it starts to become easier to think about how we do the same for the |
Could you make a demonstration, how the swap-module above would look then alike? I think, swap is a pretty good example, as it covers many aspects of the generics while being super simple. |
My thoughts aren't quite concrete enough for that yet. I'll try to come up with some examples of what we could do though. |
Cool! I just have realized, that issue #4 has a very similar subject, maybe it would be worth to close this issue and continue discussion there? |
First thanks a big thanks to Ondrej and Zach for standing up this site. Its something some have advocated for a while and is long overdue. Also, thanks to Arjen. Your proposal looks like it might be a workable first step to templates and to me fits into current Fortran syntax etc. better than some of the other proposals I've seen. One idea that i had as a first step to a generic container was an extension of parameterized types to allow the syntax introduced in F2008 that allowed the type statement to define intrinisic variables to be allowed in the definition of the PDT but with an associated modification to the underlying KIND parameter facility to make it truly generic (syntax wise). Specifically, in F2008 you can do the following to define a double precision variable USE ISO_FORTRAN_ENV Ideally you would want to be able to just write Type(REAL64) :: areal Unfortunately, because the standard does not mandate that KIND parameters be unique in the sense that the values returned by the SELECTED_REAL, SELECTED_INT, and KIND functions along with the intrinsic parameters in ISO_FORTRAN_ENV are never the same value. Currently I think the majority of compilers will return 4 for both KIND(1) and KIND(1.0) plus INT64 and REAL64 are both 8. If the values were unique then for me parameterized types become a little more generic because I can do the following Type :: genval(listkind) without having to create a separate type for each intrinsic value. Real(8) :: areal However, I can't see any reason that the values returned by the various KIND setting functions and the intrinsic values in ISO_FORTAN_ENV can't be unique and still support the old Real(8) syntax. For the functions etc. just let the kind values returned for each intrinsic type live in a predefined range (ie. integers live in 100-199, reals in 200-299 etc). These modifications would help to address support for containers with intrinsic values but still leave open how you support user defined types in a similar manner. I would like to hear others ideas on how the compiler might do this. And has been pointed out, you still have the len parameter issue that prevents PDT's from being statically polymorphic which is what templates are really all about. |
Thanks for these comments. I am going to study this conversation in more detail. Hope to reformulate my proposal in a couple of days. |
@rweed Would it be possible to present a simple self containing example using your generalisation idea? For example, how would the swap-algorithm above look like when using your suggestion? (As stated above, the swap-algorithm contains many aspects of generic programming, while being very-simple.) I think whatever we come up with, it should work with intrinsic types, derived types (maybe even with parameterized ones) and classes as well, each of those with arbitrary ranks. Do you think, your approach can cover those cases? |
@arjenmarkus thanks for opening an issue for this. I posted here how to get involved with @tclune who is leading the effort: |
@aradi requested an example of what I'm proposing for modifying PDTs. First look at the following code that uses PDTs to define a node and list type for a circular-doubly-linked list of integers. This code compiles without errors with Intel 2019.5 ifort Program testpdt USE ISO_FORTRAN_ENV Implicit NONE Type :: CDLnode_t(ikind) Type :: CDLlist_t(ikind) Now assume that the modification to KINDS I'm proposing is in place. CDLnode_t now becomes Type :: CDLnode_t(ikind) This gives you an truly generic container for reals and ints. Support for character strings might take a little more thought. There is support for deferred length strings etc for PDTs but how to fit that into a truly generic framework is something we can discuss. The big issue is how do you support user defined types using just the kind parameters. There are obviously a lot of other issues that need to be addressed but I think what I'm proposing is a tiny first step that builds on current Fortran syntax and gives programmers another option besides using unlimited polymorphic variables (I've done this for lists, trees, hashmaps etc and its not pretty) or relying on preprocessor tricks to emulate templates. |
@tclune, in issue #30 you mentioned that "rank-agnostic array references" are likely to emerge in the 202X standard. This would likely be an important enabling feature for generic programming/templating, as it would allow the same code to work on any rank of array. I've been struggling a bit to think of decent syntax for generic rank variables (beyond what already exists for assumed-rank arrays, which aren't doing quite what we're looking for here given that their rank remains unknown at run-time). It might be useful if you could elaborate on what is being discussed for this new feature, as it could stimulate some ideas on this issue. |
Let's create a separate issue for "rank-agnostic array references" to track that feature (link to committee papers/proposals, etc.). |
I think Ada has a quite sophisticated generics system. I'd love it if some those concepts could be adopted in Fortran. See https://en.wikibooks.org/wiki/Ada_Programming/Generics. |
I proposed parameterized modules in 2004. The paper was 04-383r1. It's similar in spirit to Arjen's proposal, but uses the MODULE and USE statements. It provides what Magne Haveraaen asked for in Tokyo. |
For reference, I have been experimenting with templating using simple The vast majority of times I use templating it is for a generic interface. Essentially, a "module procedure" declaration could "call" a generic It would not by itself handle when conditionals are required but I Supporting this only in a module would allow for the strings to be public pubname=>name(tokens ...) would work as well. interface swap
module procedure swap('REAL','REAL32')
module procedure swap(type='REAL',kind='REAL64')
module procedure swap('INTEGER','INT8')
module procedure swap('INTEGER','INT16')
module procedure swap('INTEGER','INT32')
module procedure swap('INTEGER','INT64')
module procedure swap(type='logical',)
end interface
contains
! adding the TOKENS field indicates this procedure is
! only to be processed if referenced from an interface
! definition. Having a default specified by NAME='string'
! would be not just convenient, but document what strings
! are expected and allow for them to be called by name.
elemental subroutine swap(x,y) TOKENS(TYPE='integer',KIND='int32')
!@(#) M_sort::d_swap(3fp): swap two double variables
integer, parameter :: wp={KIND}
{TYPE}(kind={KIND}), intent(inout) :: x,y
{TYPE}(kind={KIND}) :: temp
{TYPE}(kind={KIND}) :: unused
temp = x; x = y; y = temp
! note an issue with using _wp here :
unused=10_wp
unused=10.0_wp
end subroutine swap If you allowed arrays to create permutations something like module procedure swap('INTEGER',['INT8','INT32','INT16','INT64']) would be nice, and some way to conditionally build only if a {type,kind} So that got me re-reading some of the links regarding templating. I was surprised how similiar some of the proposals were in many ways, I am wondering what the benefits are to placing templating straight into For example, perhaps templating might occur at run-time, or only include |
On Sun, 2022-05-08 at 09:53 -0700, urbanjost wrote:
I am wondering what the benefits are to placing templating straight
into
the language versus Fortran developing a standard pre-processor that
would
include templating?
This was Part 3 of the Fortran standard, which essentially nobody used,
and has been withdrawn.
It was also briefly part of the 2008 draft -- a subclause about a
built-in macro system -- which was also withdrawn.
The advantage of parameterized modules or templates is that the
processor understands them. One of the goals of the current design
effort is that there will be no syntax or semantic errors within an
instantiated paramaterized module or template if there are no syntax or
semantic errors in the parameterized module or template. This is
enforced with adequate declarations of the parameters and their
relationships, checked by the processor before instantiation, which is
not possible with a string-substitution, token-substitution, or macro
system.
|
Thanks. Not totally convinced a standardized preprocessor could not be tightly bound with the processor and provide the same checks, but then it essentially might as well be part of the language, I suppose. Thanks for the clear explanation. Indeed, I can use a preprocessor on a non-Fortran program and certainly generate incorrect code with the preprocessor being completely unaware of that; but it also allows applying information conditionally using information the templating proposals do not include, such as conditional coding depending on system or processor type, and so on. So the need for a preprocessor is reduced but not eliminated. For templating alone that is a big advantage though. Since I use preprocessing for other reasons (generating documentation, conditional code selection, recording date and time of compilation, .... it seemed like "well, let it do templating too, and keep the core language simpler"; but your explanation drives home a major advantage for the current approaches. Thanks again, I really enjoyed that explanation. |
If this is a goal, it is not possible to completely achieve it. Not all errors stem from interactions between actual parameters to instantiations of parameterized entities. I appreciate a desire to have better error messages than those that even good C++ compliers emit for errors in the semantics of template instantiations, but to insist that all errors can and will be detected and diagnosed prior to instantiation is not a feasible design goal. |
I can clarify the goals: the generics subgroup is currently going with templates with "restrictions" (called "strong concepts" in the C++ world). To understand the details what it means, you can read our comparison document here: It explains exactly in what sense errors will be caught and when. In short, you can indeed catch all errors before instantiating, but obviously you can only do it at the instantiation site when you know the user types, and you check them against the "restrictions / strong concepts", and if they pass, then you can instantiate without errors. See the document above for the details, and the kinds of error messages you can expect (showing actual error messages in Rust, Haskell for strong concepts and C++ for weak concepts). Indeed, C++ cannot catch all errors prior to instantiation (it only supports "weak concepts"), but Haskell, Rust and Go catch all errors prior to instantiation ("strong concepts"), and that is the goal for Fortran also. |
Yes, thanks, I know about concepts, and have read the design. Haskell doesn't really have concepts -- its typeclass constraints are really not the same thing. Standard ML's functors and signatures are a much closer match to the idea. Fortran mandates that a compiler enforce hundreds of constraints at compilation time. I guarantee you that I can write a test program that violates at least one of them in an instantiation without violating any concept on a template parameter. |
@klausler I think you are onto something. What you are saying is that if you write a templated function that has an argument Ok, here is an example:
Here the template |
Sure. Write a template subprogram that abstracts the kind of a REAL argument. In the subprogram, use the kind to deduce the next kind of REAL with higher precision -- e.g., given default real, determine double precision -- and declare a local variable with that kind. You won't be able to instantiate this subprogram for the kind of REAL with the most precision available in the implementation, only for those with less than the maximum. Perhaps you can devise a way for the programmer to encode a restriction on the kind of the template REAL kind, but detecting a failure to impose such a restriction would require instantiation. |
So something like this: subroutine bad_swap(x, y)
type(T), intent(inout) :: x
type(T) :: tmp
real(kind(x)+4) :: r
...
tmp = x
x = y
y = tmp
end subroutine The natural way in my mind how to make this work is that the compiler when compiling |
I'm not sure whether `kind(x)+4` would be portable across all compilers.
IIRC the last time I used Silverfrost FTN95 on Windows they use 1 and 2 for
single and double precision, instead of the more common 4 and 8.
…On Mon, May 9, 2022, 13:36 Ondřej Čertík ***@***.***> wrote:
So something like this:
subroutine bad_swap(x, y)
type(T), intent(inout) :: x
type(T) :: tmpreal(kind(x)+4) :: r
...
tmp = x
x = y
y = tmpend subroutine
The natural way in my mind how to make this work is that the compiler when
compiling bad_swap to some intermediate representation would figure out
the restriction on kind(x)+4 being a valid real kind (say either 4 or 8),
and then it would require the user to specify this restriction (somehow) in
the restriction for the template T. Whether this can be done in all cases
I don't know, but these are the kinds of things the generics subgroup is
trying to figure out.
—
Reply to this email directly, view it on GitHub
<#29 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AMERY5GWP7CJNTUMUUPWUPTVJFEIJANCNFSM4JCZN74A>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
Obviously that's unportable as written, but the issue remains even when the code is written in a portable manner -- say And don't focus on patching a single hole. The general problem is that the hundreds of compile-time constraints cannot all be checked without actually instantiating a templatized subprogram (or better, module, but those are infuriatingly excluded). I think this would become more obvious if J3 were to require a demonstrable prototype implementation before standardizing the feature. |
I don't know if this is true. If it is true however, then we cannot do "strong concepts", we can only do "weak concepts", almost for the same reason C++ is doing "weak concepts". That would be quite a big change in direction, so we should have a solid answer to this. Do you have another example of such constraints that would be hard to check without instantiating? Why can Rust and Go do it, but not Fortran? (Or Haskell, although you said it's slightly different there --- I don't know Haskell much). It seems there are constraints on kind, type and rank to consider. You are right that there are hundreds of potential compile-time constraints that the compiler must check. It seems to me the compiler would simply check every statement/expression/declaration in a templatized function against the restrictions/concepts.
Yes! It should be an absolute requirement for all features. @everythingfunctional and I sat down two months ago in Santa Fe and started working on a prototype in LFortran (https://gitlab.com/lfortran/lfortran/-/merge_requests/1664), but I need to focus on compiling actual projects with LFortran before I can spend more time on this. However, you might be further along with Flang, so you should consider if you can create a prototype. |
Prototyping J3's designs is J3's job, not mine. Constraints that are obvious concerns for a "no need to instantiate" design include C712, C714, C715, C718, C732-C734, C736, C737, C740, C748, C758, C764, C784, C785, C788, C791, C792, C798, C799, C7103, C7111, and that's just from a quick pass over subclause 7. Also, detecting ambiguous generic interfaces in templatized subprograms (or derived types in them) can't be done until the interfaces that depend on template arguments are instantiated. |
Not that I disagree, but which compiler should they use for prototyping? And should they really be expected to do it without help from someone on that compiler's development team?
Obviously the design of a single language feature can't prevent all possible bugs, but I think we can catch a lot of them. Catching any at time of processing a template is better than waiting till instantiation (IMO). No reason to suggest that if we can't be perfect there's no reason to do it at all. And not that there's no use for trying to do something like this, but I think it might be better to not allow at first, until we can be more deliberate about it. For example, I think the template you'd want is something like the following template foo(k)
integer :: k
require valid_but_not_largest_real_kind(k) ! We haven't even discussed this yet for value parameters
contains
function bar(x) result(y)
real(kind=k), intent(in) :: x
real(kind=k) :: y
integer, parameter :: wp = selected_real_kind(p=precision(x)+1)
real(kind=wp) :: tmp
! now we can do the math with higher precision
end function
end template We haven't thought about how we could specify restrictions on the values of template parameters, but I think before we allow their use in a context where that value matters to whether the code will even compile, we should think about that. |
On Mon, 2022-05-09 at 08:18 -0700, Ondřej Čertík wrote:
> but to insist that all errors can and will be detected and
> diagnosed prior to instantiation is not a feasible design goal.
I can clarify the goals: the generics subgroup is currently going
with templates with "restrictions" (called "strong concepts" in the
C++ world). To understand the details what it means, you can read our
comparison document here:
* https://github.com/j3-
fortran/generics/blob/52a9152da03b044a109aea1f9c08b3118f5db384/theor
y/comparison/comparison.md
It explains exactly in what sense errors will be caught and when. In
short, you can indeed catch all errors before instantiating, but
obviously you can only do it at the instantiation site when you know
the user types, and you check them against the "restrictions / strong
concepts", and if they pass, then you can instantiate without errors.
See the document above for the details, and the kinds of error
messages you can expect (showing actual error messages in Rust,
Haskell for strong concepts and C++ for weak concepts).
The important thing is that there will be no mysterious errors
announced WITHIN an instance when it is instantiated, if the template
is syntactically and semantically correct.
… —
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you commented.Message ID: <j3-
***@***.***>
|
Yes, all these would have to be checked.
We are all volunteers on J3. If you wanted to help us out, I would really appreciate it. :) |
And each of them is a constraint for which I could write a template example that can't be checked prior to instantiation against a specific set of template arguments.
I did try with DO CONCURRENT and was met with nothing but denial that there's even a problem. No thanks. |
Not trying to hijack this thread, but I submitted a GSoC proposal for |
Yes. DO CONCURRENT admits non-parallelizable usage that can't be detected at compilation time, and there's no standard way to promise to the compiler than a given DO CONCURRENT construct is free of such usage. |
I think that definitely would be a problem problem. I think the way forward is to implement an extension (say in Flang) that allows the user to promise to the compiler that a given construct is parallelizable, and then when we submit a paper with a prototype, I think we have a good chance to fix this. |
Here are a couple examples of templatized Fortran that I believe demonstrate violations that would require instantiation to detect. These are obviously not tested and the syntax is almost certainly incorrect.
|
I believe we have, or intend to, prohibit extending from template type parameters, at least for now. Also, we don't assume the accessibility of an intrinsic structure, let alone user defined, constructor for a template type parameter, so that usage isn't allowed either. The other example is something I'm not sure we've considered very strongly yet. We may desire preventing use of template type parameters as arguments in generic interfaces with multiple actual procedures. Not quite sure if that has all the right implications. This is something worth thinking about. |
On Tue, 2022-05-10 at 14:07 -0700, Peter Klausler wrote:
Here are a couple examples of templatized Fortran that I believe
demonstrate violations that would require instantiation to detect.
These are obviously not tested and the syntax is almost certainly
incorrect.
Even if the goal of preventing any constraint violations in an instance
of a template cannot be achieved, eliminating the vast majority of
possible situations using a parameterized module or template system is
better than catching none using a string-substitution preprocessor or a
built-in token-substitution macro system.
… template tmplt(ty)
type :: ty; end type
contains
subroutine subr(x)
type(ty), intent(in) :: x
generic :: gen => inner1, inner2
call gen(x) ! ambiguous if instantiated with ty = real
contains
subroutine inner1(x)
type(ty), intent(in) :: x
end subroutine
subroutine inner2(x)
real, intent(in) :: x
end subroutine
end subroutine
end template
template tmplt(ty)
type :: ty; end type
contains
subroutine sub(x)
type, extends(ty) :: ext ! error if ty is not a derived type
real :: comp ! error if ty already has a component named "comp"
end type
type(ty) :: v
v = ty() ! error if ty has type parameters or components without defaults
end subroutine
end template
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you commented.Message ID: <j3-
***@***.***>
|
Thanks Peter! The first one is similar to a template specialization. I can see multiple ways forward:
In the second example, the way I understand it, it would not compile, with an error message that you are using template tmplt(ty)
type :: ty; end type
allow_extend(ty)
type_params_and_components_have_defaults(ty)
contains
subroutine sub(x)
type, extends(ty) :: ext ! allowed in the requirements section
real :: comp ! -- multiple options here, see below
end type
type(ty) :: v
v = ty() ! now works, as it is allowed in the requirements section
end subroutine
end template The
That is the one and only component that is allowed (in the user type that will get passed in at call site), and it must be called My understanding of the general approach is that initially you get compiler error for most of the usages above (and indeed you can always determine ahead of time if a given user type can be instantiated or not). Then we can add more ways to specify "requirements", and then you can use the template in more cases, such as your examples above, while keeping the property that if the user type satisfies the requirements, then it can always be instantiated. |
It's a shame about the parameterized modules not being followed up on, though. Even if modules were parameterizable only by other modules (not types or anything else), you'd have an effective and highly composable solution, and the implementation would be easy to prototype and demonstrate. |
Generic features and the apparant lack of them in Fortran (up to the 2018 standard) are a widely discussed subject. I have written a note on the use of modules to achieve a (limted) form of genericity and that has inspired me to a new note which contains a concrete proposal for general-use templates. See the attachment.
Note: this note has been extended after discussion with Magne Haveraaen an Damian Rouson. I realise that the ideas may not have been formulated as clearly as required.
proposal_templates.pdf
The text was updated successfully, but these errors were encountered: