Stream: helpdesk (published)

Topic: Variable defined in the function scope as an argument


view this post on Zulip Moorits Muru (Nov 21 2023 at 14:08):

I have a situation, where I have an integration and I want to give the integration limit as an argument to a function, but the limit depends on something that is calculated in the same function. Therefore, I would like to use a variable name as a function argument, but that variable is only defined in the function scope.

It should be doable with metaprogramming, but I can't figure it out.

I guess it is something analogous to making these function calls work

function foo(x)
  bar = 1
  baz = 2
  eval(x)
end

foo(bar) # 1

foo(baz) # 2

At the moment this gives UndefVarError.

view this post on Zulip Sergio Vargas (Nov 21 2023 at 17:15):

You could use a dictionary or a named tuple and use symbols as keys. no need to eval here.

view this post on Zulip Moorits Muru (Nov 22 2023 at 07:13):

Thanks!

view this post on Zulip Moorits Muru (Nov 22 2023 at 08:32):

In some cases, I would also like to do some simple manipulation with the variable and in that case the dictionary does not work. For example, foo(10*bar).

view this post on Zulip Leandro Martínez (Nov 23 2023 at 12:39):

I think you should give a more realistic example of what you're trying to do. It doesn't seem a case for metaprograming at all, but your example foo(x) function does not make sense, as x, is the argument. You example works with:

julia> function foo(x)
           if x == :bar
               1
           elseif x == :baz
               2
           end
       end
foo (generic function with 1 method)

julia> foo(:bar)
1

julia> foo(:baz)
2

or

julia> function foo(f)
           f()
       end
foo (generic function with 1 method)

julia> foo(() -> 1)
1

julia> foo(() -> 2)
2

but understanding why this is not what you need requires some more explanation.

view this post on Zulip Moorits Muru (Nov 23 2023 at 13:15):

The actual use case is that I am calculating the mass of galaxy groups, which includes taking an integral to some radius from the group's center. There are different definitions of radii that can be used as the radius of the group. I want to be able to give the main function an argument, which determines the upper limit of that integral. The problem is that the different radii are not known beforehand. They are calculated in the main function. For example, I would like to run a function calculate_mass(galaxies; integral_limit=10*R1) or change the limit to integral_limit=5*R2. In the end, R1 and R2 will have some value, and the upper limit will be used as an argument for the quadgk function.

Fortunately, at the moment, I am only comparing two different radii and implementing it as a simple boolean. I only need multiplication, so I added another argument to the calculate_mass function that contains the factor as a number. I just hoped there was an elegant way to solve this so that I could have an argument 10*R1 without R1 being defined yet (but it will be determined before it is actually used).

Does this make any sense?

view this post on Zulip Daniel González (Nov 23 2023 at 13:24):

if there are not a lot of additional usage instances, you could just define a Radius{N} struct, define Base.:(*)(::Number, ::Radius)and then have a method that evaluates it once it's known (kind of like a minimal symbolics stack)

view this post on Zulip Daniel González (Nov 23 2023 at 13:27):

so something like

struct Radius{N}
       scale::Float64
end

Base.:(*)(s::Number, r::Radius{N}) where {N} = Radius{N}(r.scale * s)
const R1 = Radius{1}(1.0)
const R2 = Radius{2}(1.0)

view this post on Zulip Daniel González (Nov 23 2023 at 13:27):

Then

julia> 10 * R1
Radius{1}(10.0)

view this post on Zulip Daniel González (Nov 23 2023 at 13:29):

then resolve it once the value is known as resolve(r::Radius{1}, r1, r2) = r1 * r.scale and resolve(r::Radius{2}, r1, r2) = r2 * r.scale(if there are two options, otherwise you could have all of them as a tuple and define all of them with a single method)

view this post on Zulip Daniel González (Nov 23 2023 at 13:34):

Though I just read the post where you said you wanted to do some manipulation as foo(10*R1), which I guess discards my proposal because you'd basically be reinventing a symbolics engine (or maybe it just works if you define another struct with the function and the Radius{N} as fields, for delayed evaluation? In any case hard to assess without the details)

view this post on Zulip Moorits Muru (Nov 23 2023 at 13:49):

I am slipping up with trying to simplify things :grinning: Using just R1 and R2 wasn't the best example because the radii actually have names (such as R_grav and R_200). I like your solution, but I guess I would just have to put the radii into some ordered list. I think in general, there is no point in trying to figure out more complicated solutions, as my case was quite simple in practice and solvable with just a boolean and an extra argument, and it is unlikely to get any more complicated.

view this post on Zulip Moorits Muru (Nov 23 2023 at 13:51):

I guess the question I was looking an answer to was whether I could have an expression 10*x, when x is not defined, and evaluate it after x has been defined.

view this post on Zulip Daniel González (Nov 23 2023 at 13:52):

yeah, if a flag works then it's probably the best solution instead of adding code (you could still build R_grav = Radius{:grav}(1.0) if you really really wanted to, though)

view this post on Zulip Mason Protter (Nov 23 2023 at 14:19):

Why aren't these just separate arguments to calculate_mass?

view this post on Zulip Mason Protter (Nov 23 2023 at 14:21):

i.e.

calculate_mass(galaxies; integral_limit=10, limit_style=:R_grav)

or something?

view this post on Zulip Gunnar Farnebäck (Nov 23 2023 at 15:36):

I guess the question I was looking an answer to was whether I could have an expression 10*x, when x is not defined, and evaluate it after x has been defined.

You sort of can, but it's a terribly bad idea in many ways.

julia> function foo(x)
         global bar = 1
         global baz = 2
         eval(x)
       end
foo (generic function with 1 method)

julia> foo(:(10 * bar))
10

view this post on Zulip Mason Protter (Nov 23 2023 at 16:20):

please don't do this in real code

view this post on Zulip Sergio Vargas (Nov 23 2023 at 17:25):

why can't the radii calculations be refactored into a separate function? :thinking:

view this post on Zulip Fredrik Bagge Carlson (Nov 23 2023 at 18:06):

Is there a reason to not let the user pass in a function that computes the radius, rather than an expression?

view this post on Zulip Leandro Martínez (Nov 23 2023 at 18:16):

I guess in the general case you just want to pass a function:

julia> function foo(f)
           x = 2
           f(x)
       end
foo (generic function with 1 method)

julia> foo(x -> 10*x)
20

view this post on Zulip Leandro Martínez (Nov 23 2023 at 18:16):

(oh yes, everybody just said that)

view this post on Zulip Moorits Muru (Nov 24 2023 at 07:19):

Mason Protter said:

i.e.

calculate_mass(galaxies; integral_limit=10, limit_style=:R_grav)

or something?

This is what I ended up doing.

view this post on Zulip Moorits Muru (Nov 24 2023 at 07:21):

Sergio Vargas said:

why can't the radii calculations be refactored into a separate function? :thinking:

It is an iterative calculation that cannot be calculated beforehand as the integral has to be taken in every iteration until the results have converged.

view this post on Zulip Moorits Muru (Nov 24 2023 at 07:26):

Fredrik Bagge Carlson said:

Is there a reason to not let the user pass in a function that computes the radius, rather than an expression?

In general, I agree, but these two radii that I wanted to compare are both needed in the calculations in different steps and are already calculated.

view this post on Zulip Fredrik Bagge Carlson (Nov 24 2023 at 14:45):

Multiplying two numbers takes on the order of a nanosecond, are you sure that recalculsting them is an issue? Even if you pass in a function and that function is called twice, julia will likely inline that function and optimize away any simple calculation that is repeated. How expensive is this operation we're talking about?

view this post on Zulip Moorits Muru (Nov 28 2023 at 07:31):

They aren't expensive computationally, and I could definitely just calculate them twice, once for the general calculations and once for the integral limit. I think I figured that reusing the values would be simpler than doing the radius calculations twice, but that does not seem to be the case.


Last updated: Nov 22 2024 at 04:41 UTC