Output the same type of struct in function

Hi everyone,

I have a questions regarding return types in functions. In principle, I have 2 similar structs (one is bigger, one smaller) defined, and for convenience, I want to auto-fill some fields with default values if they are not explicitly stated - I did this via methods. A short version is given here:

using Distributions

abstract type PLAN{A<:VariateForm} end

struct PLAN1{A,B}  <: PLAN{A}
    a::Vector{Distribution{A}}
    b::Vector{B}
    PLAN1{A,B}(a,b) where {A,B} = new(a,b)
end
struct PLAN2{A,B}  <: PLAN{A}
    a::Vector{Distribution{A}}
    b::Vector{B}
    c::Dict{Symbol, Vector{C} where {C} }
    PLAN2{A,B}(a,b,c) where {A,B,C} = new(a,b,c)
end

#####################################
#Methods
PLAN1(a::AbstractVector{<:Distribution{A}}, b::AbstractVector{B}) where {A,B} = PLAN1{A,B}(a,b)
PLAN2(a::AbstractVector{<:Distribution{A}}, b::AbstractVector{B}, c::Dict{Symbol, <:AbstractVector{C} where {C} }) where {A,B} = PLAN2{A,B}(a,b,c)
#PlAN2 without c field gets as default empty set
PLAN2(a::AbstractVector{<:Distribution{A}}, b::AbstractVector{B}) where {A,B} = PLAN2(a, b, Dict(:∅ => []))

#####################################
#Working as expected
plan1 = PLAN1( [Normal(1,2), Gamma(1,2)], [0.9, 0.1] )
plan2 = PLAN2( [Normal(1,2), Gamma(1,2)], [0.9, 0.1],
               Dict(:τ =>[Dirichlet( [1, 2]), Dirichlet( [1, 2])],
                    :μ => [Normal(1,2), Gamma(1,2)] ) )
#PLAN 2 without c field
plan3 = PLAN2( [Normal(1,2), Gamma(1,2)], [0.9, 0.1])

typeof(plan1) #PLAN1
typeof(plan2) #PLAN2
typeof(plan3) #PLAN2 -> as intended

One could think of PLAN1 as frequentist model, and PLAN2 as Bayesian model, with the field c as prior for the parameter. I would like that my functions, that I have usually written for the frequentist struct PLAN1, are also working for PLAN2 but ignore the prior fields. This usually works with 90% of my functions except when I need to return the same type of the input as output of some function, like here:

function fun1(model::PLAN{A}) where A
#some calculations of the fields PLAN.a and PLAN.b
a_new = model.a #simplified
b_new = model.b #simplified
return ( typeof(model)(a_new,b_new) )  ##ERROR MESSAGE HERE
end

fun1(plan1)  #WORKING
fun1(plan2 ) #NOT WORKING?!?
fun1(plan3 ) #NOT WORKING?!?

Question1: I get an error message at the end of fun1 that says ‘no method is defined’ for plan2 and plan3, but I honestly do not know why because I used methods above exactly for the case when the field c is missing. I tried to google it and to find it here, but my search was not successful. I guess this might be a common beginner mistake, and would be happy if someone could give me a tip/solution what I did wrong or how I could properly use methods such that fun1 also works for plan2 without c field.

Question2: I am interested if it has any performance implications that I did not define the parametric type C at the beginning of the struct PLAN2, but within the field row. The type itself has not any interesting meaning.

Thank you for your inputs and best regards!

Hey,
your code has some problem with type stability which I believe causes your observed problems.
See, your distributions you try to put in this vector have different types.

typeof(Normal(1,2)) == Normal{Float64}
typeof(Gamma(1,2)) == Gamma{Float64}

This means that your vector of distributions cannot be concretely typed.
If you accept that, then you can simplify the code a little more.

using Distributions

abstract type PLAN{A<:VariateForm} end

struct PLAN1{A,B}  <: PLAN{A}
    a::Vector{ <: Distribution{A}}
    b::Vector{B}
end
struct PLAN2{A,B}  <: PLAN{A}
    a::Vector{ <: Distribution{A}}
    b::Vector{B}
    c::Dict{Symbol}
    PLAN2{A,B}(a,b, c= Dict(:∅ => [])) where {A,B}= new(a,b,c)
end
PLAN2(a:: Vector{ <: Distribution{A}},b::Vector{B}, c = Dict(:∅ => [])) where{A,B}=  PLAN2{A,B}(a,b,c)

#####################################
#Working as expected
plan1 = PLAN1( [Normal(1,2), Gamma(1,2)], [0.9, 0.1] )
plan2 = PLAN2( [Normal(1,2), Gamma(1,2)], [0.9, 0.1],
               Dict(:τ =>[Dirichlet( [1, 2]), Dirichlet( [1, 2])],
                    :μ => [Normal(1,2), Gamma(1,2)] ) )
#PLAN 2 without c field
plan3 = PLAN2( [Normal(1,2), Gamma(1,2)], [0.9, 0.1])

typeof(plan1) #PLAN1
typeof(plan2) #PLAN2
typeof(plan3) #PLAN2 -> as intended

function fun1(model::PLAN{A}) where A
    #some calculations of the fields PLAN.a and PLAN.b
    a_new = model.a #simplified
    b_new = model.b #simplified
    return ( typeof(model)(a_new,b_new) )  ##ERROR MESSAGE HERE
end

fun1(plan1)  #WORKS
fun1(plan2 ) #works
fun1(plan3 ) #works
1 Like
  1. typeof(plan2) == PLAN2{Univariate,Float64} and, indeed, there is no function PLAN2{Univariate,Float64}(a, b) but only PLAN2(a, b) defined. If you want the type without parameters, you can get it with typeof(plan2).name.wrapper, but I would disencourage relying on “internal” fields like name and wrapper. I don’t know what the best approach would be here, but one possibility is to define functions initialize(model::PLAN1, a, b) = PLAN1(a, b) and initialize(model::PLAN2, a, b) = PLAN2(a, b).
  2. I suspect that you have a performance decrease. I would benchmark this with BenchmarkTools.

By the way, and sorry for raising this when you didn’t ask for it, but I think the outer constructor of PLAN2 can be simplified, e.g. PLAN2(a, b, c = Dict(:∅ => [])) = ... and it is always worth reading a bit in the Julia style guide, in particular here.

1 Like

Thanks for your reply! That astounds me, I just tested it again and

fun1(plan2 ) #NOT WORKING?!?
fun1(plan3 ) #NOT WORKING?!?

should not work with the current methods.

Yeah, I’m sorry. I messed up.

I updated my previous comment.

This is the reason that it works now: you defined PLAN2{A,B}(a,b) by making use of the optional argument c = .... The type instabilities are another issue.

1 Like

Thank you for your tips!

Regarding 1.:
It puzzles me that fun1(plan1) works nevertheless though, because there is also no typeof(plan1) == PLAN1{Univariate,Float64} defined. I guess it somehow works via typeof(plan1)(a,b), but I dont seem to find an appropriate method to make it work with the other struct PLAN2. That being said, I thought it might work if I add the parametric types as methods like in PLAN1:

PLAN2(a::AbstractVector{<:Distribution{A}}, b::AbstractVector{B}) where {A,B} = PLAN2{A,B}(a, b, Dict(:∅ => [])),

but without success.

There is the default constructor for PLAN1.

1 Like

Thank you for your answer! I am aware of the type issue via Distributions but I decided to use that module because it simplifies tasks by a lot.

I am unsure if your suggestion would be a solution because the field C should be an optional argument with varying names and length, i.e. different priors for different parameter

You can do PLAN2{A, B}(a, b) where {A, B} = PLAN2(a, b) instead.

1 Like

Thank you that did the trick - and it works only in conjunction with

PLAN2(a::AbstractVector{<:Distribution{A}}, b::AbstractVector{B}) where {A,B} = PLAN2{A,B}(a, b, Dict(:∅ => []))

of course. I will have a look at your style guide and constructors links as well. I also have to benchmark if the parametric type C causes any performance issues if it is not stated in the wrapper, but there is not really a solution I could think of to put it there as there is not any concrete type of a union of different distributions.

I ran into a similar problem recently. I wanted to obtain the type of a variable but without any of the parameters, so that i can call the non-parametric constructor and let it figure out the parameters.

I ended up defining a function like this

typeofNoParam(::PLAN2)=PLAN2
typeofNoParam(::PLAN1)=PLAN1

and then use

typeofNoParam(model)(...)

instead of

typeof(model)(...)

It is similar to your solution with the initialize function, however, you don’t need to know the number of parameters to the constructor (PLAN1 and PLAN2).

EDIT: I just found an older thread that discussed the same issue with the same solution Extract type name only from parametric type