Using eval to evaluate a variable inside an expression when string interpolation is not ok

I am learning Julia and I am trying to create a function to shuffle an n-dimensional array X along a desired dimension. This same strategy will be used later to split an array into 2 subarrays along a desired dimension.

The intended use is as follows:

Command:

A=[[1,2,3] [4,5,6]]
shuffle_array(A,1)

Expected Result (random):

3×2 Matrix{Int64}:
 2  5
 1  4
 3  6

My approach was to build a string, parse it and evaluate it. First, I was not using string interpolation for X and I got UndefVarError: X not defined. I understand this happens because X is defined inside a function and not a global variable. So I wrote $X, but this string interpolation writing all the elements of X as strings and then parsing them as symbols. In my application X will contain thousands of elements.

Question 1: how can I pass X into an expression so that it is only evaluated in the line that defines Xshuffled?
Question 2: if you have another strategy that avoids using eval, please let me know.

Below is a sample code. The line with @show shows the undesired effect of typing $X

I am running the code in a jupyter notebook with Julia v1.7.

Thank you in advance.

using Random
function shuffle_array(X::AbstractArray,shuffle_dim::Int=1)
    """ Shuffle a multidimensional array along desired direction"""
    dims = size(X)
    @show ndims = length(dims)
    m = dims[shuffle_dim]    
    ndims==1 && return X[Random.shuffle(1:m)]    
    if shuffle_dim ==1
        left_part = ""
        middle_part = "$X[Random.shuffle(1:$(m)),"
        right_part = chop(":,"^(ndims-shuffle_dim))*"]"        
    elseif shuffle_dim == ndims
        left_part = "$X[" * (":,"^(shuffle_dim-1))
        middle_part = "Random.shuffle(1:$(m))]" 
        right_part = ""        
    else
        left_part = "$X[" * (":,"^(shuffle_dim-1))
        middle_part = "Random.shuffle(1:$(m))," 
        right_part = chop(":,"^(ndims-shuffle_dim))*"]"
    end
    @show left_part*middle_part*right_part
     Xshuffled = eval(Meta.parse(left_part*middle_part*right_part))
    return Xshuffled
end

For question 2, I realized that this can be done with the help of a splat (...) operator. See code below. However, I would like to have an answer for question 1. Maybe it would require a macro?

function shuffle_array2(X::AbstractArray,shuffle_dim::Int=1)
    """ Shuffle a multidimensional array along desired direction"""
    dims = size(X)
    ndims = length(dims)
    ind = vcat([1:dims[i] for i in 1:shuffle_dim-1],
                [Random.shuffle(1:dims[shuffle_dim])],
                [1:dims[i] for i in shuffle_dim+1:ndims])
    return X[ind...]
end

About question 1, the issue is that you’re interpolating into a string, so it has to convert the array X to a substring. There’s really no way around that. You could interpolate X without conversion into an Expr (what Meta.parse turns a string into), and I’m pretty sure no conversion happens when that Expr is eval-ed into working code.

But you really don’t want to generate code for every input array or run all your code in the global scope. Something like shuffle_array2 or generating such a reusable function is much more preferable. Only suggestion I could give on that is you could instead index with variable numbers of :, which seems to be why you tried generating code in the first place. For example, x[:,:, 1:3, :] is equivalent to x[fill(:, 2)..., 1:3, fill(:, 1)...].

Thank you @Benny! I had no idea I could use fill like that.
Concerning question 1, I reached the same conclusion after reading some forums. But maybe it could be done with macros (I am not sure how), because there one can quote and escape a variable.

Macro can’t generate the code you need for this case because its input expressions are parsed from source code and thus only contain expressions, symbols, and literals. X isn’t evaluated as the runtime array yet, so a macro won’t have access to the runtime size. Only a method would. If you were working with StaticArrays where the size is in the type, a generated function could work on the size, but a macro still couldn’t.

Note that you can also do A = [1 4; 2 5; 3 6] instead of first constructing 1d arrays and concatenating them.

You can use

julia> using Random # for shuffle

julia> mapslices(shuffle, A, dims=1)
3×2 Matrix{Int64}:
 2  5
 3  4
 1  6

Metaprogramming (generating code) can be a powerful tool, but it’s also a specialized one that is used rarely in practice — it’s not something I would typically recommend for starting users. See How to warn new users away from metaprogramming - #22 by stevengj

1 Like