Is `let` the right tool for this task?

I would like to define a certain function that takes a vector as input and does some computations that involving varying just one of the entries. For example, if the input vector is a, I would like to compute f(a) whlie “pretending” that a[1] = 0.

One way to do this is b = copy(a); b[1] = 0, f(b), but this requires copying a each time I call f. The problem is that I will need to do this many times, letting a[1] = 0.0, a[1] = 0.1, etc., and then similar for a[2] (think numerical integration).

Here is a working but inefficient function that creates copies:

# Desired behavior, but makes a bunch of copies
function foo(a::Vector)
    for m in 0:0.1:1
        b = copy(a)
        b[1] = m
        # do some computations on b
        @show b
    end
end

foo(rand(3))

#= 
    b = [0.0, 0.70594453750852, 0.21204039022374865]
    b = [0.1, 0.70594453750852, 0.21204039022374865]
    b = [0.2, 0.70594453750852, 0.21204039022374865]
    b = [0.3, 0.70594453750852, 0.21204039022374865]
    b = [0.4, 0.70594453750852, 0.21204039022374865]
    b = [0.5, 0.70594453750852, 0.21204039022374865]
    b = [0.6, 0.70594453750852, 0.21204039022374865]
    b = [0.7, 0.70594453750852, 0.21204039022374865]
    b = [0.8, 0.70594453750852, 0.21204039022374865]
    b = [0.9, 0.70594453750852, 0.21204039022374865]
    b = [1.0, 0.70594453750852, 0.21204039022374865]
=#

Here I use let to avoid making copies, but I have to tuple-unpack the vector, so it only works for vectors of length 3:

# Working version using let; avoids copying, but only
# works for 3-vectors
function bar(z::Vector)
    a, b, c = z
    
    for m in 0:0.1:1
        let a = m
            # do some computations
            @show [a, b, c]
        end
    end
end

bar(rand(3))

#= 
    [a, b, c] = [0.0, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.1, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.2, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.3, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.4, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.5, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.6, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.7, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.8, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [0.9, 0.2899170892058196, 0.9979264085173909]
    [a, b, c] = [1.0, 0.2899170892058196, 0.9979264085173909]
=#

Here is what I would really like to do:

# Attempt to do this without making so many copies of a
function baz(a::Vector)
    for m in 0:0.1;1
        let a = a, a[1] = m
            # do some computations on a
            @show a
        end
    end
end

baz(rand(3))

#= 
    syntax: invalid let syntax around In[66]:4

    Stacktrace:
     [1] top-level scope at In[66]:2
     [2] include_string(::Function, ::Module, ::String, ::String) at .\loading.jl:1091
=#

Am I on the right track? Is there a better way to accomplish this task?

Are you always changing the same entry b[1] at each iteration? If so, do you need to copy b every time, or can you just reuse the same one?

There is an inner loop in which I am just taking a[1] = m for m in 0:0.1:1, and then an outer loop where I change a[i] for i in 1:length(a). So I can move b = copy(a) to the outside of the inner loop, but this is only a partial solution, because it still requires creating length(a) copies of a in the outer loop.

Even if this were valid syntax, it wouldn’t do what you want anyway because let a=a doesn’t change the fact that a points to the same value, so mutating that array will still mutate the input array (since they’re all the same array).

I’m not sure you need any copies at all, though. Can you just look up the current value of a[I] and store that, then restore that single value after the end of the inner loop?

4 Likes

I think you should be more explicit about what kind of computation do you need to do with b. But if it is something that really requires it as a vector, I would create a struct as

struct B{T} <: Vector{T}
  i::Int
  b::T
  a1:: Vector{T}
end
function Base.getindex(x::B,i)
  if i == x.i
    return x.b
  else
    return x.a[i]
  end
end
  

(Overload size probably as well, but I understand that you will not modify the values of b in the computation part, thus no need to overload setindex! and make B mutable)

With that you can do

b = B(5, 2.8, a)

and work with it as an array without having to copy the whole a. This b will behave as vector identical to a except at position 5.

Edit: or simply copy the value of a[i] in a temporary variable, change it, and restore it later, as rdeits said, if you don’t need both a and b in the computation)

1 Like

This has proven to be the best solution for the problem at hand.

I guess I just expected to be some a built-in way to do this kind of “substitution,” sort of like using filter() instead of storing a new array or the various masking features in NumPy. However, I have no real basis for this expectation, such as analogous functionality in another language, so I won’t complain.

Masked arrays are discussed in Mask on a 1D array - #2 by rdeits

2 Likes