Type With Variable Number Of Parameters

Hello Julians,

I am trying to define a parametric type MyType{T1,T2,T3,...} where the number of Ti’s may vary according to constructor’s arg… I’ve done something similar to:

struct MyType{T}
  m::Union{T,Tuple{Vararg{T}}} where T
  MyType(a::A...) where A = new{typeof(a)}(a)
end

…but it doesn’t really do what I need. Any suggestion?

Regards.

Not possible.

But one parameter can subtype Tuple, thus be any length. You won’t get extra parameter names to annotate fields with, but it doesn’t seem like you need it anyway. This will have a different concrete type for a different tuple length:

julia> struct MyType{T, S<:Tuple{T,T,Vararg{T}}} # tuple >= 2
         m::Union{T, S}
         MyType(a::A...) where A = new{A, typeof(a)}(a)
       end

julia> MyType(1,2,3,4)
MyType{Int64, NTuple{4, Int64}}((1, 2, 3, 4))

julia> MyType(1,2)
MyType{Int64, Tuple{Int64, Int64}}((1, 2))

julia> MyType(1)
ERROR: TypeError: in MyType, in S, expected S<:Tuple{Int64, Int64, Vararg{Int64}}, got Type{Tuple{Int64}}

Not sure how you intend the 1-argument scenario to infer the alternate tuple length though. Might be easier to go for a 1-tuple instead, just store tuples in m.

2 Likes

Thank you for your reply,

The usage of MyType will be 90% with a single object in field m and I already have a lot of code implemented with MyType having m as a single object, I don’t want to go and change hundreds of lines… I’m just adding the ability to have multiple models stored in m… having MyType{A,B,C,...} would’ve been cool though!

1 Like

Would an alternative thought maybe be to have the storage “the other way around”, so a Vector[MyType] (or tuple) instead of a tuple “inside”?
That would be basically broadcasting in the functions then (or dispatch on the vector my type case, depending on what your functions do.

Of oourse that depends a lot on the actual situation, but maybe keeping the MyType a “simpler structure” might be something that could be nice.

2 Likes

Yeah, but implemented it this way because some models (financial) come with the “parameters”, the very model creates its specific framework, its specific world… It was to enforce those ideas…
But yeah, the struct is simple, lightweight… I wanted it to dispatch… it does so with MyType{Tuple{A,B,C}}

Thanks.

1 Like

I know it’s too late now, so I’m sorry to Monday Morning Quarterback you, but the “I’m accessing a property so I can’t change it’s behavior without doing a major refactor” problem is why it’s recommended to use accessor functions.

struct MyType{T}
    m::T
end

get_model(x::MyType) = x.m

function f(x)
    y = get_model(x)
    # do something with singleton y
end

# oops I realized that sometimes m is a tuple
# which will get confusing.

function get_model(::MyType{<:Tuple})
    # setup default behavior
    # maybe error to fail loudly
    # maybe just return the first model
end

#always return a tuple
get_models(x::MyType) = (x.m,)
get_models(x::MyType{<:Tuple) = x.m

This makes it easier to refactor because now you can search for reference to the function instead of trying to CTRL+F for .m.

8 Likes

Thank you sir for your reply, accessors functions are useful indeed.
By the way (it’s kinda another question), what about the speed of x.m compared to getfield(x,:m) ? Everywhere in my code (and I have 30K+ LOC so far) I prefer the later syntax and folks find it verbose… I do it because of the speed; getfield is a heck faster (at least for every benchmark I have run so far)!

There shouldn’t be a difference. The little indirection of accessors or getproperty (what dot syntax lowers to) can be handled at compile-time.

4 Likes

The Julia compiler will calculate anything it can prove is a constant and just insert the result, like @Benny said.

Can you post a benchmark where you are seeing a difference?

1 Like

For anyone interested in a type like this its relatively easy to just wrap a NamedTuple, and get a totally flexible struct.

You can copy Extents.jl GitHub - rafaqz/Extents.jl: A shared Extent object for Julia spatial data, with DE-9IM spatial predicates for a simple template.

1 Like

It’s worth pointing out this is a stage where design can be reconsidered, which is easier than having to refactor later, and that in general, the language’s design isn’t a template for how anything is organized. Just as type parameters vary the field types or add limited isbits type information to parametric types rather than cleanly correspond to “parameters” in other contexts or the general sense, there’s no inherent reasons for all models to be grouped under a parametric type, for a model to be represented by one type, or even for different models to have different types. For example, if there are a fixed number of models, then it would be cleaner to incorporate model-parameter names as varying fields of distinct types rather than the same set of fields of a parametric type, and if they need to dispatch to entirely separate methods instead of sharing any, there isn’t a need for a supertype. What’s best depends on the context, just use your best judgment and accept occasional refactoring.

2 Likes

Here’s an example, the statistical significance of the difference might be argued…

julia> struct JL
         a::Real
         b::Real
         c::Real
       end

julia> a=JL(rand(3)...);

julia> @benchmark a.a
BenchmarkTools.Trial: 10000 samples with 996 evaluations per sample.
 Range (min … max):  23.972 ns … 32.499 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     24.185 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   24.198 ns ±  0.233 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
...

julia> @benchmark getfield(a,:a)
BenchmarkTools.Trial: 10000 samples with 997 evaluations per sample.
 Range (min … max):  19.556 ns … 45.117 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     19.566 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   19.650 ns ±  0.939 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
...

Yes, thank you. But for what I am doing, I think it’s better to have it like that. It’s a Monte-Carlo facility… You feed it with some models (I guess under the hood it’s just a pointer to those models) and when you need to price the financial instrument, you just use the (hugely) overloaded pricing method! Models need to be categorized (volatility models…) though I’ll see what best fits everything… Everyday we have to refine the ideas…

Your benchmark is getting thrown off by referencing a global variable a rather than taking it as an argument, which is more typical for non-const variables for performance reasons. $-interpolation does the latter.

julia> @benchmark $a.a
BenchmarkTools.Trial: 10000 samples with 1000 evaluations per sample.
 Range (min … max):  1.800 ns … 92.400 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.900 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.892 ns ±  1.182 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▅                           █
  █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂ ▂
  1.8 ns         Histogram: frequency by time           2 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> @benchmark getfield($a, :a)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations per sample.
 Range (min … max):  1.800 ns … 42.100 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.800 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.849 ns ±  0.693 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █
  █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇ ▂
  1.8 ns         Histogram: frequency by time         1.9 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.
2 Likes

To be honest, with the examples I first tried, there was no difference… Btw, I benchmarked real objects in my package… I have to find a good example for it to be reproducible… After all, y’all are Julia experts, if you think that MyType.a is the equivalent to getfield(MyType,:a) (performance-wise) you have a good reason to think so… I’ll look at the implementation though…

…thank you for the interpolation hint by the way… haven’t realized it…

I was about to comment on the implementation. Your benchmark isn’t “wrong”, it’s a real phenomenon that BenchmarkTools is measuring. My benchmark would be relevant to code that looks like this:

x = (a=1,)
geta(y) = y.a # named y for disambiguation, could be local x instead

Your benchmark would be relevant to code that looks like this:

x = (a=1,)
geta() = x.a

In the latter case, the compiler can’t leverage the input types of the method call to optimize, instead it must assume that the referenced x can be of ANY type. x.a lowers to getproperty(x, :a) when the method definition is parsed (long before the compiler gets involved), and that is implemented in v1.11.3 as:

getproperty(x::Type, f::Symbol) = (@inline; getfield(x, f))

So your benchmark shows that getproperty’s extra indirection adds a bit more time to the underlying getfield when the compiler can’t optimize. The Performance Tips discourage referencing non-const global variables for this reason, though it has its uses.

1 Like

…so as a rule of thumb one is better off with getfield!

Well, no, it makes no difference when the compiler handles it, so the more concise dot syntax will be nicer to read and write. Even in the case you’re working on non-const globals or something else the compiler can’t infer, saving those few nanoseconds out of dozens would only be significant for field accesses in a hot loop, and there are much more important ways to optimize (you probably want to aim for 1.8ns instead of 19.6ns, and outside of loops as much as possible).

Haha, well OK.