j3-fortran / fortran_proposals

Proposals for the Fortran Standard Committee
178 stars 15 forks source link

First draft of run-time polymorphism proposal #143

Closed difference-scheme closed 4 years ago

difference-scheme commented 4 years ago

This proposal resulted from the discussion in Issue #125. It accounts for some of the suggestions made there, but includes also some (minor) new twists. It motivates why Fortran's run-time polymorphism needs to be strengthened, and includes a use case to demonstrate the advantages of the proposed features.

everythingfunctional commented 4 years ago

This is great. Thanks. I do have few questions, to which I hadn't devised good answers myself yet.

  1. Should it be possible to give multiple interfaces in a declaration? I.e.

    trait(ISolver, IPrinter) :: client

    or would one need to "compose" a new interface from the two and provide that name as done in the examples you give?

  2. What would the interface look like for procedures that are pass? I.e.

    
    abstract interface :: IShowable
    function show(self) result(string)
        trait(IShowable), intent(in) :: self ! Is this right?
        character(len=:), allocatable :: string
    end function show
    end interface IShowable

type, implements(IShowable) :: my_type ... contains procedure :: show => my_type_show end type my_type

function my_type_show(self) result(string) class(my_type), intent(in) :: self character(len=:), allocatable :: string

string = "My Type"

end function my_type_show


3. What if you wanted an interface with a procedure that took two arguments of the same type? I.e.

abstract interface :: IAddable function add(self, other) result(combined) trait(IAddable), intent(in) :: self trait(IAddable), intent(in) :: other trait(IAddable) :: combined ! Is this possible? end function add end interface IAddable

type, implements(IAddable) :: my_type ... contains procedure :: add => my_type_add end type my_type

! Does this conform to the interface now? function my_type_add(self, other) result(combined) class(my_type), intent(in) :: self type(my_type), intent(in) :: other type(my_type) :: combined

combined = ! some expression with self and other

end function my_type_add


  I feel like this might only possible with compile time stuff like templates, but wondered if you had any ideas.
certik commented 4 years ago

Thank you for submitting this! @tclune what is your opinion on this?

difference-scheme commented 4 years ago

@everythingfunctional So far I have thoughts on your first two points, so let's focus on those.

  1. Yes, one would have to compose a new interface and use it as in the examples. The reason is that a named abstract interface would be viewed as a type in its own right, and the meaning of say the declaration trait(ISolver), allocatable :: solver is that solver is to be treated like a (polymorphic) variable of type ISolver. Fortran is needlessly verbose here. In Java the above would simply be written as follows: ISolver solver; So, in this notation, your above example would be ISolver, IPrinter client; which I am pretty sure Java doesn't allow because it views it as a self-contradiction (as the variable can only have one declared type).

  2. Concerning the declaration of the variable self within the IShowable interface of your second example: I think this variable must be declared as being an instance of the derived type my_type, rather than being of type IShowable. The reasoning being that the function show is a procedure that is bound to my_type, and would thus need to have, in general, access to my_type's data fields, in order to do some useful work. But data fields are forbidden for interfaces.

So, your example of IShowable's declaration would have to read something like this

abstract interface :: IShowable
    import :: my_type
    function show(self) result(string)
        type(my_type), intent(in) :: self
        character(len=:), allocatable :: string
    end function show
end interface IShowable

type, implements(IShowable) :: my_type
...
contains
    procedure :: show => my_type_show
end type my_type

which brings us to the next point: this is a nasty circular reference (IShowable depends on my_type, while my_type depends on IShowable) that would need to be dealt with somehow.

I personally believe that it will be essentially impossible, in any OO application code of some complexity, to entirely avoid circular references. But we want their occurrence to be among interfaces only. We don't want them to occur among concrete implementation types, and we sure don't want them to occur between interfaces and types that depend on them.

Unless I am missing something, I believe we need to check in detail how this has been dealt with in Rust.

everythingfunctional commented 4 years ago

Rust has a way of referring to the "Implementation" type inside a trait block. The keyword Self. See this. We could do something similar by simply stating that within an abstract interface block, referring to the interface (trait) as class(ITrait) or type(ITrait) means replace it with the actual type implementing the trait, whereas trait(ITrait) does not.

I think with that additional specification you could solve both my situations like

abstract interface :: IShowable
    function show(self) result(string)
        class(IShowable), intent(in) :: self
        character(len=:), allocatable :: string
    end function show
end interface IShowable

type, implements(IShowable) :: my_type
...
contains
    procedure :: show => my_type_show
end type my_type

function my_type_show(self) result(string)
    class(my_type), intent(in) :: self
    character(len=:), allocatable :: string

    string = "My Type"
end function my_type_show

and

abstract interface :: IAddable
    function add(self, other) result(combined)
        class(IAddable), intent(in) :: self
        type(IAddable), intent(in) :: other
        type(IAddable) :: combined
    end function add
end interface IAddable

type, implements(IAddable) :: my_type
...
contains
    procedure :: add => my_type_add
end type my_type

function my_type_add(self, other) result(combined)
    class(my_type), intent(in) :: self
    type(my_type), intent(in) :: other
    type(my_type) :: combined

    combined = ! some expression with self and other
end function my_type_add

We may need to think through all the implications that will have on inheritance though. For example, inside an interface block that inherits from another, does that mean referring to any of those traits that way means the actual type implementing that trait? Probably.

But what about abstract and extended types? If I create an abstract type that "implements" a trait, but defers implementation of some of the procedures, in the extended type, when implementing the necessary procedures, do arguments of type(ITrait) refer to the parent type or the child type? Probably the child type.

I think we can probably define the spec well enough to handle this, just trying to make sure we think through all the edge cases.

everythingfunctional commented 4 years ago

My next question is then about "generics", and brings up the question about which argument is pass.

Say I want to implement a something like

abstract interface :: IScalable
    function multiplyLeft(multiplier, self) result(scaled)
        double precision, intent(in) :: multiplier
        class(IScalable), intent(in) :: self
        type(IScalable) :: scaled
    end function multiplyLeft

    function multiplyRight(self, multiplier) result(scaled)
        class(IScalable), intent(in) :: self
        double precision, intent(in) :: multiplier
        type(IScalable) :: scaled
    end function multiplyRight

    function divide(self, divisor) result(scaled)
        class(IScalable), intent(in) :: self
        double precision, intent(in) :: divisor
        type(IScalable) :: scaled
    end function divide

    interface operator(*)
        module procedure multiplyLeft
        module procedure multiplyRight
    end interface operator(*)

    interface operator(/)
        module procedure divide
    end interface operator(/)
end interface IScalable
everythingfunctional commented 4 years ago

I have another thought related to this, but that should probably be a follow up proposal. What about a library that defines a trait, and another library that defines a type, but I'd like to be able to use them together?

In Rust this is possible because the implementation of a trait is separate from the definition of the type, and can be done in multiple places. Would it be possible in Fortran to allow types to be "re-opened" so as to add procedures in a different place? Maybe not, and we'll just have to stick with wrapper types for that use case, but it may be worth considering.

difference-scheme commented 4 years ago

@everythingfunctional Ok, I believe I got your point. I think you mean that we need to have a pair of names: "trait"/"class" for abstract interfaces in the same way as we have "type"/"class" for derived types (unfortunately the most logical pair: "abstract interface"/"trait" is out of the question due to its verbosity). Hence the confusion with the names.

Yes, I agree. The whole thing needs to be modelled with respect to inheritance essentially in the same way as type/class is for derived types. I need to check in detail the Rust link that you provided to understand how they did it, and to see whether we should add some example along these lines to the proposal.

difference-scheme commented 4 years ago

@everythingfunctional To comment on your last question first. I believe it should be possible to do in Fortran what is done in Rust. Fortran and Rust are very similar in that they do not have classes in the sense of Java or C++. They only have "structures" (structs in Rust, types in Fortran) to which procedures are bound.

Therefore, I believe it should be possible, in principle, to make the proposed feature a complete equivalent to Rust's impl. But then one might be forced to give up interoperability with what is already included in the language (i.e. type extension). This is why I leaned more towards the Java way, rather than the Rust way, of doing this. But I'd love to hear the opinions of compiler writers on this.

everythingfunctional commented 4 years ago

But I'd love to hear the opinions of compiler writers on this.

@certik , is there anybody on any of the major compiler teams active on here that may be able to speak to this?

@difference-scheme , there was a suggestion not to require types to specify which traits they implement, just have the compiler verify it at each use. I think that would probably slow down compile times. But it probably would enable more flexibility.

difference-scheme commented 4 years ago

I have revised the proposal to account for @everythingfunctional's feedback.

I have opted to admit only one way for declaring polymorphic variables, namely Fortran's standard way, using the class specifier (which would therefore need to be extended). I believe anything else will lead to confusion.

I have also added a Java and a Rust version of the (Fortran) example code given in Appendix B of the proposal. You can find them in the separate Examples directory, in case you'd like to see how the proposed features work in these languages.

difference-scheme commented 4 years ago

@certik Unfortunately, I couldn't find the time before the committee's upcoming meeting next week to make the present proposal as complete as I'd like it to be.

What is presently missing is a facility like Kotlin's by operator for automatic delegation of functionality to composed objects, that would spare the programmer the (present) need to write large amounts of boilerplate (function call indirection) code with object composition. I'd also like to account for those comments of @everythingfunctional that haven't been satisfactorily addressed yet.

Will the present version of this proposal, along with #142, be nevertheless discussed in the upcoming committee meeting?

certik commented 4 years ago

@difference-scheme I think it is good enough to be discussed. I think in general the committee prefers a simple text file, instead of a pdf, but I could be wrong on that. Either way, since this will not go into 202X, I think we do not perhaps need to submit this formally, but simply discuss it as an option for generic programming in the Data subcommittee.

(I personally think we should do it more like Rust does it, and do most of generic programming at compile time, not runtime. If I find time, I'll try to submit a separate proposal along those lines. But it's good that your idea got written down, so that we can discuss it and move the discussion forward.)

difference-scheme commented 4 years ago

@certik Thanks for your reply. Could you elaborate briefly on what features you think should be taken from Rust?

To my knowledge, Rust offers both compile-time generics, and run-time polymorphism (the latter by using "trait-objects"), so I would think that these two do not conflict with each other. Of course, anything that can be done at compile time should be done right then and there (at zero run-time cost).

certik commented 4 years ago

I think you are right --- I am still learning Rust and I am currently not 100% sure what happens at runtime and what happens at compile time, but it seems Rust has both compile-time generics, and run-time polymorphism, and the traits seem to be actually used for both (https://blog.rust-lang.org/2015/05/11/traits.html).

Anyway, your proposal moves the conversation forward and may end up as part of the big proposal for generics.

I am very happy that we are moving forward on these things.

difference-scheme commented 4 years ago

Yes, in Rust the traits are used for both the bounding of compile-time generics, and for enabling subtyping run-time polymorphism.

This is why having named abstract interfaces in Fortran would be so important; it would serve both these purposes. We really need to have both these capabilities in the language, because they are complementary: some things that can be done with run-time polymorphism cannot be done with compile-time generics, and vice versa.

It would be a terrible blow to Fortran if we couldn't get both these capabilities into the language (and we need them rather sooner than later). So, I hope you will find the opportunity to work on the generics proposal, and I'd be willing to help out with this.

certik commented 4 years ago

I agree that we need both. I also agree we should design both at the same time.

I'll keep you updated.

On Fri, Feb 21, 2020, at 4:38 PM, difference-scheme wrote:

Yes, in Rust the traits are used for both the bounding of compile-time generics, and for enabling subtyping run-time polymorphism.

This is why having named abstract interfaces in Fortran would be so important; it would serve both these purposes. We really need to have both these capabilities in the language, because they are complementary: some things that can be done with run-time polymorphism cannot be done with compile-time generics, and vice versa.

It would be a terrible blow to Fortran if we couldn't get both these capabilities into the language (and we need them rather sooner than later). So, I hope you will find the opportunity to work on the generics proposal, and I'd be willing to help out with this.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/j3-fortran/fortran_proposals/pull/143?email_source=notifications&email_token=AAAFAWH5S4XLTFBCTDOCJTLREBQXXA5CNFSM4KQPILZKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEMUOETA#issuecomment-589881932, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAFAWH7MTDAMB44GLIBIPDREBQXXANCNFSM4KQPILZA.

certik commented 4 years ago

This PR would be a great reference for a proposal. A good proposal should be 75 characters per line and between 50 - 400 lines. Also, it should be written as a regular text file, not latex or pdf.

As I said, I'll discuss this with the Data subcommittee and update this issue with the results.

difference-scheme commented 4 years ago

@certik: Any news on this?

certik commented 4 years ago

@difference-scheme yes, we discussed your proposal together with #125 at the subset of the Data subcommittee, if I remember well, the people present were: @tclune, @everythingfunctional, @FortranFan, Magne Haveraaen, @zjibben and myself. (See #155 for a summary of the whole meeting and attendance.)

The consensus so far seems to be that we want both runtime polymorphism (both #143 and #125 seem quite similar in this respect, obviously we would need to unify the syntax) as well as compile time polymorphism (the proposal #125 actually covers that too). It seems the way Rust handles Traits is something to consider, it might be exactly what we need. We discussed that the Rust syntax seems identical for compile time as well as runtime generics, and we were a bit unsure how Rust determines which one will happen. We should do more examples and analyze on case by case basis.

More importantly, when considering features such as #157, a common objection at the plenary was that we really need to have a plan for generics (even if they would go into 202Y) so that we can ensure that such features such as #157 are designed to be consistent with generics, so that we do not end up with a not well thought out system.

As such, I would like to push forward and hopefully get a community agreement how the generics should be done for 202Y, and then design features such as #157 for 202X to be consistent with them.

difference-scheme commented 4 years ago

@certik Thanks for the update! I agree that Rust handles traits in a very elegant and general way and that we should consider taking their approach as a baseline for both compile-time and run-time polymorphism.

We shouldn't forget Swift, though, which does things in a manner that is very similar to Rust, and which has, moreover, succeeded in making all of this even interoperable with (classical) implementation inheritance.

Personally, I wouldn't even mind declaring Fortran's implementation inheritance, introduced in the 2003 standard, an obsolescent feature, if we would have a Rust-equivalent of run-time/compile-time polymorphism as an alternative. So, I believe we should be bold/progressive. Do you think that there might be opposition in the committee to such a progressive design, by people that would prefer a more conservative path?

Concerning Rust's run-time vs. compile-time generics see also the following overview: https://thume.ca/2019/07/14/a-tour-of-metaprogramming-models-for-generics/ Basically, the dynamic part (trait-objects) is indicated by the dyn keyword.

certik commented 4 years ago

@difference-scheme great link, thanks for sharing it. I personally wouldn't mind at all to make inheritance obsolete, and to recommend Rust style composition using Traits instead, as long as there is a clear path how to upgrade. But even if it is not officially obsolete, we can still, as a community, simply recommend to use traits over inheritance.

Magne stressed a lot to try to implement Traits with similar syntax as what is already in Fortran, to make it more familiar to users as well as perhaps easier to implement for compilers.

difference-scheme commented 4 years ago

@certik I fully agree (also with Magne's point).

Rust-style composition combined with automatic (implicit) delegation (as in Kotlin) is a way better programming model than inheritance. Also, implicit delegation will be absolutely crucial for easing the programmers' transition from the use of inheritance towards the use of the new features, so it is something we will absolutely need to have.

Rust doesn't have it yet, because they couldn't agree so far on a sufficiently simple implementation: https://github.com/rust-lang/rfcs/pull/2393

It would be nice to see Fortran being quicker in this respect!

certik commented 4 years ago

@difference-scheme good point about delegation. If you have some ideas how to design it for Fortran, please open an issue / PR.

difference-scheme commented 4 years ago

@certik I have some first ideas on how delegation could be done in Fortran, in order to make it an almost drop-in replacement for implementation inheritance.

I had started to work on an update of the present proposal along these lines, because I thought implicit delegation would be best explained in this same context (to demonstrate its use on the same examples so that one may clearly judge its benefits).

But since the present proposal appears to have been merged some moments ago, would you like me to keep updating this one, or should I open a new PR (along with an accompanying issue) that deals exclusively with delegation?

certik commented 4 years ago

@difference-scheme go ahead and open a new PR that deals with delegation.

@tclune thanks for the input. Note that I assigned it before the meeting, in the hopes that we can merge and discuss more ahead of time before we meet in person. In the same spirit, I would encourage that if you come up with requirements, that you share it with us well ahead of the meeting, so that we can discuss and think about it, in order to be more efficient in person.

See also #163. I believe that in order to come up with good requirements, we need to have an initial informal proposal in hand first.