9
votes

So here is the setting. I have multiple composite types defined with their own fields and constructors. Lets show two simplified components here:

type component1
    x
    y
end

type component2
    x
    y
    z
end

Now I want to define a new type such that It can save an array of size K of previously defined composite types in it. So it is a parametric composite type with two fields: one is an integer K, and the other is an array of size K of the type passed.

type mixture{T}
    components::Array{T, 1}
    K::Int64

    function mixture(qq::T, K::Int64)
        components = Array{typeof(qq), K}
        for k in 1:K
            components[k] = qq
        end
        new(components, K)
    end
end

But this is not the correct way to do it. Because all the K components are referring to one object and manipulating mixture.components[k] will affect all K components. In python I can remedy this with deepcopy. But the deepcopy in Julia is not defined for composite types. How do I solve this problem?

1

1 Answers

13
votes

An answer to your specific question:

When you define a new type in Julia, it is common to extend some of the standard methods in Base to your new type, including deepcopy. For example:

type MyType
    x::Vector
    y::Vector
end
import Base.deepcopy
Base.deepcopy(m::MyType) = MyType(deepcopy(m.x), deepcopy(m.y))

Now you can call deepcopy over an instance of MyType and you will get a new, truly independent, copy of MyType as the output.

Note, my import Base.deepcopy is actually redundant, since I've referenced Base in my function definition, e.g. Base.deepcopy(m::MyType). However, I did both of these to show you the two ways of extending a method from Base.

Second note, if your type has lots of fields, you might instead iterate over the fields using deepcopy as follows:

Base.deepcopy(m::MyType) = MyType([ deepcopy(getfield(m, k)) for k = 1:length(names(m)) ]...)

A comment on your code:

First, it is standard practice in Julia to capitalize type names, e.g. Component1 instead of component1. Of course, you don't have to do this, but...

Second, from the Julia docs performance tips: Declare specific types for fields of composite types. Note, you can parameterize these declarations, e.g.

type Component1{T1, T2}
    x::T1
    y::T2
end

Third, here is how I would have defined your new type:

type Mixture{T}
    components::Vector{T}
    Mixture{T}(c::Vector{T}) = new(c)
end
Mixture{T}(c::Vector{T}) = Mixture{eltype(c)}(c)
Mixture(x, K::Int) = Mixture([ deepcopy(x) for k = 1:K ])

There are several important differences here between my code and yours. I'll go through them one at a time.

Your K field was redundant (I think) because it appears to just be the length of components. So it might be simpler to just extend the length method to your new type as follows:

Base.length(m::Mixture) = length(m.components)

and now you can use length(m), where m is an instance of Mixture to get what was previously stored in the K field.

The inner constructor in your type mixture was unusual. Standard practice is for the inner constructor to take arguments that correspond one-to-one (in sequence) to the fields of your type, and then the remainder of the inner constructor just performs any error checks you would like to be done whenever initialising your type. You deviated from this since qq was not (necessarily) an array. This kind of behaviour is better reserved for outer constructors. So, what have I done with constructors?

The inner constructor of Mixture doesn't really do anything other than pass the argument into the field via new. This is because currently there aren't any error checks I need to perform (but I can easily add some in the future).

If I want to call this inner constructor, I need to write something like m = Mixture{MyType}(x), where x is Vector{MyType}. This is a bit annoying. So my first outer constructor automatically infers the contents of {...} using eltype(x). Because of my first outer constructor, I can now initialise Mixture using m = Mixture(x) instead of m = Mixture{MyType}(x)

My second outer constructor corresponds to your inner constructor. It seems to me the behaviour you are after here is to initialise Mixture with the same component in every field of components, repeated K times. So I do this with a loop comprehension over x, which will work as long as the deepcopy method has been defined for x. If no deepcopy method exists, you'll get a No Method Exists error. This kind of programming is called duck-typing, and in Julia there is typically no performance penalty to using it.

Note, my second outer constructor will call my first outer constructor K times, and each time, my first outer constructor will call my inner constructor. Nesting functionality in this way will, in more complicated scenarios, massively cut-down on code duplication.

Sorry, this is a lot to take in I know. Hope it helps.