Constructors a la AbstractArray

question
type

#1

Suppose I have a regular grid defined as follows:

abstract type AbstractDomain{N,T<:Real} end

struct RegularGrid{N,T<:Real} <: AbstractDomain{N,T}
  dims::Dims{N}
  origin::NTuple{N,T}
  units::NTuple{N,T}
end

I am trying to define an outer constructor a la AbstractArray where I can specify the dimensions of the grid with a list of integers:

RegularGrid{T}(dims::Dims{N}) where {N,T<:Real} =
  RegularGrid{N,T}(dims, (zeros(T, length(dims))...), (ones(T, length(dims))...))

At the call site I would expect something like the following to work:

RegularGrid{Float64}(2,3) # creates a 2x3 grid with Float64 coordinates

What is the implementation that matches this interface?


#2

You need to use an inner constructor to get this kind of behavior:

struct Foo{N, T}
    x::Array{T, N}

    Foo{T}(dims::Vararg{<:Integer, N}) where {T, N} = new{N, T}(zeros(T, dims...))
end

julia> Foo{Float64}(2, 3)
Foo{2,Float64}([0.0 0.0 0.0; 0.0 0.0 0.0])


#3

I found that this has become a lot more logical since Julia 0.6.


#4

Thank you @rdeits, out of curiosity, why is that the case that we can only achieve this behavior with inner constructors?


#5

@rdeits, I am having a hard time trying to adapt your snippet to my specific example, could you please give a hand?

struct RegularGrid{N,T<:Real} <: AbstractDomain{N,T}
  dims::Dims{N}
  origin::NTuple{N,T}
  units::NTuple{N,T}

  function RegularGrid{T}(dims::Vararg{<:Integer,N}) where {N,T}
    new{N,T}(dims, (zeros(T,length(dims))...), (ones(T,length(dims))...))
  end
end

#6

The trouble is that you’re defining the type as RegularGrid{N,T}, but trying to use a shorthand RegularGrid{T}.

Swap the order of the parameters in your definition if you want that shorthand.


#7

First, there is another problem with your code. The constructor that should work with your definition is RegularGrid{Float64}((2,3)), i.e. with the dims specified as a tuple. However, this yields an error, because your type was defined as RegularGrid{N,T} with N first. But if you want an incomplete type specification with only T, you need to have T as first parameter (note AbstractArray{T,N}).

Then, to get the constructor you want, you need to use Vararg, but this does not need to be an inner constructor.


abstract type AbstractDomain{T<:Real,N} end

struct RegularGrid{T<:Real,N} <: AbstractDomain{T,N}
  dims::Dims{N}
  origin::NTuple{N,T}
  units::NTuple{N,T}
end

RegularGrid{T}(dims::Dims{N}) where {N,T<:Real} =
  RegularGrid{T,N}(dims, (zeros(T, length(dims))...), (ones(T, length(dims))...))

RegularGrid{T}(dims::Vararg{Int,N}) where {N,T<:Real} = RegularGrid{T}(dims)

Note that the last definition looks like it is calling itself. This is not true however. In the left, dims is a Vararg, i.e. multiple arguments. If you would not be interested in N, you could write it as dims::Int.... On the right hand side, when using the vararg dims in the function body, the different arguments to which it correspond will be collected in a tuple. Hense, on the right hand side dims is now NTuple{N,Int}===Dims{N}. This function call will then be passed on to the constructor you had written.


#8

That is something I didn’t know. I don’t remember for instance C++ having these types of issues with ordering of the parameters… Is it something that can be improved in future releases of Julia or it will stay like this?


#9

That is a very nice answer @juthohaegeman, thank you for going step-by-step explaining the nuances of the code. I am digesting and experimenting locally.


#10

@juthohaegeman, when we write an inner constructor like this:

struct Foo{T}
  a::Vector{T}
  function Foo{T}(a)
    new(a)
  end

are we saying the the argument a has type Any? Is it good practice to always include the types in the arguments of inner constructors or they are treated differently?


#11

The code after all your help looks as follows:

struct RegularGrid{T<:Real,N} <: AbstractDomain{T,N}
  dims::Dims{N}
  origin::NTuple{N,T}
  units::NTuple{N,T}

  function RegularGrid{T,N}(dims, origin, units) where {N,T<:Real}
    @assert all(dims .> 0) "dimensions must be positive"
    @assert all(units .> 0) "units must be positive"
    new(dims, origin, units)
  end
end

RegularGrid{T}(dims::Dims{N}) where {N,T<:Real} =
  RegularGrid{T,N}(dims, (zeros(T,length(dims))...), (ones(T,length(dims))...))

RegularGrid{T}(dims::Vararg{<:Integer,N}) where {N,T<:Real} = RegularGrid{T}(dims)

My only question now is about the inner constructor, should I annotate the type of the arguments or this is not necessary? I want to follow good Julian practices, still adapting to the syntax and programming style.


#12

I don’t think you need to annotate the arguments by types in the inner constructor (in this case) since they can only be of the type that you want by how the fields of your struct are specified. This might not always be the case though, and I don’t know what standard Julia style is.

instead of (zeros(T,length(dims))...) you could also use ntuple(n->zero(T), Val{N}) (on Julia 0.6) or ntuple(n->zero(T), Val(N)) (on Julia 0.7-) to get inferred types of the argument origin (and similar for units). Not that it matters much, the result of the function (i.e. a new RegularGrid{T,N} instance) will anyway be correctly inferred.


#13

@juthohaegeman is correct about not needing to annotate the inner constructor in your example (and it is best practice to omit what is not needed … as would make your panache less easily seen).