Syntax#
Overview
If you're familiar with Python and MATLAB, Julia syntax is a combination with some extra features and syntax sugar.Objectives: Learn…
The basic syntax of Julia like declaring variables, functions and structs
Broadcasting operations over arrays
Basic File IO
Common macros
Variables and Types#
Julia is strongly typed, but providing type information is optional. The compiler will try to infer type information, and will throw an error when it fails.
x = 3
y::Int64 = 4
4
# If the compiler can convert the type, it will do so automatically
a::Float64 = 3
3
# This will throw an error because 3.4 cannot be converted to Int64
z::Int64 = 3.4
InexactError: Int64(3.4)
Stacktrace:
[1] Int64
@ ./float.jl:994 [inlined]
[2] convert(::Type{Int64}, x::Float64)
@ Base ./number.jl:7
[3] top-level scope
@ In[3]:2
Julia lets you use any unicode character to define variables. Just type \alpha
and hit tab
. For underscores type c\_p
and hit tab. Note that not all subscripts and superscripts are supported. For more details about allowed characters and variable names, see this docs page.
κ = 2
L = 3
cₚ = 4
ρ = 5
t = 1
α = κ / (cₚ * ρ)
Fo = α * t / (L^2)
0.011111111111111112
Arrays#
Arrays in Julia are:
Dynamically sized
One-indexed
Column-major
Slices allocate a new array (see views)
Julia Arrays behave a lot like numpy and MATLAB arrays and allow you to broadcast operations over the entire array and use index slicing. The Array
type has several sub-types for conveinece, such as Vector
for 1D arrays and Matrix
for 2D arrays. Check out the Array
documentation for more details.
arr = [1,2,3,4]
arr[1] = 5 # arrays are 1-indexed
arr
4-element Vector{Int64}:
5
2
3
4
matrix = zeros(3,3)
matrix = ones(3,3)
3×3 Matrix{Float64}:
1.0 1.0 1.0
1.0 1.0 1.0
1.0 1.0 1.0
println(arr[1:3]) # note slice range is inclusive
println(arr[3:end]) # CANNOT just do [3:] like in Python
[5, 2, 3]
[3, 4]
The same notation can be used to create ranges as iterables, and if you want to specify a step size, this goes in the middle, not in the end like it would for a slice in Python.
println(0:2:10) # this won't be "expanded" to an array by default
println(collect(0:2:10)) # but we can force it to like this (casting to an Array type would also work)
0:2:10
[0, 2, 4, 6, 8, 10]
Functions#
Julia functions are defined with the function
keyword and wrapped with end
. If type information is not provided in the function signature then function specializations are created at compile time depending on the data passed to the function. Learn more about functions in the Julia documentation here.
function mult(x, y)
return x * y
end
mult (generic function with 1 method)
z_int = mult(1,2)
z_float = mult(1.0, 2.0)
# Side note, you can interpolate variables into a string with the $() syntax
println("Integer multiplication: $(z_int) with type $(typeof(z_int))")
println("Float multiplication: $(z_float) with type $(typeof(z_float))")
Integer multiplication: 2 with type Int64
Float multiplication: 2.0 with type Float64
Exercise:
Please define a function named get_last that takes an array and returns the last element.Solution:
function get_last(arr)
return arr[end]
end
Type information can be set by the user as well. In practice, multiple add
functions can be defined with different type information associated to their parameters. This is called multiple dispatch and will be covered in depth in the next section.
function add(x::Int, y::Int)
println("Adding integers")
return x + y
end
function add(x::Float64, y::Float64)
println("Adding floats")
return x + y
end
add (generic function with 2 methods)
z_int = add(1, 2);
z_float = add(1.0, 2.0);
Adding integers
Adding floats
Julia also supports multiple return values. You can enforce the type of the return variables but it is recomended to allow the compiler to determine their types.
function multiple_return(x, y)
x_plus_y = x + y
x_times_y = x * y
return x_plus_y, x_times_y
end
multiple_return (generic function with 1 method)
z1, z2 = multiple_return(3, 4)
println("Sum: $(z1), Product: $(z2)")
Sum: 7, Product: 12
# Small functions can be defined in one line
square(x) = x * x
square(2)
4
# Can also define them like this
f = (x,y) -> x + y
f(2,3)
5
Exercise
Define a one line function called cube that takes a Float32 and returns the cube of that number.Solution:
function cube(x::Float32)
return x*x*x
end
In Julia, data passed into a function is not copied to save memory. Therefore, modifications within the function will modify the data outside of the function as well. If your function modifies parameters, it is customary to end the function name with !
.
function modifies_x!(x)
x[1] = 336
y = [1,2,3]
return x + y
end
x = [1,2,3]
println("Before: $(x)")
modifies_x!(x)
println("After: $(x)")
Before: [1, 2, 3]
After: [336, 2, 3]
For more about scope of variables, see this page. Read lots more about functions in Julia in general in the docs here.
Loops & Conditionals#
The syntax here is very similar to Python without the :
, you just need an end
to denote the end of the block. As in Python, indentation doesn’t formally matter, but most style guides strongly suggest it for readability. The documentation on looping data can be found here.
# For loops
for i in range(1, 3) #1:3
print("$i ")
end
println()
for element in [1, 2, 3]
print("$(element) ")
end
println()
# While loops
counter = 1
while counter <= 3
print("$(counter) ")
counter += 1
end
1 2 3
1 2 3
1 2 3
Similarly, the if-else syntax is similar to Python. Just remove the :
and add an end
function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "greater than"
end
println("x is ", relation, " y.")
end
test(1, 2)
test(1,1)
x is less than y.
x is equal to y.
For more about control flow in Julia, see this docs section.
Structs#
Structs are like classes in Python and MATLAB. By default structs are immutable. Providing type information in a struct is common but not required. More info can be found in the Julia documentation here.
struct MyType
a::Int
b::Float64
end
MyType(3, 1.0)
MyType(3, 1.0)
MyType(3.4, 1.0)
InexactError: Int64(3.4)
Stacktrace:
[1] Int64
@ ./float.jl:994 [inlined]
[2] convert
@ ./number.jl:7 [inlined]
[3] MyType(a::Float64, b::Float64)
@ Main ./In[20]:2
[4] top-level scope
@ In[22]:1
In Python you need to definie __init__
which tells Python how to construct your object. Julia will always create a default constructor if one is not provided. To create an extra constructor, define a function with the same name as your struct
.
# Constructor that handles the case when only one parameter is known
function MyType(a)
return MyType(a,a)
end
MyType
MyType(3)
MyType(3, 3.0)
Exercise
- Define a struct for a circle that contains its radius.
- Define a method that returns the area of a circle
Solution:
struct Circle
radius::Float64
end
# Note how the method is not defined inside the class
# like in many other programming languages.
area(c::Circle) = pi*(c.radius^2)
c = Circle(2)
area(c)
To make your structs more flexible, you can use parametric types. Parametric types get complex quickly (but can be very useful in certain contexts!), so if you’re interested to learn more, check out the documentation here.
struct ParametricType{T}
x1::T
x2::T
end
p_int = ParametricType(1, 2)
println(typeof(p_int))
p_float = ParametricType(1.0, 2.0)
println(typeof(p_float))
ParametricType{
Int64}
ParametricType{Float64}
Broadcasting#
Like in MATLAB you can broadcast operations like, +
, with the .
syntax. In Julia you can also broadcast functions with the .
syntax. See the docs for more info.
x_vals = ones(4)
x_vals = x_vals .+ 2
4-element Vector{Float64}:
3.0
3.0
3.0
3.0
One difference from MATLAB is that Julia more often wants you to be “explicit” about what you mean by a broadcast. Whereas in MATLAB, the following would default to an element-wise add, here it errors.
x_vals + 2
MethodError: no method matching +(::Vector{Float64}, ::Int64)
For element-wise addition, use broadcasting with dot syntax: array .+ scalar
The function `+` exists, but no method is defined for this combination of argument types.
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...)
@ Base operators.jl:596
+(::Complex{Bool}, ::Real)
@ Base complex.jl:323
+(::BigInt, ::Union{Int16, Int32, Int64, Int8})
@ Base gmp.jl:550
...
Stacktrace:
[1] top-level scope
@ In[28]:1
However, multiplication does work:
x_vals * 2
4-element Vector{Float64}:
6.0
6.0
6.0
6.0
Generally, the rules follow rules about mathematical notation with vectors and matrices.
Here are some more examples of broadcasting…
x_vals .+= [3,4,5,6]
4-element Vector{Float64}:
6.0
7.0
8.0
9.0
y_vals = ones(4)
y_vals .+= 2
4-element Vector{Float64}:
3.0
3.0
3.0
3.0
function add_one(x)
return x + 1
end
z_vals = [1,2,3,4]
# The function add_one is broadcast over the array, z_vals.
add_one.(z_vals)
4-element Vector{Int64}:
2
3
4
5
Exercise
Broadcast the square function defined earlier over any array.Solution:
res = square.([1,2,3,4])
File I/O#
# Writing files
outpath = joinpath(@__DIR__, "data", "write_test.txt")
open(outpath, "w") do file
write(file, "Hello, World!")
end
SystemError: opening file "/home/runner/work/julia-introduction/julia-introduction/book/data/write_test.txt": No such file or directory
Stacktrace:
[1] systemerror(p::String, errno::Int32; extrainfo::Nothing)
@ Base ./error.jl:176
[2] systemerror
@ ./error.jl:175 [inlined]
[3] open(fname::String; lock::Bool, read::Nothing, write::Nothing, create::Nothing, truncate::Bool, append::Nothing)
@ Base ./iostream.jl:295
[4] open
@ ./iostream.jl:277 [inlined]
[5] open(fname::String, mode::String; lock::Bool)
@ Base ./iostream.jl:358
[6] open(fname::String, mode::String)
@ Base ./iostream.jl:357
[7] open(::var"#3#4", ::String, ::Vararg{String}; kwargs::@Kwargs{})
@ Base ./io.jl:408
[8] open(::Function, ::String, ::String)
@ Base ./io.jl:407
[9] top-level scope
@ In[33]:3
There is a package in base Julia (i.e. you don’t have to explicitly install it, just import it) called DelimitedFiles
, which operates similarly to numpy.loadtxt
in Python. Here we briefly pretend that we don’t know about that and use the default functionality of Julia to parse a file. This example brings together a lot of what we have learned so far.
# Reading files
function parse_file(inpath::String)
data = [] # Vector{Float64}(undef, 3)
open(inpath, "r") do file
# eachline returns an iterator over lines in the file
# this avoids loading the entire file into memory.
for line in eachline(file)
# strip() removes whitespace
line = strip(line)
# Checks if the line starts
if startswith(line, "#")
println("Ignoring Comment: ", line)
continue
else
# split() converts the line into an array, splitting on whitespace
vals = split(line)
# parse() is broadcast over the elements of vals to convert them to Float32
# push!() adds the parsed values to the vector, data.
push!(data, parse.(Float32, vals))
end
end
end
return data
end
inpath = joinpath(@__DIR__, "data", "read_test.txt")
parse_file(inpath)
SystemError: opening file "/home/runner/work/julia-introduction/julia-introduction/book/data/read_test.txt": No such file or directory
Stacktrace:
[1] systemerror(p::String, errno::Int32; extrainfo::Nothing)
@ Base ./error.jl:176
[2] systemerror
@ ./error.jl:175 [inlined]
[3] open(fname::String; lock::Bool, read::Bool, write::Nothing, create::Nothing, truncate::Nothing, append::Nothing)
@ Base ./iostream.jl:295
[4] open
@ ./iostream.jl:277 [inlined]
[5] open(fname::String, mode::String; lock::Bool)
@ Base ./iostream.jl:358
[6] open(fname::String, mode::String)
@ Base ./iostream.jl:357
[7] open(::var"#5#6"{Vector{Any}}, ::String, ::Vararg{String}; kwargs::@Kwargs{})
@ Base ./io.jl:408
[8] open
@ ./io.jl:407 [inlined]
[9] parse_file(inpath::String)
@ Main ./In[34]:4
[10] top-level scope
@ In[34]:29
Macros#
Julia has a special feature called macros, which act upon your code to generate new code. The details of how macros work are complicated, but as an end-user, there are plenty of useful macros available to use.
@time
: Measures the run time, allocations, compile time, and garbage collection time of a piece of code
@time rand(3,3)
0.000001 seconds (2 allocations: 144 bytes)
3×3 Matrix{Float64}:
0.860948 0.855272 0.00447448
0.688968 0.136781 0.930878
0.66677 0.633124 0.938454
@views
: Converts all array slices in the same line/block into views. This is very useful for avoiding unnecessary allocations
a = rand(5)
b = @views 3.1 .+ a[3:end]
MethodError: Cannot `convert` an object of type Vector{Float64} to an object of type Float64
The function `convert` exists, but no method is defined for this combination of argument types.
Closest candidates are:
convert(::Type{T}, ::T) where T<:Number
@ Base number.jl:6
convert(::Type{T}, ::T) where T
@ Base Base.jl:126
convert(::Type{T}, ::Number) where T<:Number
@ Base number.jl:7
...
Stacktrace:
[1] top-level scope
@ In[36]:1
@info
: Pretty prints data to the screen with a large INFO tag. In the REPL or a terminal this would be colored blue.
@info "May I have your attention please!";
[ Info: May I have your attention please!
@show
: Prints a quick debug statement with the variable name and value
value = 1234
@show value;
value = 1234
Key Points
- Julia syntax is simple and similar to Python and MATLAB
- Julia supports standard control flow like loops and conditionals
- Classes/Structs do not have member functions, instead functions are written which take the struct as a parameter
- Broadcasting makes applying math and functions over arrays simple and efficient
- Macros modify your code to obtain a new, useful outcome (e.g. timing your code)