-
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
Traits For Types #125
Comments
Yes!
This is precisely how we concluded templates should be implemented at the last J3 meeting. I think it's also called strong concepts in C++.
The advantage of the approach you described is that the compiler can provide good error messages and relatively quick compile times compared to the traditional C++ templates.
So I think this proposal should be pursued.
…On Sat, Dec 28, 2019, at 8:58 PM, Brad Richardson wrote:
Java calls them interfaces, Haskell calls them type classes, and Rust
calls them traits. But the basic gist is that you can define the
procedures that a type must have in order to implement a trait. A type
can then be marked as implementing that trait, and procedures and types
can specify traits instead of just types or classes.
The additional syntax would be as follows.
A new block structure, similar to interface, that defines a trait. I.e.
`trait some_trait
function someFunc(self, other)
trait(some_trait), intent(in) :: self
integer, intent(in) :: other
real :: someFunc
end function someFunc
subroutine someRoutine(input, output)
integer, intent(in) :: input
integer, intent(out) :: output
end subroutine someRoutine
end trait some_trait
`
A new statement only valid in a type definition. I.e.
`type, public :: someType
implements some_trait
contains
procedure :: someFunc => someTypeSomeFunc
procedure, nopass :: someRoutine => someTypeSomeRoutine
end type someType
`
And a new keyword for variable declarations. I.e. the following would
all be valid
`subroutine doSomething(some_input)
trait(some_trait), intent(in) :: some_input
...
end subroutine doSomething
` `type, public :: someOtherType
trait(some_trait), allocatable :: something
end type someOtherType
` `program hello
...
trait(some_trait), allocatable :: something
...
allocate(someType :: something)
...
end program hello
`
The compiler must simply confirm that a type implements the procedures
in the trait with the correct interfaces, and any type used where
`trait(some_trait)` is specified must simply be checked to have
implemented that trait. The run time implications are basically just an
extension of the dynamic capabilities that `class(someClass)` already
offers.
I think that this would enable techniques and patterns that would
otherwise require parameterized types, multiple inheritance, or
templates, or all 3, without requiring a hugely significant change to
the inner workings of the language or the syntax.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#125?email_source=notifications&email_token=AAAFAWHOCMNP5YU57XM3TNLQ3AN47A5CNFSM4KAWLA7KYY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4IDCJZEQ>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAFAWE6RSGUVWH33HLIUSDQ3AN47ANCNFSM4KAWLA7A>.
|
Actually, I think
|
It would also be desirable if more than one trait could be specified. I.e. a type could implement multiple traits, and an argument or variable could be required to implement multiple traits. |
While I agree that this would be a useful feature, I'd question whether it should be taken as the sole means by which to implement generic programming (as @certik seems to suggest). This issue is that it does not allow generic code to be written for the intrinsic types (e.g., an algorithm which could work with both reals and integers). While this could be overcome by wrapping the intrinsic types, that would be a rather tedious and inelegant workaround. Furthermore, how would one declare derived-type components by a trait. Would they be treated in much the same way as polymorphic variables? The downside of this as a way to create generic containers is that it would not allow for compile-type type-checking. Where these traits could be useful, however, would be as a means of constraining type-parameters such as those discussed in #4. |
It does allow to write generic code that works for both reals and integers. One specifies a trait (we called it an interface) that requires the arithmetic operations that you need, and if both real and integers satisfy it, then you can use both.
…On Sun, Dec 29, 2019, at 9:09 PM, Chris MacMackin wrote:
While I agree that this would be a useful feature, I'd question whether
it should be taken as the sole means by which to implement generic
programming (as @certik <https://github.com/certik> seems to suggest).
This issue is that it does not allow generic code to be written for the
intrinsic types (e.g., an algorithm which could work with both reals
and integers). While this could be overcome by wrapping the intrinsic
types, that would be a rather tedious and inelegant workaround.
Furthermore, how would one declare derived-type components by a trait.
Would they be treated in much the same way as polymorphic variables?
The downside of this as a way to create generic containers is that it
would not allow for compile-type type-checking.
Where these traits could be useful, however, would be as a means of
constraining type-parameters such as those discussed in #4
<#4>.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#125?email_source=notifications&email_token=AAAFAWGTGACHZXH57LN52ZTQ3FYA7A5CNFSM4KAWLA7KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEHZQ53Y#issuecomment-569577199>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAFAWHYZDXRR4UY37T6KWLQ3FYA7ANCNFSM4KAWLA7A>.
|
Given that the proposal required derived types to have an |
Well, the details have to be figured out, but that should be the goal.
…On Mon, Dec 30, 2019, at 4:14 AM, Chris MacMackin wrote:
>
> It does allow to write generic code that works for both reals and integers. One specifies a trait (we called it an interface) that requires the arithmetic operations that you need, and if both real and integers satisfy it, then you can use both.
Given that the proposal required derived types to have an `implements`
statement or attribute for them to match a trait, it's not clear to me
that this is the case.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#125?email_source=notifications&email_token=AAAFAWAUYLMUOWNME5HYO63Q3HJ27A5CNFSM4KAWLA7KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEH2CU5I#issuecomment-569649781>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAFAWCK3YPTQW2QB26VY5DQ3HJ27ANCNFSM4KAWLA7A>.
|
@cmacmackin , if there are some built-in traits that real, integer, etc. do implement, then you could write generic code that works for both reals and integers. While I agree that traits do not enable every kind of generic programming, I think it does take you farther than any other single feature would. As for derived-type components, yes, they would be treated like polymorphic variables. Correct, you wouldn't get as much compile-time type-checking, but you won't be able to get full compile-time type-checking without a full unification and constraint solver like you would find in Haskell. |
@certik Is there any write up of what was decided about this at the J3 meeting? I also like the |
What distinguishes a "trait" from an |
@klausler With inheritance, you also get other components and type bound procedures that you might not want. You could in theory use multiple inheritance like traits, but multiple inheritance involves dealing with a lot more problems. |
Well, thanks for the reply, but I'm still not seeing what a "trait" does that an |
That's actually kind of the point. It doesn't do everything that abstract types can do. Which makes it easier to implement, and easier to understand how it's supposed to be used. Sometimes a feature with the right constraints is actually better than a feature that can do everything. |
Thanks for the confirmation. |
Also, abstract types can't handle the intrinsic types. If you require a user to extend an abstract type to use a sorting library, then they aren't going to be able to use |
But the intrinsic types could be predefined to have procedures, such as the |
The abstract type approach requires a type A to subclass it in order for the user to use A in the generic subroutine, correct?
The advantage of the "interface" approach as we discussed at J3 meeting (I'll try to write up what we discussed soon) is that user types, such as A above, do not need to subclass anything. Thus the approach works like one would expect from templates.
…On Mon, Dec 30, 2019, at 12:04 PM, Peter Klausler wrote:
>
> Also, abstract types can't handle the intrinsic types. If you require a user to extend an abstract type to use a sorting library, then they aren't going to be able to use `real`, `integer`, variables. We don't want to have to wrap these into a custom type just to use a library, and then have to add all the operators that are already present for intrinsic types. That's one of the major problems we have now that this would solve if implemented well.
But the intrinsic types could be predefined to have procedures, such as
the `OPERATOR(<=)` needed by a sorting routine, yes?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#125?email_source=notifications&email_token=AAAFAWEQBOXDYZNOXZTQP5DQ3JA2PA5CNFSM4KAWLA7KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEH267UY#issuecomment-569765843>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAFAWEMANENJ2VM7EZ5FP3Q3JA2PANCNFSM4KAWLA7A>.
|
|
Let's discuss the various approaches on a simple example from #46. The syntax is just preliminary (I don't even like it myself, but that's not the point here): <T> function f(x)
<T>, intent(in) :: x
f = x + 1
end function This is just the old fashioned C++ style templates. What we discussed at the J3 meeting is that this causes slow compile times and very long unreadable error messages. The solution to both is to provide some kind of an interface (I just wrote this up, there might be some slight mistakes): type, requirements :: T
contains
generic :: operator(+) => plus
end type
abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs, rhs
type(T) :: plus
end function
end interface
type(T) function f(x)
type(T), intent(in) :: x
f = x + 1
end function Then when you use this, you can just call it with any type that satisfies the integer :: a, b
b = 1
a = f(b) # a will be equal to 2 and real :: a, b
b = 1
a = f(b) # a will be equal to 2 as well as any user defined type that defines type(user_type) :: a, b
b = ...
a = f(b) # "a" will be equal to "b" + 1 The way this would work is that the compiler would check that The current C++ has so called "weak concepts", where the compiler checks user code at instantiation time against the interface. But it does not check the code of "f" against T. So there can still be long error messages. So this is roughly what we discussed at the last J3 meeting. See here for more info: #4 (comment). |
This idea defers type resolution to runtime for the function |
@klausler , you could probably do both. In some instances it would be possible to specialize and inline a generic function, in others it might not be possible, (i.e. you're variable is By specifying the trait on both the type definition and the variable declarations, you don't actually have to recheck whether the type satisfies the trait at every use. |
@klausler the idea is fully compile time, there is no runtime resolution. To be specific and clear, let's have a module module A
implicit none
private
public :: T, f
type, requirements :: T
contains
generic :: operator(+) => plus
end type
abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs, rhs
type(T) :: plus
end function
end interface
contains
type(T) function f(x)
type(T), intent(in) :: x
f = x + 1
end function
end module When you compile it, the compiler will check that program B
use A, only: f
implicit none
integer :: a, b
b = 1
a = f(b) # a will be equal to 2
end When compiling |
Thanks for the clarification. Can (Additional question) And how is the availability of a conversion from default |
Like this? module A
implicit none
private
public :: T
type, requirements :: T
contains
generic :: operator(+) => plus
end type
abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs, rhs
type(T) :: plus
end function
end interface
end module A
module A2
use A, only: T
implicit none
private
public :: f
contains
type(T) function f(x)
type(T), intent(in) :: x
f = x + 1
end function
end module A2 I think so.
This is something we discussed at the J3 meeting, and there are several ideas, but I don't think we figured out how to best do this yet. @tclune do you remember about this particular point? |
@certik wrote:
Why won't this be per the usual language rules? That, when the processor "will instantiate f at the compile time of B" for type REAL say, will it not simply create an instantiated real function instantiated_f(x)
real, intent(in) :: x
instantiated_f = x + 1
end function and in which case, the standard stipulations toward the above mixed-mode arithmetic will apply? |
This is the natural thing to do. But the problem is that if this approach can cause the code to be in error at instantiation? If so, then this is not going to work, as that would be equivalent to the "weak concepts" from C++. Instead, we want the "strong concepts", because we want to catch all errors in These are the details that we have to iron out. |
I have an example which would demonstrate why a function may not be able to be fully "instantiated" at compile time. It's a bit convoluted though. Have a trait
I'm pretty sure the above is perfectly valid except for the trait part, but you could substitute that with another abstract type that A extends from instead and it would actually be valid. This is a contrived example, but you can kind of see my point. You would expect this program to print 2, but if it tried to actually "instantiate" at compile time, it'd probably end up printing 1 because it would instantiate the routines as taking type A and end up calling |
@everythingfunctional seems the only difference between your proposal and what I described above is that in your proposal, user types such as Regarding your last comment about runtime instantiation: I think that is no different to using templates in C++ with virtual functions ---- they will resolve to the base class function, known at compile time, not the overloaded function known at runtime. So the above example in C++ would return 1. If you want it to resolve to the "overloaded" function (and return 2), you have to use the CRTP pattern where you explicitly type the base class to a subclass at compile time, and then the template resolves to the overloaded function, at compile time. I would think the above design (everything at compile time) would make the most sense, as there will be no runtime overhead. |
Syntactically, I think that is the only difference. But, my proposal would be that you could provide a list of traits that are satisfied there. Also, I would propose you be able to specify a list of traits for procedure arguments and allocatable variables. That way you could say this procedure needs something that is Addable and Showable (i.e. has operator(+) and procedure Rust has some traits that are built in to the language. I would propose some be added to the Fortran standard as well. That way intrinsic types could be used for at least some traits. It would also beneficial if there was some way to add traits, or even type bound procedures, to intrinsic types. I don't know if that's too much to ask, but Rust has it. Or perhaps your idea of not specifying which traits a type implements would be sufficient. I was unclear on that behavior of C++. What does Fortran currently do in this situation? I haven't tested it. I like the idea of no runtime overhead, but sometimes you can't get away from it. In my example, of |
Ok, let's keep just this issue for now to discuss the details, it seems it's quite similar, and we have not moved beyond what I described above either at the J3 committee. Regarding your last paragraph, indeed, the whole chain |
No. Separate module procedures (15.6.2.5) are defined with |
I was thinking... would it be useful if some traits were optional, and a procedure could query if an input had a certain trait and then do something with that information? I was thinking of the automatic differentiation application (see #95). You could write a function that had an optional derivative calculation, but only if you gave it a class where you needed that (if you passed in a real, it wouldn't do that part). |
@certik wrote:
With respect to your comment in #125 (comment) , I assume you meant to catch errors at compile-time of module A2? Assuming yes and keeping in mind the J3 discussion on Meaning, in your generic procedure Whereas an instruction such as |
Yes. My understanding of the strong concept is that |
@tclune I tried to summarize our latest status on templates from the last J3 meeting in this issue. Would you have time to make some progress on this before our next meeting? It would be nice to have some draft of a proposal, even if very preliminary. If we wait until we meet at the J3 meeting, it will be too late. |
Yes, this is absolutely essential, and it is incomprehensible why this is still not part of the language! I was about to open an Issue for this myself, but I will simply comment here, instead, on what I believe would be the best way to proceed. I think that in order to get this feature rapidly into the language it is essential to focus on run-time polymorphism (i.e. OOP) first. One should leave a potential use of some similar feature for compile-time polymorphism (i.e. generics) to some later revision, that will deal with such complications separately. A 'trait' as it was called above is, in Fortran speak, nothing but an abstract type, with the important restriction that it is prohibited to contain any fields (i.e. variables) or non-deferred methods, i.e. any implementation code. Like an abstract type it may contain deferred procedures, and like an abstract type it cannot be instantiated. I prefer the name 'interface type' instead of 'trait' because the syntax for the aforementioned semantics most compatible with present-day Fortran is something like the following
(notice the nice double occurrence of "abstract" and "interface" for consistency). So the only new thing required above is the new type-qualifier "interface" in the type declaration. Thus the compiler will know that the insertion of anything above the contains statement is forbidden, and that any procedure declared below the contains statement must be deferred. To use the feature one will simply code the following
and provide an implementation of
I think that no completely new syntax should be necessary here. We should still be able to use the "class" statement, since If we need to conform to two InterfaceTypes we would have, e.g., something like the following:
This could then be combined also with implementation inheritance to, e.g., inherit variables and concrete versions of method1/method2 from some ConcreteBaseType
So, one would obtain a two-tiered inheritance capability, one to enable polymorphism, and one to inherit implementations (if one so desires), the same way it works in Java. It is the lack of such a feature which is responsible for most of the "select type" downcasting non-sense that we currently have to contend with. |
@difference-scheme thanks for the feedback. We need your help:
The reason it's not part of the language already is that nobody has put enough work to get it in. In order to get this done, we need to brainstorm and figure out a way to do this that will work and that we can agree upon, then write proposals for it, then discuss at the Committee and convince everybody, and several times (at several meetings) etc. Would you be interested in helping us with this? The hardest part right now is to collect all the various proposals above, and figure out a proposal that we would all agree upon, and to drive the discussion and try to reach an agreement. To that aim, I think we have to have document where we discuss the pros and cons of all the proposals above, so that we can move the discussion forward. Your particular proposal in your comment seems to be a variation of several of the above proposals. So I think it's time we start to work on just one document and keep improving the various aspects of it. |
@certik Yes, I am willing to help with this, though I am only just a user and not an expert on languages. In case you've already started to write up some draft for a proposal, feel free to use anything that I've written up in my comment. I could then try to come up with some examples on how typical tasks have to be coded now, vs. how they would be coded with the feature in place, to illustrate the advantages. I believe the main point to agree on first is whether we want such a feature to cover only run-time polymorphism for now, or both run-time and compile-time polymorphism. The former would make adoption much quicker and simpler, as there is very little risk involved, given that the same feature already works in other languages (Java, C#, etc.). |
Thanks. @tclune is leading the effort here, he might have some document already. Otherwise maybe somebody will find time to write it up. I want to write proposals for some other features first, before I get to this.
The proposal I had above would be only compile time polymorphism -- although as we discussed above, maybe it's both. Doing only runtime polymorphism with interfaces would be new, but one can "sort of" do it already with abstract classes, so there is a workaround. But Fortran currently does not have a compile time template / polymorphism. |
I finally got around to testing it, and Fortran does do run time look up of functions for polymorphic variables. A procedure which accepts a I.e. given
The following will print "Howdy"
This is why I think traits should be implemented as run time polymorphism. It doesn't stray far from the already existing concepts and underlying machinery of |
@everythingfunctional ok, in that case I think this proposal is different to what we discussed at the Committee so far, as my understanding was that we were discussing compile time polymorphism / templates. I will try to create a separate issue for that, and we can use this issue for your original proposal, which is runtime polymorphism with traits. |
@everythingfunctional, @certik Great! I have started to write up a draft for a proposal along these lines (the preliminary title is: "Improved run-time polymorphism for Fortran"). |
@difference-scheme just create a PR with the proposal. Once we have the proposals written and we can see the differences between them, if two of them should be merged, then we can do it. Otherwise we'll have several different proposals and we can then discuss the pros and cons of each approach, which will move the discussion forward. |
FWIW in the example module A
implicit none
private
public :: T
type, requirements :: T
contains
generic :: operator(+) => plus
end type
abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs, rhs
type(T) :: plus
end function
end interface
end module A
module A2
use A, only: T
implicit none
private
public :: f
contains
type(T) function f(x)
type(T), intent(in) :: x
f = x + 1
end function
end module A2 The interface should be: abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs
integer, intent(in) :: rhs
type(T) :: plus
end function
end interface The type probably also needs an assignment interface for the "strong concepts" to be enforced, and would benefit from having an example instantiation, so a full example should probably be module A
implicit none
private
public :: T
type, requirements :: T
contains
generic :: operator(+) => plus
generic :: assignment(=) => assign
end type
abstract interface
function plus(lhs,rhs)
type(T), intent(in) :: lhs
integer, intent(in) :: rhs
type(T) :: plus
end function plus
end interface
abstract interface
function assgn(lhs,rhs)
type(T), intent(out) :: lhs
type(T), intent(in) :: rhs
end function assign
end interface
end module A
module A2
use A, only: T
implicit none
private
public :: f
contains
type(T) function f(x)
type(T), intent(in) :: x
f = x + 1
end function
end module A2
program example
use A2
implicit none
real :: two
two = f(1.0)
end program example |
(Let me know if you think should be a different linked issue vs a comment on this one) It would be great if the intrinsics such as This would be particularly useful for working with a broad range of algebraic structures. |
Java calls them interfaces, Haskell calls them type classes, and Rust calls them traits. But the basic gist is that you can define the procedures that a type must have in order to implement a trait. A type can then be marked as implementing that trait, and procedures and types can specify traits instead of just types or classes.
The additional syntax would be as follows.
A new block structure, similar to interface, that defines a trait. I.e.
A new statement only valid in a type definition. I.e.
And a new keyword for variable declarations. I.e. the following would all be valid
The compiler must simply confirm that a type implements the procedures in the trait with the correct interfaces, and any type used where
trait(some_trait)
is specified must simply be checked to have implemented that trait. The run time implications are basically just an extension of the dynamic capabilities thatclass(someClass)
already offers.I think that this would enable techniques and patterns that would otherwise require parameterized types, multiple inheritance, or templates, or all 3, without requiring a hugely significant change to the inner workings of the language or the syntax.
The text was updated successfully, but these errors were encountered: