Traits, Generics, and modern-day OO for Fortran

So what is the plan? Will this proposal be implemented in LFortran then submitted to the committee or the other way around?

Given that the committee does not require a prototype first (which I disagree with, but thatā€™s how they currently operate), I think this can be submitted now to the committee.

My personal plan is to finish LFortran to compile all codes, so thatā€™s what I need to focus on. Only then focus on prototyping new features. However we are looking for a person who would be interested in prototyping these generics in LFortran, we can help you get up to speed. Itā€™s been tremendously useful to prototype the function-based generics in LFortran and play with them in an actual compiler. @meow464 you contributed to LFortran before, would you be interested in prototyping this?

Finally, I also invite other compilers (Flang, GFortran, Intel, NAG, Cray, etc.) to implement these generics.

4 Likes

My schedule this semester will be a bit tight but Iā€™m definitely interested.

Given you experience with function based generics, where do you suggest I start?
My first thought would be implementing parser ā†’ AST ā†’ ASR for small chunks of the proposal such as interface blocks.

1 Like

That would be awesome! Yes, I would pick a standalone example, a very small subset, say the generic summation function above. And then try to compile it end-to-end. We have lots of internals already working for this, so you would just need the interfaces, extend the parser as needed, and then improve AST->ASR. We might need some small changes in ASR. We can then implement a few other depth-first approaches. It will not be hard.

2 Likes

How about this syntax, e.g., for the simple_sum function, which overloads the generic keyword, introduces some additional properties including numeric or the like and is also in keeping with current practice of declaring variables:

function simple_sum(x) result(s)
  generic(T), allocatable, numeric, intent(in) :: x(:)
  generic(T) :: s
  integer    :: i
  s = T(0)
  do i = 1, size(x)
    s = s + x(i)
  end do
end function

This removes the need to add or look for different symbols lie <>, {}, etc.

Generally speaking, I believe that questions regarding the (final) syntax should be among the things that we should decide on only in the later stages of the development process, i.e. once we have some actual practical experience with the design from a compiler implementation.

Regarding your particular suggestion, Iā€™d like you to consider that INumeric, as it is used in the proposal, is a user defined entity/interface. Itā€™s not some property that is (or should be) hardwired into the language, and that could thus be predefined as an attribute ā€“ as your example seems to suggest.

There are, of course, also other reasons why it will be hardly possible to dispose of the generic type parameter list (within the curly braces). What one might be able to do, though, is to have only the type parameter appear in the braces, and to move the constraint after the {}() combination, as follows:

function sum{T}(x) result(s)
   INumeric            :: T
   type(T), intent(in) :: x(:)
   type(T)             :: s
   integer :: i
   s = T(0)
   do i = 1, size(x)
      s = s + x(i)
   end do
end function sum

But this would destroy the nice symmetry that is present in the current syntax between generic procedure and generic derived type definitions.

Personally, I believe the present syntax is sufficiently ā€œFortranicā€, as @rwmsu has put it, to be given a chance in a prototype implementation. If it should be found wanting, we can always change it later.

1 Like

Congratulations for the proposal and the way it is presented. One can see a lot of effort went into it.

I was wondering how this approach compares to Mojoā€™s. Mojo is a new language under development, and I suppose Chris Lattner is also trying to combine the best ideas from Swift (his own baby) with approaches from Rust, Go, etc.

BTW, for those ā€“ like me ā€“ not acquainted with traits/interfaces/etc., Mojoā€™s manual offers a rather smooth and hands-on intro to the topic.

3 Likes

Thank you for your good words, and also for providing the link, and for reminding me to have a look into Mojo.

I intended to contact Chris Lattner anyway, in order to send him our document and to thank him for his work on Swift, and the ideas we borrowed from it. It would be only natural for these ideas to find their way back also into LLVMā€™s Flang.

Thanks for reminding me about it!

2 Likes

Finally skimmed through it.

Well done, looks great!

In Rust, traits can define default implementations. Maybe it is already discussed and I missed it, but is that taken into consideration in the Fortran implementation?

2 Likes

@meow464, it seems that I only answered half of your question (namely the first half pertaining to the use of implementation inheritance).

The answer to the second half of your question, namely whether the code of the function

function sum{INumeric :: T}(x) result(s)
    type(T), intent(in) :: x(:)
    type(T) :: s
    integer :: i
    s = T(0)
    do i = 1, size(x)
        s = s + x(i)
    end do
end function sum

can be used with different types T that conform to the INumeric interface, is affirmative.

Assume you start off with the need to use this function with only one type, say the real type. Then you would code interface INumeric as follows:

abstract interface :: INumeric
  real
end interface INumeric

Now suppose that somewhat later you decide that you want to use this function together with all the real, and also with all the complex kinds, that your compiler supports. The only thing that you need to change then is the definition of INumeric, namely as follows:

abstract interface :: INumeric
  real(*) | complex(*)
end interface INumeric

Your sum function will then work with both these types and with all their kinds.

2 Likes

Thanks for your comment. Yes, Rust allows having default implementations in traits.

This is a delicate issue, because if you allow this, then you are essentially doing a step backwards into inheriting implementation, with all its associated dangers that go by the moniker ā€œThe fragile base class problemā€.

From the standpoint of purity of the paradigm this should be avoided. I recommend to always use object composition (and delegation), instead. So, Iā€™d like to avoid supporting this, if possible.

1 Like

I agree with you. I guess the main issue this modern OO solution is trying to solve is the generics - compile-time polymorfism that does not exist in dynamic typing.

However, I still think composition does not naturally solve the whole space of inheritance or code re-usage issues. Composition solves problems like ā€œCar has an engineā€ but it does not solve the ā€œCat is an animalā€ issue. I am not an expert like you, so I would be interested to hear your take on this. And again, by experience (shooting myself in the foot too many times), I agree with you that implementation inheritance is a delicate issue (I am actually a bit surprised that Rust allows this).

2 Likes

I presume the Rust developers simply gave in, a little bit, to that part of their user community that demands Rust to support inheriting implementation (without actually going the whole way and introducing it into the language).

Also, I do not think that the ā€œis aā€ and ā€œhas aā€ paradigms, as they are often cited in OO, have any real significance. Composition and delegation, if they are combined with interface inheritance, allow you to do everything (and more) that implementation inheritance allows you to do.

We can actually improve upon the usability of composition + delegation by introducing implicit delegation (by the compiler) into Fortran, as it is available in the Kotlin language. This would make composition + delegation as comfortable to use as implementation inheritance.

See here: https://kotlinlang.org/docs/delegation.html

This could be considered after weā€™d have acquired some first experience with a prototype. To facilitate a prototype implementation we need to initially limit the amount of new features to what is absolutely essential.

1 Like

The advantage of this approach is that the interface definition is super simple. The disadvantage is that you have to modify the interface for every new type that you want to support. Sometimes this downside is ok, since the modification is simple but if you do not control user code, this might be a problem. And if this is a problem, the solution is to specify the full interface. I have shown both approaches in the example at Traits, Generics, and modern-day OO for Fortran - #2 by certik. If you specify the full interface, then this will work with any user code and types in the future.

1 Like

Yes, exactly. Good that you pointed this out.

If you want to protect yourself from such future change then you need to specify the full interface.

1 Like

This whitepaper looks exceptionally well researched and thought out. I donā€™t have enough Fortran experience to comment on how well the proposal would work in practice, but I love how the paper does a thorough job looking at modern languages (including Go, whose generics donā€™t get enough credit I think) for inspiration and cross comparison.

As for Mojo, we havenā€™t made hard decisions about many points of the design space, but we do follow implicit conformance (ala Go) and will support implementation inheritance in classes, ala Python and Swift. Thank you for the thoughtful writeup!

-Chris

11 Likes

Thanks a lot for sharing your thoughts with all of us Chris, and for giving us the opportunity to learn more about Mojo.

Iā€™ve had a look into the Mojo link that was provided up-thread, and it is awesome work! Very inspirational, as is the entire rest of your work.

1 Like

Thanks @clattner_llvm for your comment! Much appreciated. I am sure it made @kkifonidis very happy. :slight_smile:

2 Likes