-
Notifications
You must be signed in to change notification settings - Fork 8
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
Intrinsic type operators and genericity #21
Comments
I am having trouble understanding how an "operator" parameter adds value here. Can you put together a use case that demonstrares why this is better than other coding styles such as user defined operators? |
The OPERATOR(defined-operator) syntax is currently used in three ways:
I propose to extend that syntax to provide syntactic sugar in two mutually related generic contexts:
For the purposes of the above the intrinsic operators are considered functions that are (currently) only available under their operator names. Examples of generic instantiations where I would want to use a function of one or two parameters with operator syntax:
Examples of generic definitions where I would want to search among a list of operator names for the appropriate function would include any operators for the intrinsic types or derived types where the underlying name for the operator's function is private:
Note edited to change one use of parameters to arguments. |
For a use case to consider alternative syntaxes, let us follow Tom Clune's example and consider a generic where we want to use a plus operator of uniform type Tom asserts that the only way to do this and maintain "concepts" is to use interfaces defined by type bound procedures. This would have a syntax somewhat like the following.
which might be instantiated as
This has the advantage that the only argument you have to pass is a single type so it will work with the most restrictive definition of generics. It has as its main disadvantage that it is difficult to to make this work with intrinsic types as the instantiation type. Tom tries a number of approaches to try to make this work for intrinsics, but I am not happy with any of them. It has as an additional disadvantage that the only function that can be used for the As one alternative I suggest type attributes. With them the above example might be written:
which might be instantiated as
This has the following advantages:
It has the following disadvantages:
As a partial workaround, one might consider allowing procedures as generic parameters. Then Tom's example becomes:
which might be instantiated as
This has the following disadvantages:
As a generalization of allowing procedures as generic parameters, I suggested allowing operators as generic parameters. Then the example might be written:
which might be instantiated as
or as
which might be instantiated as
This has the following advantages over Tom's approach:
Its main disadvantage is that it requires a more flexible, and hence more complicated, generic definition. If you are going to allow operators as parameters, you are almost certainly going to want to allow procedures as parameters. I would argue that to represent ranks and dimensions you will also want to allow integers and integer vectors as generic parameters, so you will wind up with a definition significantly more complicated than that of, say, Java. |
@wclodius2 For your final example: generic module uses_plus(T_generic, operator(+))
type :: T_generic
end type
interface operator(+)
function plus(a,b) result(c)
type(T_generic):: c
type(T_generic), intent(in) :: a
type(T_generic), intent(in) :: b
end function plus
end interface how would this work with a derived type that has defined is |
The issue of type-bound vs non type-bound operators is only one aspect that I was exploring with the directory that started this discussion. Consider this case. I want to have a template that is a simple container that stores a single object whose type is defined by the type parameter T. And provide setters and getters. The template could look something like type :: container<T>
type(T), allocatable :: object
contains
procedure :: set
procedure :: get
end type
abstract interface
subroutine set(this, value)
class(container<T>), intent(out) ::this
type(T), intent(in) :: value
this%value = value
end subroutine set
subroutine get(this, value)
class(container<T>), intent(in) :: this
type(T), intent(out) :: value
end subroutine get
end interface Something like that ought to work for most intrinsics and simple derived types, but what if the type has a len type prarameter? Consider how we want the template to be instantiated when T is How is the compiler to "know" that we meant for T to be declared differently in the case of character (or indeed any type with a len type parameter? It is not impossible to do, but its at best an awkward aspect of all this. A similar concern is whether or not a generic type parameter can include rank. I.e, could I declare T in the above example to be a rank 3 integer array? Or only scalars? A rank 3 integer array is a different type than a scalar integer in most other languages. We can solve this one by allowing an additional integer template parameter to specify the length -- hopefully allowing a default of 0 so that we don't burden the most common case. None of these issues is likely to undermine an otherwise solid solution for generics. But I do want to watch for solutions which allow such aspects to be handled as seamlessly as possible. |
I now realize that I've really not even mentioned the primary reason that Magne asked me to explore this. Namely we don't want the user to have to "teach" the compiler what operations are supported by the compiler. There is some concern that the arithmetic operators on intrinsics are handled at such a low level that it is not trivial for the compilers to determine that a given operation is supported at the more abstract level of a template short of instantiating and compiling. With traits/requirements/constraints we want the compiler to have this higher level understanding of intrinsic operations. Probably will not play a role in how the language specs are written, but rather as a way to convince the compiler developers that this is straightforward. |
There are five "forms" of operators that must be dealt with:
My understanding of type-bound procedures is that it is not that there is no The distinction between Note edited to allow the viewer to recognize the code fragments as code. |
"How is the compiler to "know" that we meant for T to be declared differently in the case of character (or indeed any type with a len type parameter?" The only way I can see of doing this is through something similar to C++'s pattern matching.
Provided the matching can be kept to a single level, the resulting generic language should still be type checkable. |
"There is some concern that the arithmetic operators on intrinsics are handled at such a low level that it is not trivial for the compilers to determine that a given operation is supported at the more abstract level of a template short of instantiating and compiling." My strong suspicion is that compilers will reduce the template to an intermediate representation, and whether this intermediate representation is before or after "type checking" will have little to do with the properties of the intrinsic types, and more as to what intermediate representations are already used in the compiler and the perceived demands of the user community. The most obvious intermediate representation is the parse tree, and this will have essentially no "type checking", although using the parse tree will reduce the high lexical cost of processing Fortran in instantiating the template. However if preprocessing is used in the template file they may not be able to even reduce it to a parse tree prior to instantiation. |
In generics/theory/intrinsics/Readme.md we find the odd statements
"The purpose of this directory is to explore issues related to ensuring that intrinsic types are on an equal footing with user defined types.
The main concern has to do with "concepts" (ref C++). If Fortran generics provide a similar mechanism for restricting type-name parameters in a template, it would presumably be based upon the interfaces defined by type-bound procedures. But intrinsic types do not have type-bound procedures."
I say it is odd primarily because of the phrase "it would presumably be based upon the interfaces defined by type-bound procedures". While the early enforcement of type safety in generics, what C++ calls "strong concepts", in Fortran would have to be based upon interfaces, they do not have to be "type-bound" procedure interfaces. I can think of two other ways of specifying operator interfaces for type safety: either implicitly as a type attribute, or as separate generic parameters.
Ada in its generics can qualify the type parameters by what is essentially an attribute specifying the operations a type must support. These include assignment, equality operators, comparison operators, and numerical operators. In Fortran these could be specified by extending the type-attr-spec-list of the derived-type-stmt
derived-type-stmt is TYPE [ [ , type-attr-spec-list ] :: ] type-name [ ( type-param-name-list ) ]
to add, say, a required operators specifier, required-ops-spec
type-attr-spec is ABSTRACT
or access-spec
or BIND (C)
or EXTENDS ( parent-type-name )
or required-ops-spec
where required-ops-spec might be
required-ops-spec is BASIC
or EQUALITY
or COMPARABLE
or GROUP
or FIELD
or NUMERIC
or CONCATENABLE
with the following requirements on the operators supported by the type
==
,/=
==
,/=
,>
,>=
,<
,<=
+
,-
+
,-
,*
,/
+
,-
,*
,/
,**
//
The example of the Readme.md then becomes
The above approach has four weaknesses:
>
operator and the user wants to substitute the<
operator to sort in the opposite order;.operator.
.All four weaknesses can be addressed by allowing generics to take operators as parameters, say by using OPERATOR(defined-operator) as a parameter. Then the example becomes in Fortranesque pseudo-code
Then an instantiation might be
or if you want to substitute the
*
operator for the+
As operators are just symbols for functions with one or two arguments, the standard could even allow renaming using functions as the instantiation
The above is easily generalized to operators with non-uniform arguments
The text was updated successfully, but these errors were encountered: