Julia Overview

Julia is a language for scientific computing that has similar features as Matlab and Python, but can usually (via automatic just-in-time compilation) achieve performance close to C/C++.

Similarly to Python, it can be used interactively on the terminal, for executing script files, or via notebooks. The basic language and the accompanying tools are free software.

This is a brief overview of syntax and capabilities. A complete documentation can be found here.

Links

Software

Documentation

Examples

Basics

Built-in capabilities for handling matrices and vectors similar to Matlab

In [1]:
A = [4.  -1.; 1.  2.]
Out[1]:
2×2 Array{Float64,2}:
 4.0  -1.0
 1.0   2.0
In [2]:
b = [1., 2.]
Out[2]:
2-element Array{Float64,1}:
 1.0
 2.0
In [3]:
x = A\b   # solve A*x = b
Out[3]:
2-element Array{Float64,1}:
 0.4444444444444444
 0.7777777777777778
In [4]:
A*x
Out[4]:
2-element Array{Float64,1}:
 0.9999999999999999
 2.0               

Unlike Matlab, Julia differentiates between various basic types:

  • Floating point: Float16, Float32, Float64 (default)
  • Signed integers: Int16, Int32, Int64 (default, alias Int), Int128
  • Unsigned integers: UInt16, UInt32, UInt64 (default, alias UInt), UInt128
  • Bool
  • Complex numbers, rational numbers
  • Arbitrary-precision types: BigInt, BigFloat
  • Char, String
In [5]:
i = 103491;
typeof(i)
Out[5]:
Int64
In [6]:
a = 1. + 3im
b = complex(2., 1.)
a/b
Out[6]:
1.0 + 1.0im
In [7]:
n = 17
c = 's'
s = "tip "*string(n)*": string"*string(c)*" are concatenated with *"
Out[7]:
"tip 17: strings are concatenated with *"

Argument types are strictly enforced:

In [8]:
log(-1.)
DomainError with -1.0:
log will only return a complex result if called with a complex argument. Try log(Complex(x)).

Stacktrace:
 [1] throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:31
 [2] log(::Float64) at ./special/log.jl:285
 [3] top-level scope at In[8]:1
In [9]:
log(complex(-1))
Out[9]:
0.0 + 3.141592653589793im

Arbitrary unicode characters are allowed as identifiers (entered, e.g., via \lambda [TAB], A\^- [TAB] \^1 [TAB]) - see also the character list

In [10]:
using LinearAlgebra
λ, Ψ = eigen(A)  # eigenvalue decomposition, with two separate return values
Out[10]:
Eigen{Float64,Float64,Array{Float64,2},Array{Float64,1}}
eigenvalues:
2-element Array{Float64,1}:
 3.0
 3.0
eigenvectors:
2×2 Array{Float64,2}:
 0.707107  0.707107
 0.707107  0.707107
In [11]:
A⁻¹= inv(A)
Out[11]:
2×2 Array{Float64,2}:
  0.222222  0.111111
 -0.111111  0.444444

General note: In the interactive terminal, to display the help for a specific function, type "?" and then its name (e.g., "eig")

Control flow

In [12]:
c = rand();
if c <= 0.49999
    println("heads");
elseif c >= 0.50001
    println("tails");
else
    println("side");
end
tails
In [13]:
(rand() <= 0.5 ? "heads" : "tails")
Out[13]:
"heads"
In [14]:
for i = 1:10
    print(i, " ");
end
1 2 3 4 5 6 7 8 9 10 
In [15]:
p = 0;
while p < 3 && p > -5
    p += rand([-1,1]);
    println(p, "  ", abs(p), "  ", sign(p));
end
1  1  1
2  2  1
1  1  1
2  2  1
1  1  1
2  2  1
3  3  1

Advanced data structures

Arrays

Matrices and vectors are of Array type. Note that indexing is 1-based.

In [16]:
y = zeros(size(A,2));
for i = 1:size(A,1)
    for j = 1:size(A,2)
        y[i] += A[i,j] * x[j];
    end
end
y
Out[16]:
2-element Array{Float64,1}:
 0.9999999999999999
 2.0               

Support for Array-based operations is similar to Matlab:

In [17]:
x = range(0., stop=1., length=20);
x.^4 .* exp.(-x.^2/2)
Out[17]:
20-element Array{Float64,1}:
 0.0                  
 7.662739828393933e-6 
 0.0001220954599444615
 0.0006138425254290904
 0.0019213270938265815
 0.00463263107424585  
 0.009460977839160578 
 0.017214859280621424 
 0.0287639500805727   
 0.045002106780113724 
 0.06680885149456278  
 0.0950107863621051   
 0.13034437391683382  
 0.17342145188748168  
 0.22469873214322042  
 0.28445236848224176  
 0.3527584743644178   
 0.42948023864709434  
 0.5142620350122585   
 0.6065306597126334   
In [18]:
A = [1. 2. ; 3. 4.]
Out[18]:
2×2 Array{Float64,2}:
 1.0  2.0
 3.0  4.0
In [19]:
A[1,:]
Out[19]:
2-element Array{Float64,1}:
 1.0
 2.0
In [20]:
A[:,1] = [5, 6];
A
Out[20]:
2×2 Array{Float64,2}:
 5.0  2.0
 6.0  4.0

Arrays can also hold other types (e.g., other Arrays). Note the syntax Type{T} for parameterized types (similar to templates in C++, generics in Java and recent versions of Python)

In [21]:
arr1 = Array{Array{Float64,1},2}(undef, 2, 2)
Out[21]:
2×2 Array{Array{Float64,1},2}:
 #undef  #undef
 #undef  #undef
In [22]:
arr1[1,1] = Array{Float64,1}();
arr1[1,2] = Float64[];
arr1[2,1] = Array{Float64,1}([1., 2.]);
arr1[2,2] = Float64[2., 3.];
arr1
Out[22]:
2×2 Array{Array{Float64,1},2}:
 []          []        
 [1.0, 2.0]  [2.0, 3.0]

Even mixed Arrays are possible (but are usually not best for performance):

In [23]:
arr2 = [1, 1., "1", [1.]]
Out[23]:
4-element Array{Any,1}:
 1     
 1.0   
  "1"  
  [1.0]

Arrays are always handled by reference, copies need to be requested specifically:

In [24]:
a = [1., 1.];
b = a;  # b references the same array as a
c = copy(a); # c is a copy of a
b[1] = 2.; 
println("a = ", a, "\nb = ", b, "\nc = ", c)
a = [2.0, 1.0]
b = [2.0, 1.0]
c = [1.0, 1.0]

Tuples

Tuple is an array type that is indexed similarly as a 1D Array. They are initialized with round brackets or simply by a comma-separated list. Tuples are, however, immutable: they cannot be modified after creation.

In [25]:
t = (1, 2)
Out[25]:
(1, 2)
In [26]:
t = 1, 2
Out[26]:
(1, 2)
In [27]:
x, y = 1, 2;   # same as (x,y) = (1,2)
println("x = ", x, ", y = ", y);
x = 1, y = 2
In [28]:
y, x = x, y;  # swap x and y
println("x = ", x, ", y = ", y);
x = 2, y = 1

Sets

In [29]:
MixedSet = Set();
push!(MixedSet,"abc");
push!(MixedSet,π);
push!(MixedSet,9);
MixedSet
Out[29]:
Set(Any["abc", 9, π = 3.1415926535897...])
In [30]:
π in MixedSet
Out[30]:
true
In [31]:
SparseIntSet = Set{Int}([-2411, 1022981,9]);
push!(SparseIntSet, 3);
for i in SparseIntSet
    if i  MixedSet
        println(i);
    end
end
9
In [32]:
DenseIntSet = BitSet([1,3,5,7,11])  # implemented by bit vectors, for non-sparse sets
Out[32]:
BitSet([1, 3, 5, 7, 11])
In [33]:
union(SparseIntSet,DenseIntSet)  # analogously: intersect, symdiff, ...
Out[33]:
Set([7, 9, 1022981, 3, -2411, 5, 11, 1])

Dictionaries

In [34]:
D = Dict{Tuple{Int,Int},Float64}((1,2)=>π, (1,0)=>);
D[(9,9)] = 0.;
D
Out[34]:
Dict{Tuple{Int64,Int64},Float64} with 3 entries:
  (1, 2) => 3.14159
  (1, 0) => 2.71828
  (9, 9) => 0.0
In [35]:
(1,1)  keys(D)
Out[35]:
false
In [36]:
D[(1,0)]
Out[36]:
2.718281828459045

Iterating over collections and other objects

Using the syntax for i = I (or equivalently for i in I or for i ∈ I) one can iterate over any object for which the functions
    iterate(I) → (firstitem, initialstate), iterate(I, state) → (nextitem,newstate),
both returning nothing if when no elements remain, are available, see the documentation. In particular, this applies to the built-in containers:

In [37]:
A = [1., 2., 3.];
for a  A
    print(a, "  ");
end
println("");
B = BitSet([13, 5, 2, 9]);
for b  B
    print(b, "  ");
end
1.0  2.0  3.0  
2  5  9  13  

The range notation 1:n does not create a vector as in Matlab, but a UnitRange object that can be iterated over (but needs only constant memory!)

In [38]:
typeof(1:10)
Out[38]:
UnitRange{Int64}
In [39]:
typeof(0:0.1:1)
Out[39]:
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}

To obtain an actual vector from any iterable, one can use collect

In [40]:
collect(0:0.1:1)
Out[40]:
11-element Array{Float64,1}:
 0.0
 0.1
 0.2
 0.3
 0.4
 0.5
 0.6
 0.7
 0.8
 0.9
 1.0

There are some useful built-in functions for generating new iterable objects from collections.

In [41]:
for (i, x) in enumerate(B)
    println(i, ":  ", x);
end
1:  2
2:  5
3:  9
4:  13
In [42]:
V = [1, 2, 3]; W = [4, 5, 6];
for (x,y) in zip(V, W)
    println(x, ", ", y);
end
1, 4
2, 5
3, 6

A similar syntax can be used in array comprehensions (similar to Python) and in generator expressions:

In [43]:
[n^2 for n=1:5]
Out[43]:
5-element Array{Int64,1}:
  1
  4
  9
 16
 25
In [44]:
[1.0/(i+j) for i = 1:3, j = 1:6]
Out[44]:
3×6 Array{Float64,2}:
 0.5       0.333333  0.25      0.2       0.166667  0.142857
 0.333333  0.25      0.2       0.166667  0.142857  0.125   
 0.25      0.2       0.166667  0.142857  0.125     0.111111
In [45]:
sum(i for i=1:10)
Out[45]:
55
In [46]:
Dict(x => sin(π*x) for x in 0:.5:2.)
Out[46]:
Dict{Float64,Float64} with 5 entries:
  0.0 => 0.0
  0.5 => 1.0
  2.0 => -2.44929e-16
  1.5 => -1.0
  1.0 => 1.22465e-16

Functions and multiple dispatch

In [47]:
function f(x,y)
    y*cos(x), y*sin(x)
end
f(π/3, 2)
Out[47]:
(1.0000000000000002, 1.7320508075688772)
In [48]:
function newtonstep(f::Array, Df::Array, x::Array)
    println("using vector definition");
    return x - Df\f
end

function newtonstep(f::Number, Df::Number, x::Number)
    println("using scalar definition");
    return x - f/Df
end
Out[48]:
newtonstep (generic function with 2 methods)
In [49]:
newtonstep([1,1], [2. -1.; -1. 2.], [0.,0.])
using vector definition
Out[49]:
2-element Array{Float64,1}:
 -1.0
 -1.0
In [50]:
newtonstep(1, 2., 0)
using scalar definition
Out[50]:
-0.5

Inline declaration and anonymous functions:

In [51]:
f(x) = x^2;
g = x->x^3;
f(2), g(2)
Out[51]:
(4, 8)
In [52]:
h = x->(cos(x),sin(x));
h.([π, π/3, π/8])
Out[52]:
3-element Array{Tuple{Float64,Float64},1}:
 (-1.0, 1.2246467991473532e-16)          
 (0.5000000000000001, 0.8660254037844386)
 (0.9238795325112867, 0.3826834323650898)
In [53]:
map(t->t[1], [h(i*π/10) for i = 0:10])
Out[53]:
11-element Array{Float64,1}:
  1.0                  
  0.9510565162951535   
  0.8090169943749475   
  0.5877852522924731   
  0.30901699437494745  
  6.123233995736766e-17
 -0.30901699437494734  
 -0.587785252292473    
 -0.8090169943749473   
 -0.9510565162951535   
 -1.0                  

Names of functions that modify their arguments by convention end in !. For instance, push! adds an element to a data structure, whereas empty! removes all content.

In [54]:
SomeSet = Set([1, 2]);
empty!(SomeSet)
Out[54]:
Set(Int64[])

Note the following pitfall, which arises because array indexing with : creates copies:

In [55]:
function setone!(v)
    fill!(v, 1.);
end
A = [2. 2.; 2. 2.];
setone!(A[:,1]);
A
Out[55]:
2×2 Array{Float64,2}:
 2.0  2.0
 2.0  2.0

To instead pass a reference to the memory of the underlying array, use view:

In [56]:
setone!(view(A,:,1));
A
Out[56]:
2×2 Array{Float64,2}:
 1.0  2.0
 1.0  2.0

Custom types

In [57]:
abstract type RoundShape end

mutable struct Ellipse <: RoundShape
    center::Tuple{Float64,Float64}
    A::Real
    B::Real
end

mutable struct Circle <: RoundShape
    center::Tuple{Float64,Float64}
    r::Real
end

getcenter(S::RoundShape) = S.center;

area(S::Ellipse) = π * S.A * S.B;
area(S::Circle) = π * S.r^2;

Ell = Ellipse((1., 0.), 2, 1);
getcenter(Ell), area(Ell)
Out[57]:
((1.0, 0.0), 6.283185307179586)

Note that concrete types (such as Ellipse, Circle) can only be subtypes of abstract types (here: RoundShape). Built-in abstract types are, e.g., Number, Real, Integer

Macros

In [58]:
@time s = 0; for i=1:1e6 s += i^3; end; print("result: ", s);
  0.000001 seconds (4 allocations: 160 bytes)
result: 2.5000050000024622e23
In [59]:
@time s = sum(map(x->x^3, 1:1e6)); print("result: ", s);
  0.100751 seconds (286.55 k allocations: 21.845 MiB, 4.34% gc time)
result: 2.5000050000025e23
In [60]:
@code_native sin(π/2)^2
	.section	__TEXT,__text,regular,pure_instructions
; Function ^ {
; Location: math.jl:793
; Function Type; {
; Location: math.jl:793
	vcvtsi2sdl	%edi, %xmm1, %xmm1
	decl	%eax
	movl	$3298697720, %eax       ## imm = 0xC49E21F8
	.byte	0xff	.byte	0x7f	.byte	0x00
	addb	%bh, %bh
	loopne	0x68
	nopw	%cs:(%eax,%eax)
;}}
In [61]:
using Printf
println(@sprintf "iteration %d: error %e, step size %e" 139 2.34232131e-5 1.290137e-6)
iteration 139: error 2.342321e-05, step size 1.290137e-06

Modules

Modules are used to encapsulate functions and types. Additional modules can be installed by the built-in package manager.

In [62]:
using Pkg
Pkg.add("StaticArrays");
  Updating registry at `~/.julia/registries/General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
[1mFetching: [========================================>]  100.0 %.0 % Resolving package versions...
  Updating `~/.julia/environments/v1.0/Project.toml`
 [no changes]
  Updating `~/.julia/environments/v1.0/Manifest.toml`
 [no changes]

They are loaded by using or import (where the latter puts identifiers into a separate directory)

In [63]:
using StaticArrays

For using and import, julia searches for modules in the directories listed in LOAD_PATH. To also search for module files you have created in /some/directory, add the following line to the file ~/.juliarc.jl,

push!(LOAD_PATH,"/some/directory")

See also workflow tips for working with modules, and the Revise package:

In [64]:
Pkg.add("Revise");
using Revise
 Resolving package versions...
  Updating `~/.julia/environments/v1.0/Project.toml`
 [no changes]
  Updating `~/.julia/environments/v1.0/Manifest.toml`
 [no changes]

After Revise has been loaded, every change in

Language interoperability

Julia offers interfaces to FORTRAN, C, C++, and Python. It is especially easy to use existing Python packages:

In [65]:
Pkg.add("PyPlot");
using PyPlot
x = range(1e-2,stop=1,length=10000);
plot(x, sin.(1.0./x));
 Resolving package versions...
  Updating `~/.julia/environments/v1.0/Project.toml`
 [no changes]
  Updating
 `~/.julia/environments/v1.0/Manifest.toml`
 [no changes]
In [66]:
Pkg.add("SymPy");
using SymPy
x, y = symbols("x, y", real=true);
diff(x^y*exp(y*exp(x*y)) - x*y + 1, y)
 Resolving package versions...
  Updating `~/.julia/environments/v1.0/Project.toml`
 [no changes]
  Updating `~/.julia/environments/v1.0/Manifest.toml`
 [no changes]
Out[66]:
\begin{equation*}- x + x^{y} \left(x y e^{x y} + e^{x y}\right) e^{y e^{x y}} + x^{y} e^{y e^{x y}} \log{\left (x \right )}\end{equation*}

(Pseudo-)Random numbers

In [67]:
rand([1, 4, 16])
Out[67]:
4
In [68]:
rand("abc")
Out[68]:
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
In [69]:
rand(3,3,3)
Out[69]:
3×3×3 Array{Float64,3}:
[:, :, 1] =
 0.137612  0.219521  0.258723
 0.846962  0.605823  0.961066
 0.125312  0.598955  0.400506

[:, :, 2] =
 0.368323  0.776075  0.625162
 0.274516  0.688724  0.984733
 0.163143  0.878335  0.715124

[:, :, 3] =
 0.436599   0.221008   0.0171596
 0.0239657  0.421176   0.613731 
 0.791671   0.0843151  0.150949 
In [70]:
rand(Int, 2, 2, 2)
Out[70]:
2×2×2 Array{Int64,3}:
[:, :, 1] =
 -5020191725107600484  -5847271543186582121
 -6008095998924860804  -3567908730296754207

[:, :, 2] =
  9164288876322877779  4615745002041857521
 -8504649829447353854  2004894841892499977
In [71]:
using Random
A = zeros(5)
rand!(view(A,1:3))
A
Out[71]:
5-element Array{Float64,1}:
 0.04579687676883348
 0.42363037620611   
 0.17266345126070703
 0.0                
 0.0                
In [72]:
rng = MersenneTwister(5312)  
rand!(rng, A)   # similar effect as calling seed!(5312), but this does not change global random number generator
Out[72]:
5-element Array{Float64,1}:
 0.1824382704421581 
 0.4456902956386928 
 0.17818792893243773
 0.5461182756070553 
 0.12142121921163795
In [73]:
bitrand(3, 3, 3)
Out[73]:
3×3×3 BitArray{3}:
[:, :, 1] =
 false  true  false
 false  true   true
  true  true  false

[:, :, 2] =
  true  false  false
  true   true   true
 false  false  false

[:, :, 3] =
 false  false  true
 false  false  true
  true   true  true
In [74]:
G = randn(1000000);
In [75]:
using Plots; gr()
histogram(G, nbins=200)
WARNING: using Plots.plot in module Main conflicts with an existing identifier.
Out[75]:
-4 -2 0 2 4 0 5.0×10 3 1.0×10 4 1.5×10 4 2.0×10 4 y1
In [ ]: