Best practices for defining inner constructors for parametric types

In much of my development, I have parametric types that need inner constructors to be defined with some validation code:

struct MyType{T<:Number}
    a::T
    b::T
    # Inner constructor goes here.
end

However, I’m not sure whether there’s a reason we should prefer to define the inner constructor with or without type parameters. Either we could use type parameters:

function MyType{T}(a, b) where T
    # Validation logic goes here
    return new(a,b)
end

Or we could define the inner constructor without them, if we can infer what T should be from the argument types:

function MyType(a, b)
    T = promote_type(typeof(a), typeof(b))
    # Validation logic goes here
    return new{T}(a, b)
end

With either case, to get both a constructor for MyType and MyType{T}, we’d have to define the missing method as an outer constructor. Is there a broad, overarching reason to implement one over the other as the inner constructor? Or should it be handled on a case-by-case basis? Or does it matter at all?

In this example, it’ll almost work the same, only differences being which of the default constructors you have explicitly replaced.

Method type parameters are useful for enforcing that the call provides a value for the parameter, enforcing that multiple arguments share a parameter in some way, or forcing specialization on arguments that aren’t always automatically (Function, types, Vararg). If you don’t need those, you don’t have to use them.

Pretty sure that defining an inner constructor prevents any default constructors from being defined.

2 Likes

In Julia the convention (sadly not enforced by the compiler) is that, for any type S (either abstract or concrete) and any args, when calling S(args...), if the call returns it should return a value that isa S. So the concrete type constructor (MyType{T}) is, in a sense, more basic than the abstract type constructor (MyType). So IMO the former option in your question is much preferable than the latter.

Furthermore, the former option may make it easier for the programmer to be easy on the compiler (regarding type inference) when necessary, although this would rarely matter.

2 Likes