Variables#

Learning Objectives#

  • Understand how to create and assign variables in Julia

  • Recognise and use basic data types in Julia, including integers, floating-point numbers, strings and booleans

  • Gain awareness of container types that Julia offers

  • Utilise Julia’s dynamic typing system for variable assignment and manipulation

  • Understand the basics of abstract types and type comparison

  • Understand the significance of nothing

  • Explain what the Any type is

Overview#

Variables in Julia are created simply by assigning to them with the = sign. When you assign a value to a new variable name, Julia creates the variable if it does not exist and binds it to that value. Julia is dynamically typed, meaning that a variable can be rebound to values of any type at runtime - types stick to values, not names. This is in contrast to statically typed languages, such as C, C++, Java, etc., where the type of a variable cannot change once the variable has been declared.

# Assign an integer value to x and print it
x = 10 
println("x is ", x)

# Reassign x to a string value and print it
x = "Julia"
println("Now x is ", x)
x is 10
Now x is Julia

Here, x first was an integer, then became a string. You can always check the type of a value using the typeof() function:

# Print type of a variable
y = 3.14 
println(typeof(y)) # This will print the type of y, e.g. Float64
Float64

Exercise: Basic Variable Assignment#

Copy the code below into a REPL (or notebook) and run it with different values for x. Notice what type is returned. Some examples you could try:

  • A floating point number (e.g. 3.14)

  • A boolean (true or false)

  • A array (e.g. [1, 2, 3])

# TODO: Try different values
x = 

# Print the value and type of x
println("x is ", x)
println("The type of x is: ", typeof(x))
# Answer here

Extension exercise: dynamic typing#

Reflect (or do some research) on what might be advantages and possible pitfalls of the dynamically typed nature of Julia.

Basic Types in Julia#

Julia has several built-in data types for basic values:

  • Integers (Int8, Int16, Int32, Int64, Int128, Int) - represent whole numbers (… -2, -1, 0, 1, 2, …) that can be represented by the specified number of bits (8, 16, 32, …). The Int type is an alias for the integer type with number of bits naturally supported with the system architecture. Most systems are 64-bit (so Int64), which can hold very large integers. Literal integer declarations are for type Int, i.e. x = 42 here would have typeof(x) being Int).

  • Floating-point numbers (Float16, Float32, Float64) - represent floating point numbers comprising the given number of bits. Literal floating point declarations (e.g. x = 3.14) are 64-bit double precisions (Float64).

  • Strings (String) - represent text data. Julia strings are contained in double quotes, e.g. name = "Julia". They are UTF-8 encoded and can handle Unicode characters.

  • Booleans (Bool) - represent logical true/false values. There are exactly two: true and false (all lowercase). They are often the results of comparisons or logical operations.

  • Characters (Char) - represent a single Unicode code point. Written in single quotes.

  • Complex numbers (Complex{T}) - Numbers of the form a + b·im where a and b are of type T. E.g. a and b are Float64, the type is Complex{Float64}.

  • Rational numbers (Rational{T}) - Exact fractions of integers of type T e.g. Rational{Int32}.

  • Arbitrary-precision integers (BigInt) - Integers of unlimited size.

  • Arbitrary-precision floats (BigFloat) - Floating-point numbers with user-controllable precision (default ~256 bits).

  • Unsigned integers (UInt8, UInt16, UInt32, UInt64, UInt128, UInt) - Non-negative whole numbers represented by the specified number of bits. You can pick a size (e.g. UInt8 for 0–255) or use UInt (platform-sized, typically UInt64).

For example, consider this code, which creates one variable of each type and then prints its value and type:

x_int = 100                      # Int64 on 64 bit machines
x_int16 = Int16(-3000)           # Int16
y_float = 100.05                 # Float64
y_float32 = Float32(99.3)        # Float32 (equivalently: 99.3f)
name_str = "Julia🧡"             # String
flag_bool = true                 # Bool
c_char = 'A'                     # Char
z_complex = 3 + 4im              # Complex{Int}
z_complex_f64 = 3.0 + 4.5im      # ComplexF64 (alias for Complex{Float64})
rational_val = 3//4              # Rational{Int}
big_int = BigInt(2)^100          # BigInt

setprecision(256)                # ensure BigFloat precision
big_float = BigFloat("2.71828")  # BigFloat

u8 = UInt8(255)                  # UInt8
u_def = UInt(100)                # UInt64 on 64-bit machines

println("x_int:         ", x_int,         "  (", typeof(x_int),         ")")
println("x_int16:       ", x_int16,       "  (", typeof(x_int16),       ")")
println("y_float:       ", y_float,       "  (", typeof(y_float),       ")")
println("y_float32:     ", y_float32,     "  (", typeof(y_float32),     ")")
println("name_str:      ", name_str,      "  (", typeof(name_str),      ")")
println("flag_bool:     ", flag_bool,     "  (", typeof(flag_bool),     ")")
println("c_char:        ", c_char,        "  (", typeof(c_char),        ")")
println("z_complex:     ", z_complex,     "  (", typeof(z_complex),     ")")
println("z_complex_f64: ", z_complex_f64, "  (", typeof(z_complex_f64), ")")
println("rational_val:  ", rational_val,  "  (", typeof(rational_val),  ")")
println("big_int:       ", big_int,       "  (", typeof(big_int),       ")")
println("big_float:     ", big_float,     "  (", typeof(big_float),     ")")
println("u8:            ", u8,            "  (", typeof(u8),            ")")
println("u_def:         ", u_def,         "  (", typeof(u_def),         ")")
x_int:         100  (Int64)
x_int16:       -3000  (Int16)
y_float:       100.05  (Float64)
y_float32:     99.3  (Float32)
name_str:      Julia🧡  (String)
flag_bool:     true  (Bool)
c_char:        A  (Char)
z_complex:     3 + 4im  (Complex{Int64})
z_complex_f64: 3.0 + 4.5im  (ComplexF64)
rational_val:  3//4  (Rational{Int64})
big_int:       1267650600228229401496703205376  (BigInt)
big_float:     2.718279999999999999999999999999999999999999999999999999999999999999999999999989  (BigFloat)
u8:            255  (UInt8)
u_def:         100  (UInt64)

Min/max values for integer types#

The typemin and typemax functions return the minimum and maximum values for different integers types. In general the range of possible values for integers with the number of bits \(b\) is

  • Unsigned: \(0 \leq x \leq 2^b - 1\)

  • Signed: \(-2^{b-1} \leq x \leq 2^{b-1} - 1\)

println("Signed ints:")
println("Int8: ", "$(typemin(Int8)) <= x <= $(typemax(Int8))")
println("Int16: ", "$(typemin(Int16)) <= x <= $(typemax(Int16))")
println("Int32: ", "$(typemin(Int32)) <= x <= $(typemax(Int32))")
println("Int64: ", "$(typemin(Int64)) <= x <= $(typemax(Int64))")
println("Int128: ", "$(typemin(Int128)) <= x <= $(typemax(Int128))")
println("\nUnsigned ints:")
println("UInt8: ", "$(typemin(UInt8)) <= x <= $(typemax(UInt8))")
println("UInt16: ", "$(typemin(UInt16)) <= x <= $(typemax(UInt16))")
println("UInt32: ", "$(typemin(UInt32)) <= x <= $(typemax(UInt32))")
println("UInt64: ", "$(typemin(UInt64)) <= x <= $(typemax(UInt64))")
println("UInt128: ", "$(typemin(UInt128)) <= x <= $(typemax(UInt128))")
Signed ints:
Int8: -128 <= x <= 127
Int16: -32768 <= x <= 32767
Int32: -2147483648 <= x <= 2147483647
Int64: -9223372036854775808 <= x <= 9223372036854775807
Int128: -170141183460469231731687303715884105728 <= x <= 170141183460469231731687303715884105727

Unsigned ints:
UInt8: 0 <= x <= 255
UInt16: 0 <= x <= 65535
UInt32: 0 <= x <= 4294967295
UInt64: 0 <= x <= 18446744073709551615
UInt128: 0 <= x <= 340282366920938463463374607431768211455

More information on integers and floating-points#

There are several topics that get more into the nitty-gritty detail of working with integers and floating point numbers of different precisions. Check out the Julia manual page (https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/) for more information, including details such as:

  • Inf and NaN for floating point numbers

  • Integer wraparound (e.g. what is Int8(127) + Int8(1))

  • How to specify numerical literals for e.g. Int16, Float32, …

Container Types in Julia#

Julia provides several built-in container types for grouping values, including:

  • Tuples (Tuple) - Immutable, ordered collections of heterogenous values.

  • NamedTuples (NamedTuples) - Like tuples but with named fields for more readable access.

  • Arrays (Array{T, N}) - Mutable, indexed collections of elements all of the same type T, in N dimensions (more later).

  • Dictionaries (Dict{K,V}) - Mutable key-values maps, with keys of type K and values of type V.

  • Sets (Set{T}) - Mutable collections of unique values of type T.

In a similar manner as for the basic types in Julia:

tup = (42, "Julia", 3.14)
nt = (x = 10, y = 20)
v = [1, 2, 3]
m = [1 2; 3 4]
d = Dict("a" => 1, "b" => 2)
s = Set(["apple", "banana", "apple"])

println("Tuple:      ", tup,    "  (", typeof(tup),    ")")
println("NamedTuple: ", nt,     "  (", typeof(nt),     ")")
println("Vector:     ", v,      "  (", typeof(v),      ")")
println("Matrix:     ", m,      "  (", typeof(m),      ")")
println("Dict:       ", d,      "  (", typeof(d),      ")")
println("Set:        ", s,      "  (", typeof(s),      ")")
Tuple:      (42, "Julia", 3.14)  (Tuple{Int64, String, Float64})
NamedTuple: (x = 10, y = 20)  (@NamedTuple{x::Int64, y::Int64})
Vector:     [1, 2, 3]  (Vector{Int64})
Matrix:     [1 2; 3 4]  (Matrix{Int64})
Dict:       Dict("b" => 2, "a" => 1)  (Dict{String, Int64})
Set:        Set(["banana", "apple"])  (Set{String})

Explicit Type Conversion#

We can convert between types using the convert function:

# Note: use the @show macro only for more detailed printing

# Float32 --> Float64
@show convert(Float64, Float32(1.23))

# Rational --> Float32
@show convert(Float32, 1 // 3)

# UInt16 --> Int64
@show convert(Int64, UInt16(1000))
convert(Float64, Float32(1.23)) = 1.2300000190734863
convert(Float32, 1 // 3) = 0.33333334f0
convert(Int64, UInt16(1000)) = 1000
1000

Note that if the target type is an Integer type, an InexactError will be raised if the value is not representable by the type, e.g. for example if x is not integer-valued, or is outside the range supported by T.

# Error: not integer-valued
convert(Int, 1.1)
InexactError: Int64(1.1)

Stacktrace:
 [1] Int64
   @ .\float.jl:912 [inlined]
 [2] convert(::Type{Int64}, x::Float64)
   @ Base .\number.jl:7
 [3] top-level scope
   @ In[8]:2

(In this case, we probably want to instead take the nearest integer to the floating point value, which we can do using the trunc function instead.)

# Truncate to nearest integer
trunc(Int, 1.1)
1
# Error: out of range
convert(Int16, 2^15 + 1)
InexactError: trunc(Int16, 32769)

Stacktrace:
 [1] throw_inexacterror(f::Symbol, ::Type{Int16}, val::Int64)
   @ Core .\boot.jl:634
 [2] checked_trunc_sint
   @ .\boot.jl:656 [inlined]
 [3] toInt16
   @ .\boot.jl:682 [inlined]
 [4] Int16
   @ .\boot.jl:782 [inlined]
 [5] convert(::Type{Int16}, x::Int64)
   @ Base .\number.jl:7
 [6] top-level scope
   @ In[10]:2

String representation#

We can use the string function to represent an object as a string:

# Show string representations
@show string(1 / 3)
@show string(BigInt(2)^100)  # 2^100 needs a BigInt for calculating
@show string('A')
string(1 / 3) = "0.3333333333333333"
string(BigInt(2) ^ 100) = "1267650600228229401496703205376"
string('A') = "A"
"A"

We can also use string interpolation to put the values of expressions into strings via $(...) (which we’ve already been using above):

# Show result of expression with string interpolation
"The value of 1 + 2 is $(1 + 2)"
"The value of 1 + 2 is 3"

Exercise: Exploring Variables#

Try creating a few different variables on your own. For example:

  • Create a variable city and assign a city name to it (as a string).

  • Create a variable temperature and assign a number (integer or float).

  • Use convert to convert your temperature to a different type.

  • Use println to output a sentence like “The temperature in <CITY> is <TEMPERATURE>”.

  • Check the types of your variable with typeof() to confirm they match your expectations.

# Answer here

Nothing (Nothing)#

Like other high-level languages, Julia has a ‘null’ object called nothing, typically used to indicate the absence of a value (similar to null or None in other languages). nothing has the type Nothing; in fact, it is the only value that has type Nothing:

# Type of `nothing`
typeof(nothing)
Nothing

It can be checked using the isnothing function:

# Checking whether something is nothing... :/
x = nothing
isnothing(x)
true

Optional type annotations#

You can optonally declare the type of value for a new variable explicitly. Because Julia infers types for you, you rarely need to do this, but in some situations this can lead to better performance (mostly when used with global variables).

# Assign to String variable `name`
name::String = "Joe Bloggs"
"Joe Bloggs"

Note that if you do this, it is no longer possible to assign a value of a different type to the variable (unless the new value can be implicitly converted to variable’s declared type):

# Error -- cannot convert 1.0 to a string
name = 1.0
MethodError: Cannot `convert` an object of type Float64 to an object of type String

Closest candidates are:
  convert(::Type{String}, ::Base.JuliaSyntax.Kind)
   @ Base C:\workdir\base\JuliaSyntax\src\kinds.jl:975
  convert(::Type{String}, ::String)
   @ Base essentials.jl:321
  convert(::Type{T}, ::T) where T<:AbstractString
   @ Base strings\basic.jl:231
  ...


Stacktrace:
 [1] top-level scope
   @ In[17]:2

Furthermore, we’re not even allowed to redeclare the variable to be of a different type:

# Error -- cannot redeclare a variable with fixed type
name::Float64 = 1.0
cannot set type for global Main.name. It already has a value or is already set to a different type.

Stacktrace:
 [1] top-level scope
   @ In[18]:2

If we want to indicate that a variable always has the same value, we can use the const keyword. Attempts to change the value of such a variable will raise a warning (though not an error) if the new value has exactly the same type, or an error if the type is different (even if it could in principle be converted):

# Declare Avagadro's constant: 6.02214076 * 10^23 atoms per mole
const AVAGADRO_CONSTANT = 6.02214076e23

# Reassignment
AVAGADRO_CONSTANT = 99.0
WARNING: redefinition of constant Main.AVAGADRO_CONSTANT. This may fail, cause incorrect answers, or produce other errors.
99.0
# Reassignment with different type (Int)
AVAGADRO_CONSTANT = 1
invalid redefinition of constant Main.AVAGADRO_CONSTANT

Stacktrace:
 [1] top-level scope
   @ In[20]:2

Exercise: type declarations#

Which of the following assignments do you think will work? Test out your predictions by running the code.

  • a_float::Float64 = 1.0

  • another_float::Float64 = 2

  • a_string::String = "a"

  • another_string::String = 'a'

# Answer here

Abstract Types#

Julia’s type hierarchy includes the concept of abstract types. Unlike the normal, ‘concrete’ types we’ve seen so far, abstract types cannot be instantiated. Instead, they are used to define a set of related concrete types that share some common aspects. A concrete type may thus be a subtype of an abstract type.

Here are some examples:

  • Integer — all integer types (subtypes include Int64, Int32, Int16, UInt64, etc.)

  • AbstractFloat — all floating-point types (subtypes include Float64, Float32, Float16)

  • Real — all real numbers (includes Rational and any subtypes of Integer and AbstractFloat)

  • Number — the root of all numeric types (integers, floating point numbers, complex numbers, …)

Note that abstract types can also be subtypes of other abstract types. For example, Integer is a subtype of Real, which in turn is a subtype of Number.

The operator <: may be used to check whether a type is a subtype of an abstract type:

println("Int is a subtype of Integer: ", Int <: Integer)
println("UInt8 is a subtype of Integer: ", UInt8 <: Integer)
println("Float64 is a subtype of Integer: ", Float64 <: Integer)
println("AbstractFloat is a subtype of Real: ", AbstractFloat <: Real)
println("ComplexF64 is a subtype of Real: ", ComplexF64 <: Real)
println("ComplexF64 is a subtype of Number: ", ComplexF64 <: Number)
Int is a subtype of Integer: true
UInt8 is a subtype of Integer: true
Float64 is a subtype of Integer: false
AbstractFloat is a subtype of Real: true
ComplexF64 is a subtype of Real: false
ComplexF64 is a subtype of Number: true

Similarly, the function isa can be used to check whether an object x is of some type T (whether T is a concrete type or abstract type). This function also supports infix notation:

# Check whether `x` is an object of type `T`
isa(x, T)

# Equivalently:
x isa T
x = 1.0

# Check if a Float64
println("1.0 is a Float64: ", x isa Float64)

# Check if a Int
println("1.0 is a Int: ", x isa Int)

# Check if a Real
println("1.0 is a Real: ", x isa Real)
1.0 is a Float64: true
1.0 is a Int: false
1.0 is a Real: true

Something to be aware of is that, in Julia, concrete types cannot be subtypes of other concrete types. This is quite different to other languages, e.g. Python or C++, where we can have classes subclassing other classes. For example:

# Float32 is not a type of Float64
Float32 <: Float64
false

The Any type#

There is an important abstract type, called Any. It is the abstract type where all other types are a subtype of it. Put another way, any objects you create a instances of Any.

Exercise: types#

Write code to show that:

  • "bob", true, 1 + 4im are all instances of Any

  • String is not a subtype of Number

  • Char is not a subtype of String (two concrete types)

  • nothing is an instance of Any

# Answer here

Summary on abstract types#

Abstract types allow us to view types as part of a hierarchy, which is useful for organising our code. We’ll also see later that they feed into Julia’s multiple dispatch system. We won’t talk too much about Julia’s type system in this course, but it’s useful to be aware of the notion of an abstract type. To learn much more about Julia’s type system, check out the manual page on types in the Julia docs.

End of Section Quiz#

Given the assignment `x = 1.4`, what happens if you reassign a new value to `x` of a different type than `Float64`?

Considering that Julia’s default integer type depends on system architecture, what is the default data type for an integer value assigned to a variable on a typical 64-bit system?

Which of the following code snippets will output Float64 in Julia?