Arrays are a good example to get some idea of the Julian way of defining interfaces.
Arrays can be defined by explicitly providing their content:
i = [1, 2, 3 , 4] # An integer array
f = [1.2, 3.4, 2.3, π] # A float array
s = ["abc", "def", "ghi"] # A string array
Notice, that the type of arrays is Array{T, N}
, where T
is a type and N
is the number of dimensions. This type is an example of a parametric type, i.e. a type, which itself is parametrised by other values or types.
Arrays do not need to be of one type only, for example:
m = [1, 3.4, "abc"] # A mixed array
In some cases, Julia tries to be smart, however, and converts the types of the elements. For example:
m = [1, 3.4, 4] # As soon as there is one float element, it's a float array
One can influence this by explictly denoting the type:
m = Number[1, 3.4, 4]
# or
m = Float32[1, 3.4, 4]
Even though all above arrays present as Array
, their data layout in memory differs. Arrays with pointer-free element types such as Array{Float64, 1}
or Array{Int64, 1}
are stored contiguously in memory, but Array
s with mutable types (such as Array{String, 1}
and arrays containing abstract types are stored boxed, which means that only a pointer to a different memory location is stored inside the array.
On the interface level Julia does not distinguish between both kind of arrays. Since operations on boxed elements are a lot slower, arrays with abstract types should be avoided in critical parts of a program.
Multidimensional arrays can be defined similarly:
A = [[1 2 3]; # Define row-wise
[5 6 7]]
A = [[1, 5] [2, 6] [3, 7]] # Define column-wise
Notice the subtle difference to an Array of Arrays
A = [[1, 5], [2, 6], [3, 7]]
Yet another option is to explicitly filled arrays:
A = zeros(3, 4, 2)
@show randn(1, 2) # Float64
A = randn(Float32, 3, 4, 2)
Array addition (+
, -
) and scalar multiplication are directly available on arrays (of any dimension):
x = [1,2,3]
y = [4,5,6]
x + 2.0y
For element-wise operations the vectorisation syntax is used:
x .* y # elementwise mulitplication
x .^ y # Elementwise exponentiation
Note, that the .
-syntax continues to functions of the standard library ...
sqrt.(cos.(2π * x) .+ sin.(2π * x))
@. sqrt(cos(2π * x) + sin(2π * x))
... custom functions ...
myrand(x) = rand() * x
myrand.(y)
... and may be easily chained
@. exp(cos(x^2))
Create the following arrays using Julia code: $$\left(\begin{array}{ccccc} 2&2&2&2&2 \\ 2&2&2&2&2 \\ 2&2&2&2&2 \\ \end{array}\right) \qquad \left(\begin{array}{cccc} 0.1&0.5&0.9&1.3\\ 0.2&0.6&1.0&1.4\\ 0.3&0.7&1.1&1.5\\ 0.4&0.8&1.2&1.6\\ \end{array}\right) $$
Array elements can be indexed individually ...
A[1, 2, 1] = 15
@show A[1, 2, 1]
@show A[2, 2, 2];
... or using ranges
A[2:end, 1, :] = ones(2, 2) # : is full range, 2:end means ignore 1st
@show A[2:end, 1, :]
@show A[1, 1, :];
It should be noted that Arrays in Julia are stored in column-major order (like in MATLAB, FORTRAN and R) and that indices start with 1:
vec = collect(1:6) # Collect all numbers 1 to 6 in a Vector
@show vec
A = reshape(vec, 2, 3) # Reshape the result
for i in 1:length(A)
# Iterate through the storage in memory order
println(i, " ", A[i])
end
A = randn(Float32, 4, 5)
ndims(A) # Get the number of dimensions
eltype(A) # Get the type of the array elements
length(A) # Return the number of elements
size(A) # Get the size of the array
size(A, 1) # Get the size along an axis
reshape(A, 2, 5, 2) # Return an array with the shape changed
We want improve our capabilities to perform simulations to two dimensions using the 2D harmonic potential $$ V\left( \begin{array}{c} x_1 \\ x_2 \end{array}\right) = x_1^2 + x_2^2 $$ and the acceleration map $$ \vec{A}_V = - \nabla V. $$ The adapted forward-Euler scheme is $$\left\{\begin{array}{l} \vec{v}^{(n+1)} = \vec{v}^{(n)} + \vec{A}_V(\vec{x}^{(n)}) \Delta t\\ \vec{x}^{(n+1)} = \vec{x}^{(n)} + \vec{v}^{(n)} \Delta t\\ \end{array}\right. .$$
Change the euler
function we introduced in 02_Functions_Types_Dispatch.ipynb
accordingly (Hint: Suprisingly few changes are needed) and run the dynamics for five steps using
$$x_n = \left( \begin{array}{c} 0 \\ 0 \end{array}\right) \qquad v_n = \left( \begin{array}{c} 1 \\ 0 \end{array}\right) .$$
Check against your previous implementation.
# You're code here
Julia provides the push!
and append!
functions to add additional elements to an existing array.
For example:
A = Vector{Float64}() # Create an empty Float64 array
push!(A, 4.)
append!(A, [5, 6, 7])
Notice, that the !
is part of the name of the function. In Julia the !
is a convention to indicate that the respective function mutates the content of at least one of the passed arrays.
Very helpful functions for type-generic code in Julia are:
zero
, which allocates an array of zeros of the same element typeA = randn(Float32, 3, 4)
zero(A)
similar
, which returns an uninitialised array, which is similar to the passed array. This means that by default array type, element type and size are all kept.similar(A)
similar(A, (3, 2)) # Keep element type and array type
similar(A, Float64) # Change element type
similar(A, Float64, (1, 2)) # Change element type and shape
A syntax Julia borrowed from Python are comprehensions. For example:
arr = randn(5)
[e for e in arr if e > 0]