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