How to add a property for all custom structs?

Hi!
I made my custom types as below.
As can be seen in the script, I’d like to impose a new property c to all types which are subtypes of AbstractMine.
Should I change all of the definitions of the subtypes?
Or, is there any better way?

Note that the types correspond to neural networks and I wanna add normalization factors to each network.
If you have a better idea for this, please let me know!

struct MyA <: AbstractMine
    a::Int
end


struct MyB <: AbstractMine
    b::Int
end

# I'd like to make a new property, namely `c`, for all `Mine::AbstractMine`'s.
# For example,
# ```
# modified_my_a = MyA(1, 2)
# modified_my_a.c == 2  # true
# ```

There is not a way to do this directly.

One mechanism is to use a macro.

julia> macro addc(ex)
           p = ex.args[3]
           push!(p.args, :(c::Int))
           ex
       end
@addc (macro with 1 method)

julia> abstract type AbstractMine end

julia> @addc struct MyA <: AbstractMine
           a::Int
       end

julia> @addc struct MyB <: AbstractMine
           b::Int
       end

julia> fieldnames(MyA)
(:a, :c)

julia> fieldnames(MyB)
(:b, :c)
2 Likes

Ah, yeah, using the macro will surely work.

Thanks!

Although there’s not much difference in effort between adding @addc and adding c::Int to your struct definitions. I would just update the struct definitions.

2 Likes

That makes sense :slight_smile:
But if I need to add several properties for all structs, then using macro would be better in terms of readability and the reduction of the efforts.
I need to think about it for a bit. Thx!

I should mention there are several packages which make this easier:

Here’s a deluxe version of the macro I started above:

julia> macro extendsMine(ex)
           # Make struct subtype AbstractMine
           name = ex.args[2]
           ex.args[2] = :($name <: AbstractMine)
           
           # Add some base fields
           fields = ex.args[3]
           push!(fields.args, :(c::Int))
           push!(fields.args, :(d::Float64))
         
           get_c = esc(:get_c)
           get_d = esc(:get_d)
           
           quote
               $ex
               # Add some getter methods
               $get_c(s::$name) = s.c
               $get_d(s::$name) = s.d
           end
       end
@extendsMine (macro with 1 method)

julia> abstract type AbstractMine end

julia> @extendsMine struct MyA
           a::Int
       end
get_d (generic function with 1 method)

julia> @extendsMine struct MyB
           b::Int
       end
get_d (generic function with 2 methods)

julia> MyA(1,2,3)
MyA(1, 2, 3.0)

julia> MyB(1,2,3)
MyB(1, 2, 3.0)

julia> get_c(ans)
2

julia> fieldnames(MyA)
(:a, :c, :d)

julia> fieldnames(MyB)
(:b, :c, :d)

julia> methodswith(MyA)
[1] get_c(s::MyA) in Main at REPL[1]:17
[2] get_d(s::MyA) in Main at REPL[1]:18

julia> methodswith(MyB)
[1] get_c(s::MyB) in Main at REPL[1]:17
[2] get_d(s::MyB) in Main at REPL[1]:18

julia> get_c(MyA(1,2,3))
2

julia> get_d(MyB(1,2,3))
3.0

Let’s discuss how one figures this out.

The first step is turn the starting code into an expression object. Then we probe the head, the args, and then recursively descend until we have identified all the components that we want to manipulate.

julia> ex = :(struct MyA <: AbstractMine
           a::Int
       end)
:(struct MyA <: AbstractMine
      #= REPL[15]:2 =#
      a::Int
  end)

julia> typeof(ex)
Expr

julia> ex.head
:struct

julia> ex.args
3-element Vector{Any}:
 false
      :(MyA <: AbstractMine)
      quote
    #= REPL[15]:2 =#
    a::Int
end

julia> ex.args[2]
:(MyA <: AbstractMine)

julia> ex.args[2].head
:<:

julia> ex.args[2].args
2-element Vector{Any}:
 :MyA
 :AbstractMine

julia> ex.args[3]
quote
    #= REPL[15]:2 =#
    a::Int
end

julia> ex.args[3].head
:block

julia> ex.args[3].args
2-element Vector{Any}:
 :(#= REPL[15]:2 =#)
 :(a::Int)

julia> ex.args[3].args[2]
:(a::Int)

julia> ex.args[3].args[2].head
:(::)

julia> ex.args[3].args[2].args
2-element Vector{Any}:
 :a
 :Int

julia> supertype(MyA)
AbstractMine

julia> supertype(MyB)
AbstractMine

julia> subtypes(AbstractMine)
2-element Vector{Any}:
 MyA
 MyB

In this case, I wanted to make the subtyping part of the macro, so I needed to compare this a struct definition that is not subtyped.

julia> ex_simple = :(struct MyB
           b::Int
       end)
:(struct MyB
      #= REPL[28]:2 =#
      b::Int
  end)

julia> ex_simple.args[2]
:MyB

julia> typeof(ex_simple.args[2])
Symbol

Armed with the knowledge obtained above, I see that we need to replace argument 2 with an Expr, and then we need to push some additional expressions into the field block. Lastly I need to create a new quote block to add some new escaped expressions.

Have fun!

1 Like

Alright, after a bit of thinking,
I ended up making a new type, namely, NormalizedMine and construct it as follows,

struct NormalizedMine
    nn::AbstractMine
    normalization_factor
end

which seems better for me for now.

It’s often a good idea to extend functionality like this. You might be interested in the @forward macro from the Lazy.jl package.

Also be careful of your types here, you might not get specialisation due to the abstract type. Consider using a generic:

struct NormalizedMine{T<:AbstractMine,Q}<:AbstractMine
    nn::T
    normalization_factor::Q
end

You don’t need the Q type, especially if you know the concrete type already.

2 Likes

Definitely do what @jmair recommends.

Also, to hide the implementation details you can definte “getter” methods and use that rather than accessing fields directly.

c(mine::NormalizedMine) = mine.nn.c

Alternatively, you can implement Base.getproperty:

Base.getproperty(mine::NormalizedMine, s::Symbol) = s == :c ? mine.nn.c : getfield(mine, s)