Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stack overflow when I am trying to make a composite type with a matrix as a field

Tags:

julia

In the following code, I have a composite type, and in my real code several of the fields are matrices. In this example is only 1. I keep getting stack overflow when I try constructing the composite type. Here is the code sample:

struct Tables
    eij::Array{Float64,2}
end

Tables(eij::Array{Float64,2}) = Tables(eij) # works if this is commented out
x = 5   # arbitrary matrix dimension    
e_ij = Array{Float64,2}(undef,x,x)

for i=1:x
    for j=1:x
        e_ij[i,j] = i*j/2.3     #dummy numbers, but not the case in real code
    end
end

vdwTable = Tables([e_ij[i,j] for i=1:x,j=1:x])

I use the temporary variable e_ij to make the matrix first since I don't want the composite Tables to be mutable. So, my reasoning is that by generating the tables first in dummy variables like e_ij, I can then initialize the immutable Tables that I really want.

If I comment out the outer constructor for the struct Tables it works. However, I actually want to have several different outer constructors for cases where different fields are not passed data to be initialized. In these cases, I want to give them default matrices.

The error I get is the following: ERROR: LoadError: StackOverflowError: on the line vdwTable = Tables([e_ij[i,j] for i=1:x,j=1:x])

like image 948
Charlie Crown Avatar asked Dec 23 '22 01:12

Charlie Crown


2 Answers

When you define a composite type, an inner constructor is automatically defined, so this:

struct Tables
    eij::Array{Float64,2}
end

is equivalent to this:

struct Tables
    eij::Array{Float64,2}
    Tables(eij::Array{Float64,2}) = new(eij)
end

When you define this outer constructor

Tables(eij::Array{Float64,2}) = Tables(eij)

you get in the way of the inner constructor. Your outer constructor just calls itself recursively until you get a stack overflow.

Doing this, on the other hand,

Tables(eij) = Tables(eij)

is actually equivalent to this:

Tables(eij::Any) = Tables(eij)

so when you subsequently call

vdwTable = Tables([e_ij[i,j] for i=1:x,j=1:x])

it then just ignores your outer constructor, because there is a more specific method match, namely the inner constructor. So that particular outer constructor is quite useless, it will either be ignored, or it will recurse until stack overflow.

The simplest solution is: just don't make an outer constructor. If you do need an outer one to enforce some conditions, make sure that it doesn't shadow the inner constructor by having the same type signature. For example,

Tables() = Tables(zero(5, 5))

should work.

I would probably do it like this, though:

struct Tables
    eij::Array{Float64,2}
    Tables(eij=zeros(5, 5)) = new(eij)
end

For your second example, with two fields, you can try this:

struct Tables
    eij::Array{Float64,2}
    sij::Array{Float64,2}
    Tables(eij=zeros(5,5), sij=zeros(5,5)) = new(eij, sij)
end

Your inputs will be converted to Float64 matrices, if that is possible, otherwise an exception will be raised.

like image 96
DNF Avatar answered Jun 02 '23 00:06

DNF


DNF gave a proper explanation so +1. I would just like to add one small comment (not an answer to the question but something that is relevant from my experience), that is too long for a comment.

When you omit specifying an inner constructor yourself Julia automatically defines one inner and one outer constructor:

julia> struct Tables
           eij::Array{Float64,2}
       end

julia> methods(Tables)
# 2 methods for generic function "(::Type)":
[1] Tables(eij::Array{Float64,2}) in Main at REPL[1]:2
[2] Tables(eij) in Main at REPL[1]:2

while defining an inner constructor suppresses definition of the outer constructor:

julia> struct Tables
           eij::Array{Float64,2}
           Tables(eij::Array{Float64,2}) = new(eij)
       end

julia> methods(Tables)
# 1 method for generic function "(::Type)":
[1] Tables(eij::Array{Float64,2}) in Main at REPL[1]:3

So the cases are not 100% equivalent. The purpose of the auto-generated outer constructor is to perform an automatic conversion of its argument if it is possible, see e.g. (this is the result in the first case - when no inner constructor was defined):

julia> @code_lowered Tables([true false
       true false])
CodeInfo(
1 ─ %1 = (Core.apply_type)(Main.Array, Main.Float64, 2)
│   %2 = (Base.convert)(%1, eij)
│   %3 = %new(Main.Tables, %2)
└──      return %3
)

while in the second case the same call would throw a method error.

like image 42
Bogumił Kamiński Avatar answered Jun 01 '23 23:06

Bogumił Kamiński