This looks very nice! I think this syntax is for the most part very clean, and it would be a powerful addition to the language! I’ve not fully digested everything, but I have a few comments about various bits:
I know this is a bit of a distraction, but your Rust example is overly complicated, which might have impacted your conclusions from it. For instance, you say there’s no way to cast generic types without writing your own methods, but there’s Num::FromPrimitive
in the Num
crate you’re already using, which means zero new code needs to be written for Averager
to work with f32
, for example. You can also get away from using the Num
crate entirely with two (admittedly quite long!) lines:
pub trait INumeric: Default + Add<Output = Self> + AddAssign + Div<Output = Self> + Copy + From<u16> {}
impl<T: Default + Add<Output = T> + AddAssign + Div<Output = T> + Copy + From<u16>> INumeric for T {}
These built-in traits are sufficient to get the example to work, using T::default()
instead of T::new(0)
, and (x.len() as u16).into()
to cast the slice length to T
.
You can also very easily vastly simplify the creation of the averagers by moving the call to Box::new
into the custom constructors, and relying on Rust’s powerful type inference to work out the types of avi, avf
:
let (avi, avf) = match key {
1 => (Averager::new(SimpleSum {}), Averager::new(SimpleSum {})),
2 => (
Averager::new(PairwiseSum::new(SimpleSum {})),
Averager::new(PairwiseSum::new(SimpleSum {})),
),
_ => {
println!("Case not implemented!");
return;
}
};
Note that I don’t name i32
or f64
here at all – they’re inferred from the calls to avi.average()
, no explicit instantiation needed.
It also seems like the reserved init
function name isn’t really necessary, we already have a mechanism to specify constructors of types with interface TypeName
. The problem with adding reserved names is that they will inevitably clash with existing uses. The lookup mechanism is a little strange to me too, needing to find the associated type based on the return type of the function.
I think there’s a big flaw in not requiring an explicit object argument in trait function signatures. You point to this being how it works in Swift, but in Swift no explicit self
is required to access instance members. Fortran does require an explicit dummy argument for the instance, and so it should be included in the trait function signatures. Not requiring it makes it harder to a) look up the correct signature (either for a human or machine), and b) verify all the dummy argument attributes match. For instance, your definition of IAppendable
in 5.1.3:
abstract interface :: IAppendable
typedef, deferred :: Element
subroutine append(item)
type(Element), intent(in) :: item
end subroutine append
end interface IAppendable
would be much clearer if it were:
abstract interface :: IAppendable
typedef, deferred :: Element
subroutine append(self, item)
type(IAppendable), intent(inout) :: self
type(Element), intent(in) :: item
end subroutine append
end interface IAppendable
These are very different signatures, and I don’t really see the benefit from skipping the self
argument in the signature.
It also seems inconsistent to have some generic procedures not require listing the generic types. For instance, you have the two generic procedures in different sections:
function average{INumeric :: T}(self,x) result(a)
type(Averager), intent(in) :: self
type(T), intent(in) :: x(:)
type(T) :: a
a = self%drv%sum(x) / T(size(x))
end function average
subroutine append(self, item)
type(Vector{U}), intent(inout) :: self
type(U), intent(in) :: item
self%elements = [self%elements,item]
end subroutine append
Why doesn’t append
need to be subroutine append{IAnyType :: U}(self, item)
? It seems like the only way the compiler can work out that append
is a generic procedure is by parsing the dummy arguments and seeing Vector{U}
. Wouldn’t it be more consistent to require listing the generic types in the signature here too?
The type-inferring-assignment operator :=
is very nice, but I think you’re going to have to nail down a lot of formal specifics if it’s going to make any progress. For example, what scope is the name bound to, do constructs that aren’t currently scopes now become scopes:
do i := 1, 10
j := 2 * i
end do
! Is i still valid here?
! Is j still valid here?
I can imagine this small feature alone potentially being quite controversial, so you may not want to bind it too closely to the rest of your proposal!