Arrays#

Julia supports Arrays as first-class values - they’re represented in the type hierarchy as a set of parameterised types (by geometry and the type of their elements).

For 1-d Arrays, things look much like Python lists - in fact, an array literal containing a mix of types will work much like a Python list in all respects. We can also write “comprehensions” as for Python lists.

Arrays holding just one type are specialised - for efficiency - to just hold that type.

@show my_list = [1, 2, 'a'] #you can make "Python like" arrays with mixed types in them

@show my_integer_array = [1,2,7, 66] #but Julia will specialise an Array literal with only 1 type in it to be uniform

@show typeof(my_integer_array)

@show my_float_array = Float64[1, 5.5, 5//6] #or we can explicitly impose a type

@show squared_integers = [i^2 for i in 1:10];
my_list = [1, 2, 'a'] = Any[1, 2, 'a']
my_integer_array = [1, 2, 7, 66] = [1, 2, 7, 66]
typeof(my_integer_array) = Vector{Int64}
my_float_array = Float64[1, 5.5, 5 // 6] = 
[1.0, 5.5, 0.8333333333333334]
squared_integers = [i ^ 2 for i = 1:10] = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Indexing of Julia arrays starts at 1 by default, not 0, although arrays can be customised to have different index ranges. The keywords begin and end always refer to the first and last elements in a particular index.

As well as specifying a specific index, or range of indices, Julia arrays also support other selection methods - for example, selection masks where only the true elements are returned.

@show my_list[1] == my_list[begin] #basic indexing

@show my_integer_array[3:end] #last 2 elements

mask = [ iseven(i) for i in 1:10 ] #only even values are true
@show squared_integers[mask]; #selects only elements in the mask
my_list[1] == my_list[begin] = true
my_integer_array[3:end] = [7, 66]
squared_integers[mask] = [4, 16, 36, 64, 100]

Multi-dimensional Arrays#

Julia also supports multi-dimensional Arrays internally as first-class types, (that is, they’re not simply “an array of arrays” or whatever), allowing for efficient representations of specialised data (like SparseArrays or diagonal matrices).

As a result, indexing is done with a single index expression for all axes - in addition, Julia arrays are column-major, unlike C, Python etc.

@show reflection = [ [1,0] [0,-1] ]

"""The top-left element of reflection is $(reflection[1,1]) or $(reflection[begin,begin])"""
reflection = [[1, 0] [0, -1]] = [1 0; 0 -1]
"The top-left element of reflection is 1 or 1"

Multi-dimensional array literals can be created with a terse concatenation syntax: sequences of multiple semicolons represent concatenation of sub-arrays in increasingly higher dimensions (space separation is equivalent to “row” and comma separation to “column” ordering).

threed_array = [ 1 ; 0 ;; 2 ; 2 ;;; 3 ; 3 ;; 0 ; 1 ] 
2×2×2 Array{Int64, 3}:
[:, :, 1] =
 1  2
 0  2

[:, :, 2] =
 3  0
 3  1

We can also use the fact that for takes the outer product of its ranges to write a compact comprehension, if our array is expressible in such a way.

odd_powers_and_offset = [ i^j + k for i in 0:3, j in 1:2:5, k in 0:1 ] 
4×3×2 Array{Int64, 3}:
[:, :, 1] =
 0   0    0
 1   1    1
 2   8   32
 3  27  243

[:, :, 2] =
 1   1    1
 2   2    2
 3   9   33
 4  28  244

Array constructors#

As with NumPy, there are a large number of utility methods to efficiently create Arrays of a specific type and geometry, and to reshape existing arrays.

@show many_zeros = zeros(Float64, 2, 4, 3) #2x4x3 array, zero'd

m = [0,1,2,3]

reshape(m, (2,2)) #2x2 matrix, column-major!
many_zeros = zeros(Float64, 2, 4, 3) = [0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0;;; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0;;; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0]
2×2 Matrix{Int64}:
 0  2
 1  3

Views#

We can “slice” into an array in all dimensions with a syntax familiar from Python, but by default this creates a copy of the original array data.

slice1 = odd_powers_and_offset[:,:,1]
slice2 = odd_powers_and_offset[:,:,1]

@show slice1 === slice2 #false, because these are both different copies of the underlying data

slice1[end,end,end] = 88

slice2[end,end,end] #hasn't changed because these are different memory
slice1 === slice2 = false
243

Julia provides view to instead give us an equivalent slice which references the original array - there’s no copy incurred, and as a result modifying the view modifies the corresponding elements of the source array. Views are explicitly lazy, so they also work well with tabular implementations that may page rows into memory or otherwise work lazily themselves.

In order to make this more ergonomic, the macros @view (for a single slice operation) and @views (for an expression containing multiple slices) will convert slices into views automatically.

A number of methods exist for views to inspect their parent arrays.

view1 = view(odd_powers_and_offset, :, : , 1)
view2 = @view odd_powers_and_offset[:,:,begin]
@views view3 = odd_powers_and_offset[:,:,begin]   

@show view1
@show view1 === view2 === view3 #true, because these reference the same memory

view1[end,end,end] = 88

odd_powers_and_offset[end,end,1] #has changed, as these are the same memory
view1 = [0 0 0; 1 1 1; 2 8 32; 3 27 243]
view1 === view2 === view3 = true
88