Skip to content

First draft of run-time polymorphism proposal #143

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

Merged
merged 4 commits into from
Mar 2, 2020

Conversation

difference-scheme
Copy link

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
Copy link
Member

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?

  1. 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
  1. 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
Copy link
Member

certik commented Feb 5, 2020

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

@certik certik requested a review from tclune February 5, 2020 21:45
@difference-scheme
Copy link
Author

@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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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
Copy link
Author

@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
Copy link
Author

@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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

@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
Copy link
Member

certik commented Feb 21, 2020

@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
Copy link
Author

@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
Copy link
Member

certik commented Feb 21, 2020

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
Copy link
Author

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
Copy link
Member

certik commented Feb 21, 2020 via email

@certik
Copy link
Member

certik commented Feb 23, 2020

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
Copy link
Author

@certik: Any news on this?

@certik
Copy link
Member

certik commented Mar 2, 2020

@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
Copy link
Author

@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
Copy link
Member

certik commented Mar 2, 2020

@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
Copy link
Author

@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: rust-lang/rfcs#2393

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

@certik
Copy link
Member

certik commented Mar 2, 2020

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

Copy link
Member

@tclune tclune left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am approving the pull requent because @certik assigned it to me. But I do feel that this work is going into specs when we do not even have a general agreement on requirements. I'm going to work hard to have a requirements paper ready for the WG5 meeting and would encourage others to contribute clear use cases (for this and other aspects of generics).

@tclune tclune merged commit e3bcbfa into j3-fortran:master Mar 2, 2020
@difference-scheme
Copy link
Author

@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
Copy link
Member

certik commented Mar 3, 2020

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants