Defining a subclass

I realise this is a newbie question, but I think it’s sort of central to the whole Julia ethos. I really like the idea of moving away from object-orientation’s emphasis on single dispatch, and that lattices of types perform a lot of the work of subclassing. And I quite see it makes sense that you can’t derive from a concrete type. And yet I’ve come to a point where I don’t really understand how to proceed.

I want to create a type State that is essentially a Vector{Complex}, but I want to give it extra constraints like maybe the sum of all components must always be equal to 1. Normally, in the object-riented world, I’d do this by subclassing and providing constructors and operations that preserve the constraint. But of course, in Julia, I can’t derive from Vector{Complex}. I could say State is a struct that contains a Vector{Complex}, but then I’d have to relay all existing Vector functionality from the field to the struct. The simplest idea seems to be to declare them equal:

State = Vector{Complex}

But then how do I create States? Julia won’t let me declare a State() constructor. I could create a method state() that creates States that fulfil the necessary constraints, but that seems a cheat. As far as I can see, I’m missing something essential about the Julia model here, but I don’t know what it is, and I’d love for someone to tell me.

Thanks.

Hi @Niall !
I think one way to solve your problem is to define a State struct that is a subtype of AbstractVector{Complex}.
Since Vector{Complex} is a concrete type, you cannot directly subtype it. However, you can make your struct usable as an abstract array simply by defining a few methods.

Thanks. So I’d wrap the Vector{Complex} inside a State, then map size(), getindex() and setindex() from the State to the wrapped Vector{Complex}, yes? To me, that sort of feels a little clumsy, isn’t it?

I’m terribly sorry, @gdalle, I didn’t mean your idea was clumsy - I meant the necessity of doing it seems unnecessarily clumsy.

There are packages to simplify this, e.g. GitHub - JeffreySarnoff/TypedDelegation.jl: Easily apply functions onto fields' values. Use a struct's fields as operands for operations on values of that type.

2 Likes

No worries! In my opinion that would be the julianic way, but there are probably simpler alternatives. Like if you only need checks when constructing a state, maybe you could make do with a get_state function that returns a Vector{Complex}?

1 Like

OK, so assuming you’re correct (that it is the Julian way), then this is a clear statement that having to delegate methods from wrapper to field is definitely preferable to subclassing from a struct. I can see that subclassing leads to changes in memory allocation that would hit performance, but it doesn’t seem to me that that problem would be insurmountable. Is there some other reason why delegation is to be preferred?

First of all I’m not 100% confident that I’m right about this ^^
Regarding your latest question, I’m not sure what you mean by “subclassing from a struct”. In Julia it is only possible to define children of abstract types, not structs

Just an offtopic remark. The reason why this is the Julian way is that it gives you access to all the methods that operate on the types of objects you created, from any package that defined any method that can operate on the supertype you subtyped. For example, if you create a simple wrapper for a vector:

julia> struct A{T} <: AbstractVector{T}
           x::Vector{T}
       end

julia> Base.length(a::A) = length(a.x)

julia> Base.size(a::A) = size(a.x)

julia> Base.setindex(a::A,i) = setindex(a.x,i)

julia> Base.getindex(a::A,i) = getindex(a.x,i)

Then suddenly all the functions that work on vectors work for your type:

julia> a = A([1,2,3,4])
4-element A{Int64}:
 1
 2
 3
 4

julia> mean(a)
2.5

julia> dot(a,a)
30

julia> a + a
4-element Vector{Int64}:
 2
 4
 6
 8

julia> a * a'
4×4 Matrix{Int64}:
 1  2   3   4
 2  4   6   8
 3  6   9  12
 4  8  12  16

but you can also overload any of them if you want them to have a different meaning (only do this for the types you have defined, otherwise is type-piracy):

julia> import Base: +

julia> +(a::A,b::A) = a - b
+ (generic function with 319 methods)

julia> a + a
4-element Vector{Int64}:
 0
 0
 0
 0

Even the functions invented for your new type after your type was defined will work. That is, once you do the clumsy work of defining the necessary wrapper functions, the whole Julia ecosystem will be available to work on your types, not only a subset of functions that where defined specifically for the original supertype by some particular implementation of it.

8 Likes

Not at all offtopic - thanks very much to you both. I do indeed understand that I can’t subtype from structs. I teach programming, and in relation to that I wanted to pinpoint precisely why that is the case. My current thinking is this …

I think it was precisely the very simplicity of my current situation (i.e.: wrapping a Vector simply in order to place a mathematical constraint on its elements) that made me wonder for the first time whether I’d got it right. If the AbstractVector interface were more method-rich, wrapping might become significantly more effort than being able to subclass Vector{Complex}, and that made me think about whether there might be more to the julian ban on subtyping structs than met the eye.

However, it seems that there isn’t, and I think probably my simple situation would be the exception rather than the usual case.

Thanks and best wishes,

Niall.

To my understanding, the (admittedly sometimes annoying) property, that each concrete type is also final (can’t be subtyped) enables a lot of the nice features of Juila, because it allows the compiler reason about the memory layout of variables based on their runtime-type.

For example, I can define composite types

struct Foo
    x::Float64
    y::Float64
end

struct Bar
    f::Foo
end

where both Foo and Bar are bit types, which means they have a fixed size in memory. It would not be possible for Bar to be a bit type if f::Foo might represent a subtype MyFoo <: Foo. In that case f would be a pointer rather than inlined memory . Forbidding this leads to nice things, such as arrays Vector{Foo} and Vector{Bar} which are continuous in memory and just consist of tightly packed Float64 bit-sequences.

6 Likes

Ah wait - I’ve just realised what it was that set me wondering. In my code I had simply set:

State = Vector{Complex}

and it surprised me that I couldn’t write a new, additional, constructor for it - I really do think that would be useful. However, it would then also be a new constructor of Vector{Complex}, and I realise that could bring its own problems. :slightly_smiling_face:

There are already good advices for the OP’s question here, but I think it’s also worth to ask: is a new type really needed?

If the goal is to define a constructor of a vector that complies some constraint, well, constructors are just functions, so you can just define a function that returns a “normal” Vector{Complex} enforcing the target constraint.

If it is important to ensure that x::State always complies that constraint, even if some modification to the wrapped vector is attempted, just defining a new type is not sufficient. Some function checkconstraint(::State) would be needed at some point to throw an error whenever an “illegal” modification is attempted, and a generic function checkconstraint(::AbstractVector) could be made instead, so that it can be applied to normal vectors too.

From my point of view, the main reason to make a State type that just wraps a vector of complex numbers with some constraint, is to be able to distinguish between such States and “unwrapped” vectors that happen to meet the constraint. For instance: if there is some function that should only work for States and not for other vectors, or if you want a function to do different things if they are applied to State vectors, so that you can take advantage of multiple dispatch (e.g. if the sum of two State vectors must transform the result so that it also meets the constraint).

Otherwise, perhaps you can just stick to Vector{Complex}, and save the trouble of adapting the AbstractVector interface to a new type. The idea of using an alias State = Vector{Complex} is good to simplify the code, but then you should choose another name for the “constructor” function (e.g. state, in lowercase).

As an aside, maybe what you might want to wrap/alias is not Vector{Complex}, but Vector{<:Complex}, which is a more generic type:

julia> Vector{Complex} <: Vector{<:Complex}
true

julia> Vector{<:Complex} <: Vector{Complex}
false

(The difference is that if typeof(x) <: Vector{Complex}, then eltype(x) == Complex; whereas if typeof(x) <: Vector{<:Complex}, then eltype(x) <: Complex – i.e. eltype(x) can be the abstract type Complex, but also any of its subtypes.)

6 Likes

Thank you - yes that makes perfect sense.

I like your point here - particularly in combination with @hexaeder’s above. I do indeed want to maintain the constraint (these States are in fact quantum States with |s|=1), and the sum of two States must also satisfy the constraint. So I definitely want to create a new type, and so need to delegate. The notation Vector{<:Complex} is new to me - I’m just not sure what it would bring me, since Complex doesn’t (can’t) have any subtypes. What am I missing?

Sorry, I didn’t use the correct words: Complex is not an abstract type, but it is a parametric type. That means that it is not either a concrete type, as you can see when you create actual complex numbers:

julia> typeof(1+1im)
Complex{Int64}

julia> typeof(1.0 + 1.0im)
ComplexF64 (alias for Complex{Float64})

None of them is “just” Complex: the concrete types also carry information of the type of numbers contained (real and imaginary parts).

Now, about the notation Vector{<:Complex}, the first few posts of the following discussion can help you (the discussion is about the abstract type Number, not about the parametric type Complex, but the situation is analogous):

1 Like

Great - thanks very much! :grin:

By the way, perhaps the things you need are already available in packages:
https://docs.qojulia.org/

Thanks, but I’m teaching this, so I’m trying to set up a Julia environment in which students learn programming through building precisely this kind of code for themselves. However, I’ll certainly take a look at qojulia - it looks interesting.

Best wishes,

Niall.

As an afternote to this thread: I’ve since realised that my OO hankering to derive State from Vector was wrong-headed. A State really isn’t a Vector, but rather delegates certain functionalities to a Vector. I actually suspect that is quite generally the case. Thank-you Julia for the insight! :grinning:

4 Likes