Inner constructor – checking for equal lengths

I’ve defined a custom type, which assumes implicitly that 4 of its objects have the same length. Is there a standard way to check for this (or is it a bad idea)? I tried an inner constructor with length check, but it fails for some reason (and looks bloated).

using Rotations
using StaticArrays
struct Cluster{T1,T2,T3}

    positions::Vector{SVector{3,T1}}
    rotations::Vector{UnitQuaternion{T2}}
    materials::Vector{String}
    sizes::Vector{SVector{3,T3}}
    type::String
    
     # this fails, not sure why
    # Cluster(p,r,m,s,t) = (length(p) == length(r) == length(s) == length(m)) ?  new(p,r,m,s,t) : error("length mismatch") 
end


N = 3
p = [@SVector rand(3) for _ ∈ 1:N]
r = [UnitQuaternion(RotZYZ(0,0,0)) for  _ ∈ 1:N]
s = [SVector(1,2,3) for  _ ∈ 1:N]
m = ["Au" for _ ∈ 1:N]
Cluster(p,r,m,s,"type")

Cluster(p,r[1:2],m,s,"type") # would like this to fail

You did it nearly right, but you missed some syntax details about parametric types.
For the example below I have removed the dependency from Rotations as I don’t want to install it right now, but you should be able to change it easily for your needs.
For the details read here: Constructors · The Julia Language

using StaticArrays

struct Cluster{T1,T2,T3}
    positions::Vector{SVector{3,T1}}
    rotations::Vector{Vector{T2}}
    materials::Vector{String}
    sizes::Vector{SVector{3,T3}}
    type::String
    Cluster{T1,T2,T3}(p,r,m,s,t) where {T1,T2,T3} = (length(p) == length(r) == length(s) == length(m)) ?  new(p,r,m,s,t) : error("length mismatch") 
end
Cluster(p::Vector{SVector{3,T1}},r::Vector{Vector{T2}},m,s::Vector{SVector{3,T3}},t) where {T1,T2,T3} = Cluster{T1,T2,T3}(p,r,m,s,t)

N = 3
p = [@SVector rand(3) for _ in 1:N]
r = r=[ rand(Float64,3) for _ in 1:N ]
s = [SVector(1,2,3) for  _ in 1:N]
m = ["Au" for _ in 1:N]
Cluster(p,r,m,s,"type")

r2 = r=[ rand(Float64,3) for _ in 1:2 ]
Cluster(p,r2,m,s,"type")
2 Likes

I think for parametric types, new needs to be parameterized as well. Like new{X,Y,Z}(args...) where XYZ need to fit your type parameters

From the docs:

The syntax new{T,S} allows specifying parameters for the type to be constructed, i.e. this call will return a SummedArray{T,S} . new{T,S} can be used in any constructor definition, but for convenience the parameters to new{} are automatically derived from the type being constructed when possible.

In this case, it’s a can.

2 Likes

Ah I see, I’ve had problems with that before

Yes, this is nothing I can ever do right just by remembering. It’s always a look-it-up, or where-have-I-done-It-before-? thing. I pretty sure, every language has these types of syntax structures somewhere. Typically I tend to avoid those things, because I can’t read them well, say half a year later, when I have to come back, because of some issues. I always prefer the easy style even if it isn’t elegant or more performant (if not needed).

Thanks; I am bit confused why an external constructor is needed as well as an inner one :confused:

The example at
https://docs.julialang.org/en/v1/manual/constructors/#man-inner-constructor-methods

makes it sound easier to add constraints; I guess it’s the parametric types that bring the extra complication.

It’s not needed, but the types T1,T2,T3 can’t be infered automatically from the Vector types you are using here. Without the outer constructor you get:

julia> Cluster(p,r,m,s,"type")
ERROR: MethodError: no method matching Cluster(::Vector{SVector{3, Float64}}, ::Vector{Vector{Float64}}, ::Vector{String}, ::Vector{SVector{3, Int64}}, ::String)

For this to work without the outer constructor you have to give the types T1,T2,T3 explicitly:

julia> Cluster{Float64,Float64,Int64}(p,r,m,s,"type")

Let’s change to a more minimal example:

julia> struct Example1{T1}
           v::T1
           function Example1(v::T1) where T1
               new{T1}(v)
           end
       end

julia> Example1(2)
Example1{Int64}(2)

julia> struct Example2{T1}
           v::Vector{T1}
           function Example2{T1}(v) where T1
               new(v)
           end
       end

julia> Example2([2,3])
ERROR: MethodError: no method matching Example2(::Vector{Int64})
Stacktrace:
 [1] top-level scope
   @ REPL[13]:1

julia> Example2{Int64}([2, 3])
Example2{Int64}([2, 3])

For example2 an outer constructor would be convenient:

julia> Example2(v::Vector{T1}) where T1 = Example2{T1}(v)
Example2

julia> Example2([2,3])
Example2{Int64}([2, 3])

This would be a way to do it only with an inner constructor:

julia> struct Example3{T1}
           v::Vector{T1}
           function Example3(v::Vector{T1}) where T1
               new{T1}(v)
           end
       end

julia> Example3([2,3])
Example3{Int64}([2, 3])

Now the new{T1} is needed, as in Example1, because it isn’t specified at the definition of the constructor. If it is specified in the constructor, like in Example2, the calling must be the same, as in Example2{Int64}([2, 3]).

Now in your case, this would look like:

using StaticArrays

struct Cluster{T1,T2,T3}
    positions::Vector{SVector{3,T1}}
    rotations::Vector{Vector{T2}}
    materials::Vector{String}
    sizes::Vector{SVector{3,T3}}
    type::String
    Cluster(
		p::Vector{SVector{3,T1}},
		r::Vector{Vector{T2}},
		m::Vector{String},
		s::Vector{SVector{3,T3}},
		t::String
		) where {T1,T2,T3} = (length(p) == length(r) == length(s) == length(m)) ?  new{T1,T2,T3}(p,r,m,s,t) : error("length mismatch")
end

N = 3
p = [@SVector rand(3) for _ in 1:N]
r = r=[ rand(Float64,3) for _ in 1:N ]
s = [SVector(1,2,3) for  _ in 1:N]
m = ["Au" for _ in 1:N]
Cluster(p,r,m,s,"type")

r2 = r=[ rand(Float64,3) for _ in 1:2 ]
Cluster(p,r2,m,s,"type")

I have tried a few other combinations of e.g. Example{T1}(...) and new{T1,...}, but above two possible ways are the only ones which work. Well, I admit, I can’t really explain, why it is, as it is, but my guess is, at some point a specific syntax must be fixed, even if there are other logical ways to express explicitly what you want (from the point of the developer), or in other words, not every possible expression must be made valid.

2 Likes

I don’t know enough of the language but it feels to me that the compiler should have enough information from the types attached to each field (sizes::Vector{SVector{3,T3}} and so on) for the default/generic inner constructor to not need repeating them. But maybe more complex situations make it a requirement.

Yes, I think the full answer is here: Constructors · The Julia Language

When an inner constructor is not explicitly defined, you get two default constructors:

  1. an inner constructor with type parameters
  2. an outer constructor that determines type parameters automatically from arguments (and then calls the appropriately typed inner constructor)

So from the manual, the following struct definition

struct Point{T<:Real}
    x::T
    y::T
end

is equivalent to the following:

struct Point{T<:Real}
    x::T
    y::T
    Point{T}(x,y) where {T<:Real} = new(x,y)
end
Point(x::T, y::T) where {T<:Real} = Point{T}(x,y)

However, defining an explicit inner constructor suppresses the generation of the default constructors.
The rationale being that since you are overriding the defaults, you want full control.

See Constructors · The Julia Language for an example.

2 Likes

Good point, I missed the part, where no inner constructor yields default constructors with proper infered types. But OP wants to restrict the construction with the constraint that all vectors are of equal length, which is only possible in the inner constructor.