Doing trivial transformations and assertions inside a struct definition

I am defining a struct, and I would like to make it perform some trivial transformations on the input data to simplify later calculations.

The goal is for the struct to accept just a vector of qualities and prices, and then also be able to access the normalized prices (that is, prices adjusted so they some to one) and some other transformations.

Here is what I am trying to do:

struct Market
    qualities::Array{<:AbstractFloat, 1}
    prices::Array{<:AbstractFloat, 1}
    
    normalized_prices = prices / sum(prices)
end

Then calling

m = Market([.2, .2, .2], [1., 2., 2.])
m.normalized_prices

should yield the vector [.2, .4, .4].

I would also be interested in normalizing the prices in-place, i.e. something like

struct Market
    qualities::Array{<:AbstractFloat, 1}
    prices::Array{<:AbstractFloat, 1}
    
    prices /= sum(prices)
end

However, both of the struct definitions above yield an error. I know that one way to do the second task is to redefine the function Market(qualities, prices) so that it does the normalization and then constructs the market, but is there a better way?

Separately, I would also like to assert that the input vectors are of the same length:

struct Market
    qualities::Array{<:AbstractFloat, 1}
    prices::Array{<:AbstractFloat, 1}
    
    @assert size(qualities) == size(prices)
end

How should I do this?

I would probably define it as follows:

struct Market{T<:AbstractFloat,S<:AbstractFloat}
    qualities::Array{T, 1}
    prices::Array{S, 1}
    function Market{T,S}(qualities::Array{T, 1}, prices::Array{S, 1}) where {T<:AbstractFloat,S<:AbstractFloat}
        # with the inner constructor we check and process the inputs
        length(qualities) == length(prices) || return throw(DimensionMismatch)
        prices ./= sum(prices)
        return new{T,S}(qualities, prices)
    end
end

# add a convenient outer constructor which calls the inner constructor
Market(qualities::Array{T, 1}, prices::Array{S, 1}) where {T<:AbstractFloat,S<:AbstractFloat} =
    Market{T,S}(qualities, prices)

Note that I added parametric types; if, for your needs, T and S are equal, then you should only put one. For more details, you can refer to the Constructors section of the manual.

Hope this helps :slight_smile:

2 Likes

I think one could define getproperty as you can see in the documentation Base.getproperty. You have to import it first such that you can extend its definition with a new method. Note that it has to be defined outside the struct. Moreover, you can define it wherever you want, as long as it is before the first call (I think).

struct Market
    qualities
    prices
end

Base.getproperty(m::Market, :normalized_prices) = m.prices/sum(m.prices)

m = Market([1, 2], [2, 3])

m.normalized_prices # this sould give [0.4, 0.6]

May not be the solution, but I think it could help you to understand how getproperty/setproperty works.

1 Like

I think this is not what you should expect or do in Julia. Structures do not contain methods, and the way to obtain that is to use dispatch. Meaning, you should do:

julia> struct Market{T,S}
           qualities::Vector{T}  
           prices::Vector{S}
       end

julia> normalized_prices(x::Market) = x.prices / sum(x.prices)
normalized_prices (generic function with 1 method)

julia> x = Market([.2, .2, .2], [1., 2., 2.])
Market{Float64, Float64}([0.2, 0.2, 0.2], [1.0, 2.0, 2.0])

julia> normalized_prices(x)
3-element Vector{Float64}:
 0.2
 0.4
 0.4

That if you want to store the non-normalized prices and compute and retrieve the normalized prices when you want. If you want to store only the normalized prices, than you should use the constructor suggested by @OlivierHnt

If you want to store in the struct both the normalized and the non-normalized prices, you need an additional field, and then you can do:

julia> struct Market{T,S}
           qualities::Vector{T}  
           prices::Vector{S}
           normalized_prices::Vector{S}
       end

julia> Market(qualities,prices) = Market(qualities,prices,prices/sum(prices))
Market

julia> x = Market([.2, .2, .2], [1., 2., 2.])
Market{Float64, Float64}([0.2, 0.2, 0.2], [1.0, 2.0, 2.0], [0.2, 0.4, 0.4])

julia> x.normalized_prices
3-element Vector{Float64}:
 0.2
 0.4
 0.4

Which is a little bit simpler than the other constructor because there is no ambiguity in the instantiation with 2 or 3 parameters.

4 Likes

To store more that what you need is not usual. If one can avoid storing normalized_prices with the use of getproperty could be better I think. There could be special cases though :slightly_smiling_face:.

Defining a getproperty for a symbol that does not correspond to a field is not usual either. For that what is natural is to define a function and use dispatch.

Storing or not the processed data depends on how many times that operation has to be performed afterwards, and if the time required for that is relevant.

1 Like

Yes! This is true!

FWIW, take care with the types. I think it is better to write code without type annotations until you are sure that everything works.

For instance, if one calls Market([1, 2], [2, 3]) in your example it would rise an error because prices’ elements are integers while normalized_prices’ are floating point numbers.

1 Like

@Iagoba_Apellaniz Unless I am mistaken, I believe that this getproperty method will compute the sum and divide every time it is called, right? Or maybe the compiler will optimize that away?

I find @OlivierHnt’s solution to be the most “educational” in that it shows me the proper syntax for constructors. But in practice, @lmiq’s approach in the second code block seems most natural for what I am trying to do.

2 Likes

Yes, it performs the sum and it divides every time you call it. Now it depends on you. Take care with the type annotations.

DELETED: Maybe you can leave without type annotation the normalized_prices field

Uhm… Probably not. That would make it type unstable. The simplest is to just declare explicitly that it is a Vector{Float64}. But probably prices are always that as well and the constructor with the same element for both is fine.

1 Like