Manipulate types of nested data structure

I have a type, let’s call it Special <: Real, which wraps a vector of lengthN. I have defined methods of most standard functions. Sometimes, however, one or several objects of type Special are nested deep into some outer type Outer and my usage of Special inside Outer fails. I would then like to construct one Outer for each value in the vector that Special wraps.

Say the situation is the following:

struct Special
    x::Vector{Float64}
end

struct Outer{T}
    a::T
    b::Int
end

a = Outer(Special(randn(3)), 1)

More specifically, I would like to create a workspace object b which is an identical copy of a, except every occurance of an object of type Special is replaced by one of the floats inside the vector Special wraps. To perform computations on b N times.

I have looked at Nested.jl and it’s relative Flatten.jl, which allow me to “flatten” a nested structure and later reconstruct it. It’s close to what I need, but I also need to replace occurances of Special with Float64 in the the structure. I can’t really understand if Flatten.jl would allow me to do this or not, it’s implementation is very nontrivial.

Some details:
Special is my type, I have full control over its definition. Outer is not mine and I can not control it. I only have access to the object a below.
Preferably, I would like to avoid requiring the user to define any methods to get this to work.

I made an attempt at replacing all occurances of Special which works, but it only get’s me half way:

function build_container(P)
    P isa Special && (return 0.0) # This replaces a Special with a float
    P isa Number && (return P)
    if P isa AbstractArray # Special handling for arrays
        return map(build_container, P)
    end
    fields = map(fieldnames(typeof(P))) do n
        f = getfield(P,n)
        build_container(f)
    end
    fields
end

julia> a = Outer(Special(randn(3)), 1)
Outer{Special}(Special([0.2252388048584368, -1.79643970302527, 0.19512554621122083]), 1)

julia> build_container(a)
(0.0, 1)

julia> a = Outer([Special(randn(3)), Special(randn(3))], 1)
Outer{Array{Special,1}}(Special[Special([0.21367693941408203, -1.3515443961700613, 1.3534416611576705]), Special([-1.0929970912074651, -0.8659938973725613, -1.3585774872551668])], 1)

julia> build_container(a)
([0.0, 0.0], 1)

Now this works, but it only gets me halfway there, I need to reconstruct the flattend structure into the appropriate type with Special replaced with Float64, and here I get lost. Without assuming that Outer has a constructor that accepts all fields in order, I can not create it with my “container”.

Any pointers to direct me forward would be greatly appreciated!

I think this is a design problem that is usually solved by a dedicated interface function, eg not unlike Base.similar for arrays.

Yes the similar function would solve the issue very nicely, but having to rely on a similar method is somewhat unfortunate, because the type Outer is out of my control, and maybe out of the control of the user as well. I could ask the user to specify such a similar, but if Outer comes from a third-party package the user might not be familiar with it and having to ask the user to write that function is unfortunate. I, as the author of the library containing Special, know exactly what is required and just want an automatic way of accomplishing this, preferably with as few ugly hacks as possible :stuck_out_tongue: