Introductory Tour of Julia

Author

Darren Irwin

On this page we’ll take an introductory tour of Julia. This will touch on a variety of topics, providing just enough detail to get you going. (We’ll go more in-depth on various topics later. You can also learn more from the Julia Manual.)

Expressions

Now that we have Julia running (on the last page), we can learn how to enter expressions that Julia will evaluate.

The simplest expressions are just a single number or string of text, e.g. try entering something like:

5
5

After you type 5 at the julia> prompt and press enter, Julia responds with 5, the returned value of your expression.

You try a more complex expression:

7 * (6 - 1)
35
NoteValid vs. invalid expressions

If you enter something that is not interpretable by Julia, the REPL will respond indicating an error. But don’t worry! This is how we learn, and the REPL is still usable. You can just try again by modifying your expression using your growing knowledge of Julia syntax.

Arithmetic operators

Play around with other simple mathematical expressions. These are some of the main arithmetic operators, which you are likely familiar with: + , -, *, /, ^. Try using parentheses to control the order of operations (for example: compare the returned values of 3 + 5 / 2^3 versus (3 + 5) / 2^3).

One that operator that you might be less familiar with is the remainder operator % which gives the remainder of a division. Try it out:

14 % 5
4

Another is the ÷ operator, which produces just the integer part of a division (without the decimal part). You aren’t likely to use ÷ very often, but I include it here as an example of Julia’s use of unicode characters (more on this below). To type that character at the Julia prompt, enter \div followed by the tab key.

14 ÷ 5
2

Functions

We can also use some functions in our expressions. A function is something that does a specific thing defined by code already loaded into memory. You can recognize a function in Julia by the fact that it will be a string of text followed immediately by parentheses, perhaps with inputs to the function inside the parentheses. Here we use the sqrt() function, which produces a square root of the number in the parentheses.

sqrt(16)
4.0

Getting help

To look up information about a command in Julia, you can enter the help mode by typing ? at the Julia prompt. The prompt then changes to help?>, and you can type the name of a function or operator and then enter. Julia will then tell you some helpful info. Try this for the + operator, for instance.

Naming and assigning values to objects

Each of the above expressions returns a value that we can give a name and store in memory, using the equal sign =, the assignment operator. For example:

x = 2sqrt(9)
6.0

The above command does several interesting things:

  • It evaluates the expression on the right side of the equal sign.
  • It checks whether there is already an object in memory with the name given on the left side of the equal sign (in this case, x). If not, it creates that object.
  • It assigns the value produced by the right side to the object named by the left side.

We can then use that named object’s value in subsequent expressions:

4x
24.0

Assigning values to named objects allows us to save values in memory and then combine them in our calcuations:

pop1 = 50; pop2 = 200; totalPop = pop1 + pop2
250

The semicolons above are used to separate distinct expressions that Julia should evaluate before continuing to the next. This is called a compound expresssion and allows you to enter several commands into the REPL at one time. Another way to enter a compound expression is with a begin-end block:

begin
  x = 17
  y = x^2
end
289
TipCalculate the area of a circular forest patch

Pick a number for the radius (in km) of a circle, and assign that to an object name and store that in Julia’s memory. Then in a separate command, use that stored object to calculate the area of the circle (the standard formula for this is π\(r^{2}\); to write the exponent of ‘2’ though, you’ll need to use ‘^2’). A neat thing is that Julia already knows the value of π; to get it, just type pi or alternatively \pi then tab.

Different Types of numbers

When computers store numbers, they can do it in different ways, each with their advantages and disadvantages. Two main ways that Julia can store numbers are as integers or as floating-point numbers. Integers are exact but are limited in the values they can take, whereas floating-point numbers can represent more values but are not exact.

We can learn about the type that Julia uses to store values by using the typeof() function:

typeof(3)
Int64

Julia responds by saying that the value 3 is of the Int64 type. This means that it is an integer stored in memory using 64 bits. (No worries if you don’t know what this means.) Now compare to this:

typeof(3.0)
Float64

Julia responds by sayng 3.0 is of the Float64 type. This means a 64-bit floating-point number. By writing the .0 after the 3 we have told Julia we want this number treated as a floating-point number.

Much of the time, we don’t need to think about object types, because Julia is smart and handles types through something called dynamic typing. Here’s an example:

a = 2
b = 3.0
c = a + b
typeof(c)
Float64

Above, Julia figures that by entering b = 3.0 you are indicating that you want b treated as a floating-point number, so when it adds the integer 2 to this, it returns a floating-point number.

Julia has all sort of other useful Types of numbers. One that I will point out here is the Rational number type, which you can construct with the // operator and do exact calculations using ratios:

ratio1 = 1//3
ratio2 = 5//7
product = ratio1 * ratio2
5//21

As your Julia skills increase, there are large benefits to being somewhat aware of types; this can help you write efficient programs and help with debugging your code. You can even define your own object types.

Ranges

Julia has a neat data structure (i.e. a type) to store arithmetic series of numbers:

rangeOfNums = 1:100
println(rangeOfNums)
typeof(rangeOfNums)
1:100
UnitRange{Int64}

The notation 1:100 means all the integers between 1 and 100. Julia stores this range efficiently, until the actual numbers are needed. If we want to actually convert it to the numbers themselves, we could enter something like collect(rangeOfNums)

If we want only the even numbers, we enter something like this:

collect(0:2:10)   # the '2' is the increment between successive values
6-element Vector{Int64}:
  0
  2
  4
  6
  8
 10

Note the use of the # symbol above to make comments to the right of code.

TipCreate a descending vector

You have a population of 30 individuals that is decreasing by 3 individuals in each time step. Use the collect() function to create a list of numbers (a.k.a. a vector) representing the population sizes at each time step (until the population goes extinct).

Characters and Strings

Programming in biology often involves manipulating text such as “ACGT”. For this, we can use two other data types in Julia: characters (officially Char) and strings (officially String), which are usually made up of a series of characters (imagine beads of letters on a string, ha ha—this might be especially understood by Taylor Swift fans).

We enter characters with single quotes, and can combine them to make strings:

nucleotide1 = 'T'
nucleotide2 = 'C'
dinucleotide = nucleotide1 * nucleotide2
"TC"

Above, we created two objects of type Char and combined them (yes, Julia views combining characters or strings as a form of multiplication, ha ha) to produce a String.

TipExponents on Strings

Given that Julia uses the * symbol (i.e., multiplication) for combining two characters or strings into a longer string, what do you think it might use the ^ symbol for, when applied to a character or string? Make a guess, and then play around and find out. (If you are stuck, try entering 'T'^2 and think about what the REPL returns)

When you enter a String into Julia, you need to use double quotes (unlike a Char with single quotes).

oligo1 = "ACGCAT"
oligo2 = "CCCTG"
ligation = string(oligo1, oligo2)
"ACGCATCCCTG"

The function string above concatenates different strings together.

TipYour favourite species

Assign a few of your favorite species to different object names, and then combine them into a single string, with commas separating their names.

Unicode characters

One fun thing about Julia is that you can use Unicode characters in object names, strings, function names, and some operators. The motivator for this is to make code read more like humans tend to write mathematics. For example:

χ² = 30.4
β = 2χ²
60.8

To make the Unicode symbols above, you would type \chi tab \^2 tab, and on the next line \beta tab. Notice also that the expression 2χ² is evaluated the same as 2*χ² or 2 * χ².

Unicode also allows some fun:

TipPlant biology using Unicode

Assign a variable a the value of a Char given by typing \:seedling: tab. (Remember to use single quotes around the seedling symbol.) Assign a variable b the value of a Char given by typing \:deciduous_tree: tab. Then execute this line:

println(a, " grows into ", b)

We can also use Unicode symbols in variable names:

🐬 = 47    # write with: \:dolphin: tab
🐳 = 5     # write with: \:whale: tab
totalMarineMammals = 🐬 + 🐳
52

You can find how to write a whole bunch of Unicode symbols by clicking here

Comparisons and Booleans

In programming it is often important to check if certain conditions are true or false. These values are called Booleans (of type Bool in Julia).

These comparison operators are used to compare two values (place to left and right of the operator), resulting in true or false:
== : equal? (notice the two equal symbols)
!= : not equal? (or try a uncode symbol by typing \neq then tab: ≠)
< : less than?
<= : less than or equal?
> : greater than?
>= : greater than or equal?

Try a bunch of comparisons of values using the above. These are called Boolean expressions. For example:

sqrt(9) == 3
true
x = 8; sqrt(x) >= 3
false

We can use Boolean operators to combine multiple comparisons. These are used as follows, where x and y are Boolean expressions:
!x : not x
x && y : x and y
x || y : x or y

For example:

5 <= 3 && 7 == 14/2
false
5 <= 3 || 7 == 14/2
true
!(5 <= 3 || 7 == 14/2)
false
TipSelect the extremes

The expression rand() gets a random number between 0 and 1. Write a series of commands that picks such a random number, and then returns true if that number is either above 0.75 or below 0.25, and false otherwise.

(If you solve the above and have time, can you figure out a way to chain your Boolean expressions into a form similar to value1 < x < value2 ?)

Collections, e.g. Arrays

Objects can contain more than a single item in them. A general term for such a data structure is a collection. An array is a common type of collection used in Julia: It can be thought of as an n-dimensional lattice of boxes, where you can store something in each box. Below we create some kinds of arrays:

arrayA = [7.3, 3, 11.2, -5, 3.2]
5-element Vector{Float64}:
  7.3
  3.0
 11.2
 -5.0
  3.2
arrayB = [6 5 4; 3 2 1]
2×3 Matrix{Int64}:
 6  5  4
 3  2  1

Because these data structures are more complex than the simple ones we’ve looked at so far, Julia tells you the type of the returned value, before showing you the actual values.

Note that a Vector is another name (an alias) for a 1-dimensional array, and a Matrix is another name for a 2-dimensional array. Inside the curly brackets, Julia indicates the type that each element of the array belongs to (this is Float64 in the first case, and Int64 in the second).

We can ask Julia to return the values in parts of an array by indexing into the array. For example:

arrayA[3]
11.2
arrayA[end]
3.2

What do you think using begin as an index would do? Try it!

arrayB[2, 2:3]
2-element Vector{Int64}:
 2
 1

Above, we took a slice of arrayB, consisting of row 2 and columns 2-3.

We can even use such indexing to change the value stored one of the “boxes” in an array:

arrayB[2, 2] = -129
arrayB
2×3 Matrix{Int64}:
 6     5  4
 3  -129  1

We’re going to eventually learn a lot more about arrays, as they are super useful in data science, bioinformatics, and simulations. There are many Julia functions than manipulate arrays. Here we’ll mention two of them that are useful for working with vectors (1-dimensional arrays) when you are starting out:

push!() and pop!()

These two functions are used to add (push) an element to the end of a vector, or remove (pop) the last element from a vector:

vecA = ["one", "two", "three"]
push!(vecA, "four")
4-element Vector{String}:
 "one"
 "two"
 "three"
 "four"

We added an element to the end. Now we can remove it:

pop!(vecA)
"four"

That last one returns the last element of vecA, and removes it from vecA—we can verify this by asking the REPL to show what is in vecA:

vecA
3-element Vector{String}:
 "one"
 "two"
 "three"

The ! in these function names conveys that these functions change an object that is being passed to the function (in this case, vecA). Other useful and well-named functions that change vectors include insert!(), delete!(), append!(), empty!(), and many others.

Tuples

Another kind of collection is called a tuple. It is a lot like a vector (a 1-dimensional array) but is immutable. This means that once defined, you cannot change specific values stored in a particular tuple. We create them using parentheses and commas:

myTuple = (1.3, "RNA", -12, '4')
typeof(myTuple)
Tuple{Float64, String, Int64, Char}

The response tells us that the object we created is of Tuple type, and it tells us the types of each element in the tuple.

We can also create named tuples in which we give names to the values:

nt = (base = 'C', position = 35, chrom = "Z")
(base = 'C', position = 35, chrom = "Z")

We can get the value of a certain element by using that name, like this:

nt.chrom
"Z"

The above is one of the uses of the “.” symbol, to access a named element within a tuple. (Another use, quite different, is described after the next heading below).

Tuples are useful for storing and calling connected pieces of info. Because they are not mutable, Julia can store and use them in a more efficient way than arrays. They can also be used to feed data into functions (we’ll explore this later).

Dictionaries

Yet another useful kind of collection is a dictionary, which in Julia is of type Dict. These can be thought of as lookup tables, which match up keys and values into key-value pairs. For example, let’s say you wanted to setup a way for a single letter to represent a species name:

lookup = Dict('h'=>"human", 'f'=>"fruit fly", 's'=>"sunflower")
Dict{Char, String} with 3 entries:
  'f' => "fruit fly"
  'h' => "human"
  's' => "sunflower"

Now, we can get a specific values using a specific key, almost in the same way you would use indexing to access one element in an array:

lookup['s']
"sunflower"

There are many ways to modify and work with dictionaries. For now, we’ll move on to other topics in this introductory tour.

Broadcasting

We often want to apply an operation or function to each element of an array. In Julia this is called “broadcasting” and is accomplished by the humble “dot” operator: .

We can put this dot in front of any arithmetic operator to make the operator apply to each element of a collection:

arrayB.^2   # square each element of arrayB
2×3 Matrix{Int64}:
 36     25  16
  9  16641   1
3 .* [5, -1, 3]
3-element Vector{Int64}:
 15
 -3
  9

We can also put this dot right after a function to have the function apply to each element in a collection:

sqrt.([64 25 36])
1×3 Matrix{Float64}:
 8.0  5.0  6.0

Macros

Macros are a kind of function that takes text as input and converts it to code that is then evaluated. They are a somewhat advanced topic in terms of developing a full undertanding. For now, I just want to mention them because some are hugely useful even when starting out. As an example, if you are doing a lot of “dot” operations, you can use the @. macro to convert all the operators in a line to dot operators:

begin
  data = [3 7 -4 9]
  results = @. 2data + data^2
end
1×4 Matrix{Int64}:
 15  63  8  99

The line above with the @. macro produces the same output as:

results = 2 .* data .+ data.^2
1×4 Matrix{Int64}:
 15  63  8  99

Another useful macro is @time :

@time sum(rand(1000))
  0.000007 seconds (3 allocations: 8.062 KiB)
485.05211064953676

The rand(x) function produces x random numbers between 0 and 1. The above returns the sum of 1000 such numbers. Julia tells you the time it took, followed by the sum.

An aside: If you are curious about how long the same thing takes in R, you can run this code in R:

startTime <- Sys.time()
sum(runif(1000))
endTime <- Sys.time()
print(endTime - startTime)
TipEven more numbers!

Modify the last Julia expression above so that it takes the square of each of one million random numbers between 0 and 1, and then adds them up.
(Hint: pay attention to the section on Broadcasting above, especially about the dot operator.)

Defining your own functions

Julia facilitates the writing of your own functions. This can have huge benefits in terms of organizing your programs and making them efficient. Here’s an example:

sumSqrtRands(n) = sum(sqrt.(rand(n)))
sumSqrtRands (generic function with 1 method)

We’ve now defined a function that generates n random numbers, takes the square root of each, and then adds those up. Now we can use it for the sum of one million such numbers (in Julia we can write big numbers with underscores separating integers, making them easier to read):

@time sumSqrtRands(1_000_000)
  0.007976 seconds (6 allocations: 15.313 MiB)
666858.0003782036

Below demonstrates another way to write functions, in this case with two arguments (things you pass into the function, called parameters inside the function):

function xSquaredPlusY(x, y)
  x^2 + y
end
xSquaredPlusY (generic function with 1 method)

Try calling that function a few times, with different values of x and y, e.g.:

xSquaredPlusY(3, -2)
7
TipApply your function to many elements at once

Can you figure out how to call the above function in a way where you can input a series of numbers as the first argument (for example, x = [1, 3, 7]), and -2 as the second argument? (Hint: if you get an error, reviewing the Broadcasting section above might help)

Packages

Thousands of people around the world have contributed over 10,000 packages that extend functionality of Julia. Installing these packages is easy.

First, at the julia> prompt in the REPL, typing the right square bracket symbol ] to activate the package mode. You will then see the julia> prompt change into a prompt that looks something like (@v1.12) pkg> . Then, type a command to add a package, e.g.:

add Plots

This tells Julia to download and install the officially registered package with that name. The Plots package is a big one, so this can take some time.

To get out of package mode, press the “delete” key and this will return you to the normal Julia REPL mode.

To actually use the Plots package, we need to load it into the memory for this Julia session. To do that, simply write:

using Plots

Now, let’s use a function called plot() that is included in this Plots package. Try:

x = -5:5
y = x .^ 2
plot(x, y)

This will likely open another window on your computer, and show a plot. (If not, don’t worry; we’re going to set up another good way to plot in the next page.)

If the above worked, you can see another way to run the plot() function here:

f(x) = x^2
plot(f)

In our first use of the plot() function above, we passed in two argument, both vectors of numbers. In the second, we passed in just a mathematical function, and plot() figured out a good way to show us the mathematical relationship represented by that function.

This flexibility in function calls is an example of multiple dispatch, a key feature of Julia. We’ll come back to this topic in the future.

Next steps

We’ve now learned a lot of basic concepts related to Julia programming, and it is now time to put them together in more complex ways. For that, we’ll learn how to use Pluto notebooks on the next page. This will enable us to write longer programs and organize and save them.