Placing variables in array makes a copy of the variables?

Maybe this is a scoping issue? I am trying to define defaults in an outer constructor by looping over a list of variables and setting defaults if they are NaN as follows:

julia> struct S x::Float64 end

julia> function S1(;x::Float64 = NaN) 
         args = [x]
         for arg in args
           if isnan(arg)
             arg = 4
           end
         end
         @info x, args[1], args[1] === x
         S(x)
       end
S1 (generic function with 1 method)

julia> S1()
[ Info: (NaN, NaN, true)
S(NaN)

I would expect to get back S(4). So I tried:

julia> function S2(;x::Float64 = NaN) 
         args = [x]
         for i in 1:length(args)
           if isnan(args[i])
             args[i] = 4
           end
         end
         @info x, args[1], args[1] === x
         S(x)
       end
S2 (generic function with 1 method)

julia> S2()
[ Info: (NaN, 4.0, false)
S(NaN)

which gets me one step closer? Not looping over a vector of args works:

julia> function S3(;x::Float64 = NaN) 
         if isnan(x) x = 4 end 
         S(x)
       end
S3 (generic function with 1 method)

julia> S3()
S(4)

So a couple questions: why doesn’t for arg in args work the same as indexing into the args array? And why is args = [x] making a copy of x? Or I assume that is my issue (big shrug)???

[x] does not make a copy of x.

Rather, inside the for loop, arg is just a variable named assigned to elements of x. By writing arg = 4, you are just assigning the variable name, which only exists inside the scope of that loop, to 4. In a sense, arg forgets that it was ever assigned to elements of args.

For the second example, you are just filling in the ith entry of args with 4. This does not modify x in any way.

I think you meant [x]`` because x``` is a Float64 in the OP’s code, so it has no elements.

Somehow OP expects changing the contents of args aka [x] should change the value of the x variable, as if the Vector contained a pointer to x. [x] does make a copy of the value of x in a different location in memory.

ok.

NaN is a immutable, actually, it’s a (compiler) constant, but what’s more important is Julia does pass-by-sharing.

julia> x = 3
3

julia> function f(_x)
           _x = 10
           return nothing
       end
f (generic function with 1 method)

julia> x
3 # would you expect 10 here?

or maybe another example:

julia> xs = [3]
1-element Vector{Int64}:
 3

julia> xs[1] = 4;

julia> 3
3 # would you expect 4 here?

This may also be a helpful reading:

Definitely not a scoping issue.

Ok I think that I get what’s going on: the vector is getting the value of x but not the pointer to x, so it is copying x when I put it in an array, right? How do I create a vector of variable pointers so that I can loop over them, and change their values (conditionally, based on their values like the examples)?

And why doesn’t the for arg in args provide pointers to the values in args? (What use does for arg in args have if it is creating a new variable arg that is unrelated to args?)

1 Like

not quite, immutables can often be stack-allocated, so “copying” is not a good picture IMO. When you write x=3, 3 is constant in source code (i.e. hard-coded at LLVM/asm level), you don’t think of it as “copy” since it never got address on heap. x is not a pointer of Int64

please read that SO post I posted above, it should then be clear what’s pass-by-sharing and how is that different from pass by reference (as in C++)

the closest you can get is Ref() but this is an anti-pattern for most part, just return the values from function when you’re dealing with scalars like this.

Thank you @jling but I did read the SO post and I understand pass-by-sharing vs. pass-by-reference, but I don’t see how that explains the for arg in args, unless I should think of for as a function that I’m passing something to?

Is there any way to loop over a vector of function keyword arguments and modify their values as needed?

I could write out individual blocks for each variable, e.g.

function outer_S_constructor(;x1::Float64=NaN, ..., xN::Float64=Nan)
    if isnan(x1)
        x1 = x1_default_that_depends_on_other_kwargs_so_I_cant_assign_it_in_function_signature
    end
    ...
    if isnan(xN)
        xN = xN_default_that_depends_on_other_kwargs_so_I_cant_assign_it_in_function_signature
    end
    S(x1, ..., xN)
end

But this is tedious and does not follow the DRY principle.

you’re iterating over values:

julia> for i in 1:3
           @show i,typeof(i)
       end
(i, typeof(i)) = (1, Int64)
(i, typeof(i)) = (2, Int64)

there’s no connection back whatsoever, in fact 1:3 is immutable (and doesn’t even have 2 stored in it), forget about pointers, there are just values.

you can get all of your keyward arguments as a NamedTuple?

I’m not entirely sure what you really want to do

Ah I see: arg is a value in args but does not point to the same value in args.

What I would like to do is loop over a vector of keyword arg pointers so that I can change their values. So my last example would become:

function outer_S_constructor(;x1::Float64=NaN, ..., xN::Float64=Nan, <some_other_kwargs...>)
    args_to_check = [x1, ..., xN]
    for arg in args_to_check
        if isnan(arg)
            arg = arg_default_that_depends_on_other_kwargs_so_I_cant_assign_it_in_function_signature
    end
    S(x1, ..., xN)
end

which we know does not work. Do I change args_to_check to [Ref(x1), ..., Ref(xN)] ? If so, how do I check the value of each reference in args_to_check?

if we start with this, a common pattern is:

julia> a = [1,2,3,4]
4-element Vector{Int64}:

julia> for (i, v) in enumerate(a)
           if iseven(v)
               a[i] = 0
           end
       end

julia> a
4-element Vector{Int64}:
 1
 0
 3
 0

also btw, you can splat in the end to reduce typing:

S(args_to_check...)

also you can make function definition such as S(...; kwargs...) and then you don’t have to type a whole bunch of x1, x2. (although in this case you want to check length and what’s in kwargs first

1 Like

Ok now I see that I will need to unpack args_to_check into the struct S and I cannot use the keywords to fill out S since args_to_check contains pointers different from the keywords (such as x1).

no, it doesn’t, there’s no pointer, really, pointer (as in C++) is not a useful concept in normal Julia code. (internally there are pointer to be used to for passing to ccall() but this is not one of those cases)

If you want to think of x1 as a pointer as synonymous with a label, it’s fine, but just note you can’t rewrite “x1's content/data it points to” when you pass x1 to a function (or when you iterate over an array that contains x1) – because all you get is a value, when x1 is an immutable/scalar. Again, this is what pass-by-sharing entails.

1 Like

Just want to add my solution in the end: I am using a dictionary because I want to keep argument labels in order to reference the argument values

function outer_S_constructorouter_S_constructor(;x1::Float64=NaN, ..., xN::Float64=Nan, <some_other_kwargs...>)
    args_to_check = Dict(
        :x1 => x1,
        ...
        :xN => xN
    )
    for (key, val) in args_to_check
        if isnan(val)
           args_to_check[key] = arg_default_that_depends_on_other_kwargs_so_I_cant_assign_it_in_function_signature
        end
    end
    S(
        args_to_check[:x1],
        ...,
        args_to_check[:xN],
        ...,
        other_args
    )
end