Inner contructor with parametric type

I am trying to create a structure where parameter types can either be given explicitly or are inferred automatically from the arguments. However, I also would like to define the inner constructor method, such as in the following example:

struct A{T}
    x :: T
    A{T}(a) where T = new(a^2)
end

When I explicitly give the type, it works fine, e.g.,

julia> A{Float64}(2)
A{Float64}(4.0)

However, when the type is not explicitly given, it raises the following error:

julia> A(2)
ERROR: MethodError: no method matching A(::Int64)
Stacktrace:
 [1] top-level scope at none:0

What do I need to do in order to be able to have parametric types that can be inferred or given explicitly with an explicit inner constructor?

Providing a simple parametric (outer) constructor that only infers the type should be enough here. For example:

struct A{T}
    x :: T
    y :: T
    A{T}(a) where T = new(a, a^2)
end

A(x::T) where {T} = A{T}(x)

yields:

julia> A{Float64}(2)
A{Float64}(2.0, 4.0)

julia> A(2)
A{Int64}(2, 4)
2 Likes

Thank you very much for your reply. But what about if I have multiple input arguments of different numeric types and the type can either be inferred from the calculation ou explicitly given, as in the following example?

julia> struct A{T}
           x :: T
           A{T}(a, b, c) where T = new(a*b*c)
       end

julia> A{Float64}(1.0, 2, 3)
A{Float64}(6.0)

julia> A(1.0, 2, 3)
ERROR: MethodError: no method matching A(::Float64, ::Int64, ::Int64)
Stacktrace:
 [1] top-level scope at none:0

I found the following solution but I find it a bit inelegant (or it’s probably not…):

(a::T1, b::T2, c::T3) where {T1, T2, T3} = A{promote_type(T1, T2, T3)}(a, b, c)

Is there a simpler solution?

If the type of the result of your expression looks like a promotion of the operands types, then promote_type can help you determine the correct type in the outer constructor:

struct A{T}
    x :: T
    A{T}(a, b, c) where T = new(a*b*c)
end

function A(args...)
    T = promote_type(typeof.(args)...)
    A{T}(args...)
end

Yielding:

julia> A{Float64}(2.0, 3, 4)
A{Float64}(24.0)

julia> A(2.0, 3, 4)
A{Float64}(24.0)

[EDIT: it looks like our posts crossed and you thought about that as well. I’m only keeping this first part here for the sake of consistency with the second part below]




Now, in more complex cases, you might want to let the compiler perform more complex inference. One way to do it would look something like:

__b_expr(a, b, c) = a * b / c

struct B{T}
    x :: T
    B{T}(a, b, c) where T = new(__b_expr(a, b, c))
end

function B(args...)
    T = typeof(__b_expr(args...))
    B{T}(args...)
end

Using promote_type in this case would not have promoted the result of the division like it should have, but relying on the compiler make the code behave like expected:

julia> B{Float64}(2, 3, 4)
B{Float64}(1.5)

julia> B(2, 3, 4)
B{Float64}(1.5)

julia> B(2, BigInt(3), 4)
B{BigFloat}(1.5)

It should even be statically inferred in most cases:

julia> @code_warntype B(2, BigInt(3), 4)
Variables
  args::Tuple{Int64,BigInt,Int64}
  T::Type{BigFloat}

Body::B{BigFloat}
[...]

I do not know of any more elegant way to do this, but there might exist some…

1 Like

Thank you very much. Your reply was really very useful.

Just one more question, please. In your example using __b_expr(a, b, c), if the type is not explicitly given, doesn’t the calculation of a * b / c is performed twice?

In the actual code that I am writing the calculations are much larger, performed in many lines, using vectors.

If the compiler is able to infer the type of __b_expr(args...), the type will be computed statically, and the expression will really be computed only once, by the inner constructor. This is for example shown above by @warntype being able to statically determine the correct parametric type.

However, if the return type of your computations is not inferred for whatever reason (type unstability for instance), then you’re right: this kind of technique would involve computing twice the same result (and also rely on the assumption that we get at least the same output type both times).

Sorry to bother you again.

In my case I use the following function to infer the type (I use Γ for type since I use T for temperature):

function Emis(args...)
    Γ = eltype(emis_vals(args...))
    return Emis{Γ}(args...)
end

Running @code_warntype Emis(6, [1, 20], [0.4, 0.8], 0.5, BigFloat(10)), I get the following output:

Variables
  #self#::Type{Emis}
  args::Tuple{Int64,Array{Int64,1},Array{Float64,1},Float64,BigFloat}
  Γ::Type{BigFloat}

Body::Emis{BigFloat}
1 ─ %1 = Core._apply(Main.emis_vals, args)::Array{BigFloat,1}
│        (Γ = Main.eltype(%1))
│   %3 = Core.apply_type(Main.Emis, Γ::Core.Compiler.Const(BigFloat, false))::Core.Compiler.Const(Emis{BigFloat}, false)
│   %4 = Core._apply(%3, args)::Emis{BigFloat}
└──      return %4

Is my code inferring the type statically (without doing the computations twice)?

Thank you again for your help.

It does infer the type statically, but, I guess, it does call emis_vals. It has to, because emis_vals is called on mutable arguments. For all the compiler knows, it may have been called for side effects.

2 Likes