In this second post on the undesirability of receiver arguments within Fortran traits, we’ll see how one could employ associated types to declare such arguments in a trait, in order to match the three different implementation examples of the add_to
function that I gave in the first such post.
Recall, that the receiver declarations of these functions were the following:
class(Adder), intent(in) :: self
type(Adder), intent(in) :: self
type(integer), intent(in) :: self
Due to Fortran’s idiosyncrasy of requiring (in general) type
or class
specifiers to formulate variable declarations, a simple type alias (like Rust’s Self
, that would merely match the implementing type) isn’t sufficient to match all of the above declarations from within a trait. One rather needs to come up with a mechanism that can match also the different type specifiers.
In an earlier iteration of our design, this was accomplished through the introduction of a new deferred
specifier that expressed associated type declarations within traits. This specifier was provided with a variable’s name in order to let the compiler infer the complete declaration of that variable from an actual implementation of that trait.
That is, one wrote the trait in question as
abstract interface :: IAddition
subroutine add_to(self,arr)
deferred(self), intent(in) :: self
real, intent(inout) :: arr(:)
end subroutine
end interface
with the compiler then expanding deferred(self)
to match any of the three above type declarations for self
, including their specifiers. This appears like a satisfactory solution to the problems that were mentioned in my last post (although it introduces significant verbosity that is entirely avoidable; see the ultimate solution that is discussed below). Why then did we not ultimately stick to this design for our Fortran traits?
The main problem with this approach is that the presence of associated types pretty much “taints” a trait for use by run-time polymorphism, and effectively destines it for use by compile-time polymorphism. Such traits can only be made to work with run-time polymorphism if some form of (automatic) type erasure is supported by a compiler. This is, e.g., one of the things that Rust’s dyn Trait
and Swift’s any Trait
declarations accomplish.
Receiver arguments within traits, that are declared in terms of associated types, would therefore impose compile-time polymorphism on the vast majority of traits for use in the language (namely those that contain receiver arguments) – including traits that the programmer would wish to use to achieve run-time polymorphism. The programmer could only opt out of this dictate if the language would also offer him declarations with a type erasure capability.
We consider this to be a poor approach. Rather, our design philosophy is
-
to not bias Fortran’s traits facility in favor of one type of polymorphism over the other,
-
to not make run-time polymorphism any harder to use than compile-time polymorphism, and
-
to not burden prototype implementations of our traits with the requirement of having to implement type erasure in a compiler from the very beginning.
Don’t get me wrong. We will support type erasure for Fortran’s class(Trait)
(similar to Swift’s any Trait
) declarations for all the run-time polymorphic use cases involving associated types where this is truly required. But receivers within traits is not one of them.
We also wish to be able to introduce such support in an incremental manner during the development of the LFortran compiler (as it was the case during the development of the Swift compiler), which would be impossible with the above approach. Such incremental development is also the reason why, in our compiler prototype, we will initially allow traits with associated types to merely serve as generics constraints, and to relax this restriction only later on.
Now that I’ve outlined all of the problems, in two long posts, what is their actual solution? The solution is simple. It is to realize that all these troubles are entirely artificial, being caused by superfluous declarations of receiver arguments within traits. With the abolishment of receivers from traits, these problems disappear all at once (thereby confirming the soundness of the underlying reasoning).
Indeed, the vast majority of OO languages that support polymorphic interfaces/protocols/traits (namely Objective-C, Java, C#, Kotlin, D, Swift, Go, etc.) seem to get this right: they do not include receiver declarations in their polymorphic interfaces.
Importantly, this appears to be a basic principle that should be followed. It is not simply the result of some coincidental implicit passing of receivers in these languages. At least one of these languages, namely Go, uses explicitly passed receivers for its method implementations, and still doesn’t allow receivers to appear in its traits/interfaces. This fact indicates that implicit receiver passing rather happens to fulfill this principle automatically, whereas with explicit receiver passing substantially more care is required.
This becomes more obvious if one considers also the fact that Go’s interfaces appear to be straightforward translations (into the framework of a statically typed language) of Smalltalk’s and Objective-C’s message selectors, i.e. the (by definition) receiver-less parts of messages.