Generate random `struct`

I’m trying to implement an API which supports something along these lines:

struct MyObject
    field_one
    field_two
    field_three
end

using Distributions

object_dist = MyObject(field_one = Normal(0, 2), field_two = (Uniform(-2, 1), Normal(0, 1)), field_three = 5)
rand(object_dist) # returns MyObject with random float field_one, tuple of random floats field_two; field_three always = 5
nparams(object_dist) # = 3
from_params(object_dist, [1., 2., 3.]) # MyObject(1, (2., 3.), 5)
param_dists(object_dist) # [Normal(0, 2), Uniform(-2, 1), Normal(0, 1)]

But it turns out not that easy. I cannot understand how to “convert” type of object_dist replacing all Distributions with floats, and how to build a (nested) struct from a flat vector of values, filling in only some of the fields - those that are specified as Distributions.
Any ideas on how to do that, or maybe design something similar, but simpler?

Here is a starting point:

using Distributions

struct MappedDistribution{F, D <: Tuple}
    f::F
    parameter_distributions::D
end

function MappedDistribution(f, args...)
    MappedDistribution(f, args)
end

function Base.rand(o::MappedDistribution)
    params = map(rand, o.parameter_distributions)
    o.f(params...)
end

struct MyObject
    field_one
    field_two
    field_three
end

d = MappedDistribution(MyObject,
    Normal(0, 2), 
    MappedDistribution(tuple, Uniform(-2, 1), Normal(0, 1)),
    MappedDistribution(() -> 5)
)

rand(d)
#MyObject(0.7235862717111876, (0.8576196118399584, -1.0640162037995087), 5)
  • If you want keyword arguments I would use QuickTypes or Parameters and just add ;kw... to to the mapped distribution constructor.
  • Personally I would only allow distribution arguments. If you really really want to be able to pass 5, introduce another indirection:
_lift(x) = Singleton(x)
_lift(x::Distribution) = x
  • nparams is easy to define. param_dists can be defined recursively. from_params seems tricky. Also for what do you need these functions for? I suspect you are really after something slightly different.
2 Likes

First of all, thanks for you answer!
Well, maybe you are correct that I really need something different, but this was the most straightforward way I could think of. The actual problem is that I have a model like the following:

struct Params
# parameters, both plain Floats, tuples, and a few nested structs
...
end

function evaluate(p::Params, x::Float)
...
end

function loglike(p::Params, x::Float, y::Float)
...
end

and want to use an (external) monte carlo sampler to fit this model. The main thing it requires is a function from a Vector{Float} with nparams elements to Params: sampler works with plain arrays only, and I would really prefer using structs for parameters. Then it need priors of course, and I thought that a really convenient way to write them would be like I show in the first post: Params(field_one = Normal(0, 2), field_two = (Uniform(-2, 1), Normal(0, 1)), field_three = 5).
Do you think there are better or easier approaches?

Agreed, if you need it for an external program, the flattening is reasonable. Here you go:

using Distributions
using QuickTypes

struct Mapped{F, A<:Tuple, K}
    f::F
    args::A
    kw::K
end

function Base.rand(o::Mapped)
    args = map(rand, o.args)
    kw = Dict(k => rand(v) for (k,v) in o.kw)
    o.f(args...; kw...)
end

nparams(o::Distribution) = 1 #TODO univariate
function nparams(o::Mapped)
    a = mapreduce(nparams, +, o.args, init=0)
    k = mapreduce(nparams, +, values(o.kw), init=0)
    a + k
end

param_dists(o::Distribution) = o
function param_dists(o::Mapped)
    a = map(param_dists, o.args)
    k = map(param_dists, values(o.kw))
    vcat(a..., k...)
end

from_params(o::Distribution, x) = first(x)
function from_params(o::Mapped, x)
    i = 0
    args = map(o.args) do dist
        n = nparams(dist)
        index = (i+1):(i+n)
        i += n
        from_params(dist, x[index])
    end
    kw = Dict(key => begin
        n = nparams(dist)
        index = (i+1):(i+n)
        i += n
        from_params(dist, x[index])
    end for (key, dist) in o.kw)
    o.f(args...; kw...)
end

# sugar
pushforward(f, args...; kw...) = Mapped(f, args, kw)
product(args...) = pushforward(tuple, args...)
constant(x) = pushforward(()->x)


@qstruct MyObject(;
    field_one,
    field_two,
    field_three,
    )

d = pushforward(MyObject,
    field_one=Normal(0, 2), 
    field_two=product(Uniform(-2, 1), Normal(0, 1)),
    field_three=constant(5),
)

@show rand(d)
@show nparams(d)
@show param_dists(d)
@show from_params(d, [1,2,3.])