Inner constructors with parametric types, can't seem to get them to work for me

Say I have a type that looks the following:

julia> struct Test{T, N, A} <: AbstractArray{T, N}
           a::Dict{NTuple{N, Int}, String}
           b::Type{T}
           c::A
           d::Array{T, 2}

           function Test{T, N, A}(a, b, c) where {T, N, A}
               new(a, b, c, Array{T}(undef, 10, 10))
           end
       end

I try instantiating it and get a method error

julia> Test(Dict((1,2,3,4)=>"test"), Float64, 6)
ERROR: MethodError: no method matching Test(::Dict{NTuple{4,Int64},String}, ::Type{Float64}, ::Int64)
Stacktrace:
 [1] top-level scope at REPL[3]:1

However, an almost identical type with no inner constructor works as I expect:

julia> struct Test2{T, N, A} <: AbstractArray{T, N}
           a::Dict{NTuple{N, Int}, String}
           b::Type{T}
           c::A
           d::Array{T, 2}
       end

julia> Test2(Dict((1,2,3,4)=>"test"), Float64, 6, Array{Float64}(undef, 10, 10));

Why is this happening? I’m not use I grok the difference and the Julia docs didn’t clear it up for me.

I would like to use the first approach because the field d is a cache that is automatically created and users shouldn’t have to deal with it.

EDIT: Using Julia 1.2

1 Like

Your inner constructor defines a method with type parameters in the signature (the { ... } part). Unless you really need it, I would recommend just using types from the call signature. Also, you don’t need to store the type T in a slot b explicitly. I would do something like this instead:

struct Test{T, N, A} <: AbstractArray{T, N}
    a::Dict{NTuple{N, Int}, String}
    c::A
    d::Array{T, 2}
    function Test(::Type{T}, a::Dict{NTuple{N,Int},String}, c::A) where {T, N, A}
        new{T,N,A}(a, c, Array{T}(undef, 10, 10))
    end
end

# this works, but printing fails because you need to define a method 
# for `getindex` and `size` so that # `show` works, or alternatively 
# define the latter
z = Test(Float64, Dict((1,2,3,4)=>"test"), 6);
3 Likes

As @Tamas_Papp said, your inner constructor has type parameters in {} in its signature, so you have to call it with those parameters specified at the call site:

julia> Test{Int, 1, Int}(Dict(), Int, 1)

(note that trying to display the result fails because your type claims to be an AbstractArray but doesn’t implement size() or getindex()).

The reason you don’t usually have to do this is that if you do not define an inner constructor, Julia does it for you. For example, if you have a type like:

julia> struct Foo{T}
        x::T
      end

then Julia automatically defines a default inner constructor which looks something like:

Foo(x::T) where {T} = new{T}(x)

But if you define an inner constructor, then it replaces that default. So you might want to do something like:

julia> struct Bar{T}
         x::T
         function Bar{T}(x) where {T}
           println("inner constructor stuff goes here")
           new{T}(x)
         end
       end

       # This is the constructor which gets called when you do Bar(...)
       # instead of Bar{T}(...)
       Bar(x::T) where {T} = Bar{T}(x)
Bar

julia> Bar(1)
inner constructor stuff goes here
Bar{Int64}(1)

julia> Bar{Float64}(1)
inner constructor stuff goes here
Bar{Float64}(1.0)
2 Likes

Incidentally, I don’t think an explicitly parametric constructor is a common use case for most composite types that don’t contain extra parameters that are not obtainable from the arguments. In the case above, a

Test{T}(a, c)

constructor might make sense though.