More flexible parametric types

I would like to have the functionality displayed in the definition of S₂. This code obviously does not work (and I understand why), but what would be the closest thing to accomplishing such functionality? Thanks!

Background: I (will) have a lot of related types that have a lot in common, each with many fields with varying types. It is much easier (to me!), less maintenance, and less remembering to have a single parametric type than to define many individual ones and use subtyping. I can cut down on the maintenance part by generating various types programmatically, but that is not great for legibility. Alternatively, I can do something like struct S{T1,T2,T3,T4,T5}, but that’s not great either. And yes, I do want parametric types.

(I suspect I’m setting myself up for a hard no, but would appreciate any suggestions and ideas.)

struct S₁{T₁, T₂}
    x   :: T₁
    y   :: T₂
end


struct T
end

struct X
end


const D = Dict( T => ( Int, Float64 ), X => (String, Int ) )


struct S₂{Q}
    x   :: D[Q][1]
    y   :: D[Q][2]
end

x = S₂{T}( 0, 1.0 )


# I want S₂{T} to give me the same as S₁{Int, Float64}

Do you actually need different structs? the easiest thing for me, given your example, would be to overload the constructor:

julia> struct S₁{T1, T2}
           x::T1
           y::T2
       end

julia> struct T end

julia> struct X end

julia> S₁{Q}(x, y) where Q<:T = S₁{Int, Float64}(x, y)

julia> S₁{T}(3, 4)
S₁{Int64, Float64}(3, 4.0)
2 Likes

Thank you.

If the only objective were to create such objects then your method would indeed be great.

But I’d like to use it generically, so to also be able to use

function f( x :: S₁{T} ) where T
....
end

or something like that.

All you can do is to create aliases, like

julia> struct S{T, Q}
           x::T
           y::Q
       end

julia> const SIF = S{Int, Float64}
SIF (alias for S{Int64, Float64})

julia> const SSI = S{String, Int}
SSI (alias for S{String, Int64})

But note that the core of type assertion is to know types at compile time. Something like

struct S₂{Q}
    x   :: D[Q][1]
    y   :: D[Q][2]
end

function f( s :: S₂{T} ) where T
    return s.x + s.y # or anything else
end

would go completely against that. s.x and s.y compile time types would depend on a variable T that is only known at runtime

1 Like

Taking your question at face value, ComputedFieldTypes.jl kinda does what you want:

julia> using ComputedFieldTypes

julia> struct T end

julia> struct X end

julia> const D = Dict(T => (Int, Float64), X => (String, Int))
Dict{DataType, Tuple{DataType, DataType}} with 2 entries:
  X => (String, Int64)
  T => (Int64, Float64)

julia> @computed struct S₂{Q}
           x::D[Q][1]
           y::D[Q][2]
       end

julia> S₂{T}
S₂{T}

julia> S₂{T}( 0, 1.0 )
S₂{T, Int64, Float64}(0, 1.0)

julia> dump(S₂{T, Int64, Float64})
struct S₂{T, Int64, Float64} <: Any
  x::Int64
  y::Float64

However, this seems like a possible XY problem:

2 Likes

It solves X. I will continue to experiment trying to come up with better solutions for Y.

1 Like

Can you explain what you want a little more concretely? That is, one complete example of what you want to work.

One problem is that complex types (with many parameters) are somewhat sucky to write down. Type aliases can sometimes cut down on this work, but are not flexible enough for your job.

However, nobody prevents you from doing arbitrary computation on types at the time of definition. So you could do

struct S{T1,T2}
x::T1
y::T2
end
d = Dict(:T => S{Int,Float64}, :S => S{Int,Int})

_st = d[:T]
typeof(_st(1,2))
foo(x::_st) = 1

In other words, there is no reason to ever spell out the actual types; you can simply use normal code like your dict to plug together the types you want. It’s just that

foo(x::d[:T]) = ....
struct Bar x::d[:T] end

don’t work. That’s a design decision that forces you to spell out

tmp = d[:T]
foo(x::tmp) = ...

which makes it obvious that the type pattern that foo matches is computed in global/module scope one line before that, when the module is loaded, and that subsequent mutations of tmp or d will not change this definition.

A limitation is that the following won’t work:

julia> function bar()
       t = d[:T]
       function ff(x::t) 1 end
       t = d[:S]
       function ff(x::t) 2 end
       ff
       end
ERROR: syntax: local variable t cannot be used in closure declaration

And it’s clear why this cannot work: The definition of the closure must, after all, be lifted into global scope by lowering, and t is only known once bar() actually runs, not at the time of definition of bar(). (closure definitions are syntactic constructs that do not participate in local control flow)

(post deleted by author)

Thank you very much @foobar_lv2 .

All of this is just needed to avoid carrying around overly complicated type descriptions. I don’t need anything like foo(x::d[:T]).

Let me try to be precise in what the problem is. At the bottom is a type with many fields that vary on a total of (say) five types, e.g.

struct B{T1,T2,T3,T4,T5}
	F1 :: T1
	F2 :: Vector{ T1 }
	F3 :: T2
	....
	F30 :: T5
end

But I don’t need all possible combinations of T1 through T5. What I need B for is to define a collection of types S that have the same set of fields, but differ in T1 through T5. Basically, these contain data for use in different types of estimators, say E1 through E20. Instead of adding a new name for every S and use type aliases, it seems cleaner to have a single type S like

struct S{E}
...
end

which maps to B{T1,T2,T3,T4,T5} for T1 through T5 that depend on E. This is all compile time-resolvable.

In other words, I want to be able to use it as any other type, including allowing for nesting

struct K{E}
	s	:: S{E}
	i	:: Int
	...
end

and using it in function arguments

function foo( x :: S{E} )
	...
end

So if there is a more elegant way of doing this than to use computed field types then I’m all ears.

(post deleted by author)