Unexpected method additions to a struct instance's Function field

The thing I’m demonstrating here is that, the letters and numbers instances have property func that gets respective method definitions, but their supposedly separate method additions get defined for the other instance as well, which caught me off guard. This behaviour exists in the REPL as well, so it doesn’t seem to be a Pluto.jl thing.

I may end up having to do this instead, which is not so pretty:

I think my question is, should Julia be doing the first screenshot? I would’ve thought it creates a new function each time Subject gets instantiated, but apparently not.

Correct, a local function block, which is a closure, is 1 declaration so there is only 1 function type and thus method table; if this weren’t the case, performance would suffer a lot. It is technically an instantiation like any other type constructor call, but a singleton type will only ever instantiate its 1 instance, in this case the same function. If the closure captured any variables, then the instantiation can make >1 callable instances. I think your best bet is making and inputting anonymous functions via an argument, not replacing the 1 default.

Thanks @Benny, that makes a lot of sense.

I know another function definition syntax is = function(...), but func = function end is invalid syntax. Would it be worth submitting a pull request validating such a syntax so that it creates a new function with zero methods, instead of adding methods each time it runs?

If I’ve understood you correctly though, such would be a performance issue?

The reason I’m avoiding this is because of my seemingly unique use case, of which I’ve prototyped a hundred different possible alternatives in many different languages but can’t seem to figure out a “best” way. I’m simultaneously formulating a way to ask this question concisely. In general I understand I’ll have to choose one and accept any trade-offs.

No, it’s just not worth making so many different functions to compile. This issue directly concerns something else but it explains how method tables are hoisted out of enclosing methods to the top level to avoid being rebuilt per call. I should also mention that by “making” an anonymous function, I mean a global one that is evaled (but possibly from a local scope) for a fresh one because local anonymous definitions are hoisted too.

That’s a bad sign that you’re doing something in an impractical and widely discouraged way. What do you actually need?

1 Like

Well the short story is:

  • I’m building a library of different parameter calculators.
  • Each parameter can be calculated by a number of different models, sometimes multiple model specifications.
  • My implementation would like to take advantage of the optimisation capabilities of the language I’m using.

As an example of both a multi-model parameter and its implementation,

stringer(::Val{:square}, ::Val{:one}, x, y) = x^2 + 1y
stringer(::Val{:square}, ::Val{:two}, x, y) = x^2 + 2y
stringer(::Val{:cube}, ::Val{:one}, x, y) = x^3 + 1y
stringer(::Val{:cube}, ::Val{two}, x) = x^3 + 2y

A poor example, given there are better ways to specify the multiplication factor than Val(:one) and Val(:two) and better ways to specify the power than Val(:square) and Val(:cube), but there are contexts for implementation where a string (or symbol) is properly needed:

  • The model’s author(s) name(s) need to be dispatched on.
  • An automatically generated list of models*.

*As a sidenote on this, and extra motivation for my modelling goal, consider this poorly designed function of mine:

function models(func::Function)
	subject_methods = methods(func)

	subject_method_signatures = [
        subject_method.sig
        for subject_method in subject_methods
    ]

	subject_method_signature_types = [
		subject_signature.types
        for subject_signature in subject_method_signatures
	]

	subject_method_symbols = [
		[
            sig_type.parameters[1]
            for sig_type in subject_type
            if sig_type <: Val
        ]
		for subject_type in subject_method_signature_types
	]

	num_method_categories = subject_method_symbols .|> length |> unique
	@assert length(num_method_categories) == 1 "All methods of $(string(func)) must have the same number of model specifications."

	num_model_categories = num_method_categories[1]

	return Tuple(
		[subject_symbol[n]
			for subject_symbol in subject_method_symbols
		] |> sort |> unique for n in 1:num_model_categories
    )
end

As a poor summary, it outputs a list of model names as symbols. It would assist with automated documentation, test loops, general investigations of parameter behaviour, and many others. My boss is patient enough to manually change lists of models each time he changes the subject’s implementation - I’m not!

I’ve tried many different options in many different languages:

  • MATLAB: Classes; functions; packages; combinations/different utilisations of classes and/or functions and/or packages
  • C++: Yeah, definitely not.
  • Odin: lacks many scientific programming capabilities, but has binary generation out-of-the-box!
  • Julia: To sub-module or not to sub-module (getting model lists via names, parameter models implemented as functions with model names); function dispatches; figuring out how/if to involve structs throughout it all.
  • Python: Definitely not. Not properly/simply/easily scalable.

Many other programming languages to try, but Julia seems best for my use case.

And as you said, I may be doing something in an impractical and discouraged way. It’s been a little bit of a chicken-and-egg problem, not knowing exactly how to ask this question, yet I need to play around and experiment with what’s available to formulate a proper question to demonstrate my needs - the latter of which I’ve been at for a while.

This response blew up in length, and it’s scope is extending beyond the original post, but this response isn’t sufficient, nor concise, in explaining my situation, so I’m going to keep it here and postpone making a new post for this question.

I still can’t really tell what you’re going for because the terms are unfamiliar to me in this context. But it seems like you’re attempting some kind of polymorphism and want it optimized. It probably doesn’t need to be mentioned but such optimizations would need the polymorphism to occur at compile-time rather than runtime.

Looking at your stringer example, I think it would accomplish some optimization despite the lack of constant propagation because 1) the scaling factor might at least be converted at compile-time or ignored if 1, and 2) exponent literals are lowered to literal_pow calls that end up inlining specific squaring or cubing methods at compile-time. The lowerer and compiler don’t do such method-switching in general and I’m pretty sure it doesn’t happen for exponent type parameters, otherwise lowering to literal_pow would be unnecessary. I think you could collapse the stringer methods manually with:

stringer(a::Val{A}, ::Val{B}, x, y) where {A,B} = Base.literal_pow(^, x, a) + B*y

You could map symbols like :square to the underlying integer 2, whether it’s with the boilerplate (and preferably evaled in a loop) methods you wrote or in a data structure, but bear in mind that the latter would have to be immutable for compile-time mapping to be possible. Multimethods seem “mutable” (not really, mutability is a particular kind of change), but thecompiler can dispatch them at compile-time because changes to the method table invalidate compiled code and force recompilation. I personally would prefer mapping in a mutable data structure and move the runtime costs out of performance critical code.

1 Like