Type annotations in inner constructor

Hi there.
In this struct want to enforce some invariants, so I check them with an inner constructor. But I also want to use all the type annotations. This leads to a lot of repetition, which is kind of ugly.

struct LRNData
    data::AbstractMatrix{Float64}
    column_types::AbstractArray{LRNCType, 1}
    keys::AbstractArray{T, 1} where T <: Integer
    names::AbstractArray{S, 1} where S <: AbstractString
    key_name::AbstractString
    comment::AbstractString

    function LRNData(
        data::AbstractMatrix{Float64},
        column_types::AbstractArray{LRNCType, 1},
        keys::AbstractArray{T, 1} where T <: Integer,
        names::AbstractArray{S, 1} where S <: AbstractString,
        key_name::AbstractString,
        comment::AbstractString
    )
        # ...
    end
end

Is there a more elegant way of doing this?
Thanks.

Yes, remove all those abstract types :smiley:

At first sight that might sound like a laughable suggestion! But declaring abstract fields like data::AbstractMatrix{Float64} is unlikely to be very helpful for performance, as the compiler will still usually need to invoke generic dispatch when calling functions on the fields of LRNData (that is, abstract field types tend to defeat the devirtualization optimization which is very important to well performing julia code). If you do want performance, you’ll need to either make LRNData into a parametric type like LRNData{T <: AbstractMatrix{Float64}, ...} and have data::T in the body, or alternatively convert the input matrices into concrete types like data::Matrix{Float64} using the constructor.

Alternatively, if your type annotations are for documentation or safety and you don’t care about performance, you may declare them only once in the constructor and leave the field types completely generic.

If both those suggestions fail and you’re making a lot of structs with the same type of code in the constructor you could consider a macro.

6 Likes

I think Chris’ post above says it all, but there’s also a good description in the manual, including many examples:

https://docs.julialang.org/en/v1/manual/performance-tips/index.html#Type-declarations-1

2 Likes

Thank you both for the replies.

I see that all the abstract types might hamper performance. I think I don’t need them anyways.

But if I want performance and type safety, I need type annotations in both the fields and the constructor?

I’m still somewhat of a novice with the type system, but here is how I would approach the problem:

struct LRNCType
#stuff here
end

struct LRNData{T<:Int,S<:AbstractString}
    data::Array{Float64,2}
    column_types::Array{LRNCType,1}
    keys::Array{T,1} 
    names::Array{S,1} 
    key_name::String
    comment::String
end

data = rand(2,2)
ct = [LRNCType() for i in 1:2]
Keys = [1,2]
names = ["name1","name2"]
key_name = "name"
comment = "comment"
LRN = LRNData(data,ct,Keys,names,key_name,comment)

I think this makes the types concrete and therefore performant while providing the type safety check you need. For example, it will throw an error if you make Keys an array of Float64. There is no need to create a constructor with type annotations. Unless you are doing something specific, the default constructor should suffice.

Probably not. Having concretely typed fields in your struct is important for performance, but your function arguments don’t need concrete types to perform well. For example, something like this:

struct Foo
  x::Matrix{Float64}
  
  function Foo(x::AbstractMatrix)
     do_some_checking(x)
     new(x)
  end
end

will give excellent performance.

Another way of thinking about this is that functions (including constructors) are automatically specialized on the actual types of whatever values you call them with (and every value always has a concrete type in Julia), while structs use exactly whatever field types you used when you declare them (whether those are abstract or concrete).

4 Likes

I’m enforcing some invariants for the struct, so I have to have a constructor that checks them at creation.

Thanks, that’s very enlightening. I now de-abstracted (?) the types for the struct and some more generic types in the function definition.