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 can do a little more complex expression:

7 * (6 - 1)
35

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 value of 3 + 5 / 2^3 and (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:

7 % 2
1

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.

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 sssigning 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 is a convenient to enter several commands on the same line. Another way to enter a compound expression is with a begin-end block:

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

Pick a number for the radius (in km) of a circle, and assign that to a variable name and store that in Julia’s memory. Then in a separate command, use that variable 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 have exact values but are quite limited in the values they can take, whereas floating-point numbers can take many more values but have a disadvantage that they 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 variable 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 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}

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 collect(0:2:30).

Create 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 variable 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).

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

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

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

Exponents on Strings

If Julia uses the * symbol 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.

Your favourite species

Assign a few of your favorite species to different variable 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.

Plant 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:

string(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

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

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 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
Select 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 variables are more complex data structures that 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

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 function 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:

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, 2.7, -12)
typeof(myTuple)
Tuple{Float64, Float64, Int64}

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.base
"C"

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 fixed bits of info together. Because they are not mutable, Julia can store and use them in a more efficient way than arrays. They are also used when calling functions (we’ll explore this later).

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
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 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.000018 seconds (3 allocations: 7.875 KiB)
513.0156537039451

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.

Even more numbers!

Modify the above code so that it takes the square of each of one million random numbers between 0 and 1, and then adds them up.

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. There are several ways to define them:

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

We’ve defined that function. Now we can use it:

@time sumSqrtRands(1_000_000)
  0.038686 seconds (6 allocations: 15.259 MiB, 69.99% gc time)
666689.9455230328

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
Apply 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, enter the package mode of the REPL by typing the right square bracket symbol ] . You will then see the julia> prompt change into a prompt that looks something like (@v1.11) 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 to write longer programs and organize and save them.