Outer-only constructors and concrete types

Suppose I have a type like this, taken from the docs on outer-only constructors:

struct Foo{T<:Number,S<:Number} <: SuperType{T}
    data::Vector{T}
    sum::S
    function Foo(a::Vector{T}) where T
        S = widen(T)
        new{T,S}(a, sum(S, a))
    end
end

The type S in the above is not independent of T, in the sense that there is no way to make a Foo{Float32,Int32} or whatever. If I call the constructor using

foo32 = Foo(zeros(Float32,2))

I will always get a Foo{Float32, Float64}.

However, Foo{Float32} is itself not a concrete type, even though T = Float32 is enough to fully determine what type foo32 will be above.

Is there a way to make some parametric subtype or alias such that Foo{Float32}, or some variation thereof, becomes concrete?

I want to do something sort of like this (which doesn’t work):
ConcreteFoo{T} = Foo{T,widen{T}}

I think the following could help achieve your goal

julia> footype(::Type{T}) where {T<:Number} = Foo{T,widen(T)}
footype (generic function with 1 method)

julia> footype(Int)
Foo{Int64, Int128}

julia> isconcretetype(footype(Int))
true
2 Likes

Thanks. I agree that that will return the fully specified type to me based on only T. What I’m after though is a completely new type, not a function returning the existing type.

I don’t think there’s a way to do what you’re asking for. Type aliases with fewer type parameters must make the removed type parameters constant e.g. Vector = Array{T, 1} where T. In your case, both T and S vary, even if S is intended to entirely depend on T. There is a good reason for this: S can’t be specified or proven in the type to entirely depend on T. For one, the intended dependence is done by a constructor method that is separate from, albeit related to, the type. For another, the dependence requires the constructor method to be pure, which can’t be guaranteed. For example, I could define a widen method that returns various types depending on a global variable or a random selection, so the same T could result in different S across instances. So for guaranteed precision, the type does need both T and S as parameters.

Still, you can make assumptions about S’s dependency on T to make things easier to read and write. lferrant’s footype(Int) is the straightforward way to make Foo{Int, widen(Int)} easier to write (forget the {} syntax, that’s for writing the precise types). You can overload Base.show to change how the type is printed. I haven’t actually done this before so I can’t claim this is best practice, but:

# changes how the instance foo32 is printed
function Base.show(io::IO, me::Foo{T,S}) where {T,S}
    print(io, "Foo{", T, "~}(", me.data, ", ", me.sum, ")")
end

# changes how the type typeof(foo32) is printed
# I read this is unadvisable but I'm not sure exactly why
# this does prevent printing of S in the precise type
function Base.show(io::IO, me::Type{Foo{T,S}}) where {T,S}
    print(io, "Foo{", T, "~}")
end
julia> foo32
Foo{Float32~}(Float32[0.0, 0.0], 0.0)

julia> typeof(foo32)
Foo{Float32~}

julia> Foo{Float32}  # this is still abstract, and S is obscured
Foo{Float32~} where S<:Number

Thanks both.