Operators and Variables#

All languages have their syntax and here we present a whistle-stop tour of the highlights of Julia.

We don’t attempt to be exhaustive here - check the Julia Documentation (or do a search) if there’s anything you are unsure of, and for more functionality we haven’t covered here on advanced topics.

Variables#

A variable is a name bound to a value and assignment is done with = (no surprise!):

x = 1.09
my_amazing_string = "a string"
δ = 1.0e-9
1.0e-9

Note: In REPL mode, Julia will print the results of the last statement’s execution as output. Sometimes you don’t want that, in which case it can be suppressed by adding a semicolon at the end of the statement:

δ2 = 1.0e-18;

Operators#

Binary Operators#

+, -, *, / does what you expect, and there are a few less common ones:

Expression

Name

Description

x ^ y

power

raises x to the yth power

x ÷ y

integer divide

x / y, truncated to an integer, same as div(a,y)

x % y

remainder

same as rem(x,y)

In particular, note that Julia uses ^ for exponentiation, not logical operations (and not **, as some other languages).

(2^10) + (33%10)
1027

Bitwise Operators#

Bitwise operators are supported on all primitive integer types:

Expression

Name

~x

bitwise not

x & y

bitwise and

x | y

bitwise or

x y

bitwise xor (exclusive or)

a = 7
b = 13
@show a & b
@show a | b;
a & b = 5
a | b = 15

Here we’re also using the @show macro, which explicitly prints the result of an expression.

Teaser: We’ll look at a very cool feature of vectorised or broadcast operators shortly.

Updating and Testing#

Most variables (except for const globals) can be updated, with the usual updating operators (+=, -=, *=, /=, etc.):

a *= 2
14

Comparative testing of values uses the usual operators (==, >, <, >=, <=) and returns a bool type, which can be true or false:

a >= 8
true
a < δ2
false

The ! operator negates a boolean:

!true
false

Julia also has a === comparison operator that is true when two objects are the same (not just the same value(s), but really the same object)

a = [1, 2]
b = [1, 2]
c = a
@show a === b
@show a === c
a === b = false
a === c = true
true

Julia allows you to chain comparisons:

1 < 2 <= 2 < 3 == 3 > 2 >= 1 == 1 < 3 != 5
true

Basic Types#

Julia supports integers (signed and unsigned) and floats, all with varying bit widths:

f = 1.234
typeof(f)
Float64
i = UInt(12335124)
0x0000000000bc3814
typeof(i)
UInt64

Julia will generally handle mixed type arithmetic smoothly and safely (this is done by promoting variables):

f * i
1.5221543015999999e7

But it will generally throw an error if this can’t be done safely:

Int16(i)
InexactError: trunc(Int16, 12335124)

Stacktrace:
 [1] throw_inexacterror(f::Symbol, #unused#::Type{Int16}, val::UInt64)
   @ Core ./boot.jl:634
 [2] checked_trunc_sint
   @ ./boot.jl:656 [inlined]
 [3] toInt16
   @ ./boot.jl:687 [inlined]
 [4] Int16(x::UInt64)
   @ Core ./boot.jl:782
 [5] top-level scope
   @ In[15]:1

As in Python, you don’t need to specify a type for a variable (unlike C++) - by default, they work like Python, and can freely be assigned any values of any type.

However, you can specify a type for a variable, using the ::Type notation, which causes that variable to only hold values of that type:

integer_value::Int64 = 6;

integer_value = 2.0;

@show integer_value

typeof(integer_value)
integer_value = 2
Int64

As we noted above, Julia will only perform “free” conversions of type if it can do so without losing precision. If we try to put a non-integer into this variable:

integer_value = 2.5
InexactError: Int64(2.5)

Stacktrace:
 [1] Int64
   @ ./float.jl:900 [inlined]
 [2] convert(#unused#::Type{Int64}, x::Float64)
   @ Base ./number.jl:7
 [3] top-level scope
   @ In[17]:1

Julia’s types are arranged in a “hierarchy”, from more general to more specific types:

The Julia Type Hierarchy, thanks to Uwe Hernandez Acosta

Only the “leaves” of the type tree can be values (“concrete types”) - but variables (and functions) can be defined in terms of any of the types, even the “abstract” ones. This means you can, for example, define a variable that will hold any “Number” type - allowing it to hold Float, Rational or Integer values, but not, say, Strings.

We’ll come back to other aspects of this when we cover multiple dispatch.

Complex and Rational Numbers#

Complex and rational numbers are handled natively in Julia:

# "im" is the imaginary number constant
m = 1 + 2im
n = -3 - 3im
m*n
3 - 9im
# // defines a rational
r1 = 2//3
r2 = 1//4
r1*r2
1//6

Strings#

Strings in Julia are defined with double quotes:

"oh yes, they are"

or triple double quotes, which allow use of unescaped double quotes and newlines:

"""Sam exclaimed, "this is much easier than using backslashes!" """

"here is a string"
"here is a string"

One point to note is that strings are concatenated with a * operator, because concatenation is not commutative:

"hello " * "world"
"hello world"

Strings are internally represented as UTF-8, so they can hold any Unicode values. However, they are indexed by byte, and attempting to take values from the middle of a code-point will cause an error. There are methods which provide iterators to safely iterate over the individual “characters” if you need to.

Characters are defined with single quotes: 'प', and are UTF-32 (32-bit values).

текст = """The Ukrainian for "text" is "текст" and the Hindi is "पाठ"!"""

текст[38] == 'т'  #trying to take from position 37 would cause an error, as Cyrillic chars are two-bytes wide.
true

Finally, Julia supports string interpolation using $ to specify the variable, or expression if contained within parentheses, to substitute into the string.

"$m squared is $(m^2)"
"1 + 2im squared is -3 + 4im"

Arrays, Dicts and other Composite Types#

Julia supports Array, Dict, Tuple and other “container types” you may recognise from Python, and the syntax is similar for 1-dimensional examples.

Arrays are rich enough that they deserve their own notebook, which we’ll come to afterward.

Dictionaries#

We can make Dictionaries (for C++ programmers - HashMaps or AssociativeArrays) with the Dict constructor. Unlike Python, the mapping between a key and a value is denoted with a =>.

farmyard_sounds = Dict("🐮" => "moo", "sheep" => "baa", "pig" => "oink", "farmer" => "get off my land!")

@show farmyard_sounds["🐮"]

@show haskey(farmyard_sounds,"pig")

@show farmyard_sounds["🐱"] = "Meow" ;

farmyard_sounds
farmyard_sounds["🐮"] = "moo"
haskey(farmyard_sounds, "pig") = true
farmyard_sounds["🐱"] = "Meow" = "Meow"
Dict{String, String} with 5 entries:
  "farmer" => "get off my land!"
  "🐱"     => "Meow"
  "pig"    => "oink"
  "🐮"     => "moo"
  "sheep"  => "baa"

(Tuples and Sets and other container types work similarly to how you would expect in Python, so we won’t cover them explicitly)

Custom Composite Types#

You can also create composite types, structs - here you need to specify the types of their sub-fields. As with many Julia constructs, a struct definition starts with a keyword, struct, and ends with end:

struct my_vector
    x::Float64
    y::Float64
end

υϵκτωρ = my_vector(3.7, 6e7)
my_vector(3.7, 6.0e7)

By default, struct types are immutable - you can’t update their elements - but you can explicitly make mutable ones. (You can also, as in C++, make “templated” structs, defining the types of the elements via a placeholder - which is also how Arrays work internally.)

mutable struct my_mut_vector
    x::Float64
    y::Float64
end

μ_υϵκτωρ = my_mut_vector(3.7, 6e7)

μ_υϵκτωρ.x = 6.0

μ_υϵκτωρ
my_mut_vector(6.0, 6.0e7)