C14008: Introduction to being a Code Ninja in Julia

Lesson 1: The Basics

Motivation: Why study Julia?

There's a couple reasons we have identified to motivate studying Julia.

  1. Julia is fast. It offers a notable speed improvement from interpreted languages like Python.
  2. Julia is intuitive. It's familiar to languages you might have dealt with before and its syntax is easy to pick up and learn.
  3. Julia is predictable. You can define types (that are actually enforced, cough cough... Python), and the language operates off of a set of base functions that can be modified or extended through multiple dispatch.
  4. Julia is the future. It's flexible and fun to write. There's a growing programmer community around the language. It can be integrated into other languages and other languages can be integrated into Julia. Expect to see at as part of more projects in the future.

Speaking of Julia being fast...

Let's try and solve a really big problem with Julia, such as this one from Project Euler Problem 99!

Here's the problem restated.

Comparing two numbers written in index form like $2^{11}$ and $3^{7}$ is not difficult, as any calculator would confirm that $2^{11} = 2048 < 3^7 = 2187$.

However, confirming that $$632382^{518061} > 519432^{525806}$$ would be much more difficult, as both numbers contain over three million digits.

Using base_exp.txt (right click and 'Save Link/Target As...'), a 22K text file containing one thousand lines with a base/exponent pair on each line, determine which line number has the greatest numerical value.

NOTE: The first two lines in the file represent the numbers in the example given above.

In [ ]:
# I'll write my solution here

So let's get started!

Hello World

Of course, we have to start with a Hello World example. Julia actually has two types of print statements, print() and println(). println() adds a newline character after the arguments. They can take multiple arguments too, like println("Hello", " ", "World"). Here's an example:

In [ ]:
println("Hello Julia!")

Strings and comments in Julia

Unlike Python, strings in Julia have double quotes "like this", while there is a special character type for characters if you use single quotes like this: 'a' 'b' and 'c'. For multiline strings, you can use triple quotes, demonstrated below.

Comments are the same as Python, with the hash character # for a single line comment. Unlike Python, Julia can do multiline comments. #= starts a multiline comment and =# ends a multiline comment.

Also like Python, we can use the builtin function typeof() to tell us which type the variables are.

In [ ]:
# Some strings
println("Hello World!")
println(typeof("Hello World!"))

println()

# Some characters
println('a', 'b', 'c')
println(typeof('a'))

println()

# A multiline string
println("""
Hi I'm a multiline String.
I have multiple lines!
""")

# Some comments
#=
Hi I'm a multiline comment
=#

Variables in Julia

Variables in Julia look like variables in any other language you might be used to. Let's set the variable 🤠 equal to "yeehaw!". That's right! Julia has native Unicode support, so you can make your variables emojis. We'll see later that Unicode operators like ≤ and ≈ are actual operations in Julia.

In [ ]:
# Unicode variables
🤠 = "yeehaw!"

println(🤠)

We can actually reference Julia code inside of our variables if we want to through a process called string interpolation. We use the dollar sign to do this. If we want to reference a variable, we can put $variable_name inside of a string.

If we want to type an expression, we can instead write $(5 + 5) in the string.

In [ ]:
# String replacement
println("The cowboy goes $🤠")
println("5 + 5 is equal to $(5 + 5)")

Operators in Julia

Julia supports all the operators you would expect.

In [ ]:
x = 5
y = 5

# operators in Julia, add, sub, mult, div, pow, mod
addition = x + y
subtraction = x - y
multiplication = x * y
division = x/y
@assert typeof(division) == Float64 # division will be a float (decimal), despite x and y being integers

power = x ^ y
modulous = x % y

# rationals
rational = x//y

Some operators don't strictly apply to numbers. For example, we can use the * and ^ operators on strings to preform various functions.

In [ ]:
s1 = "Road work ahead? "
s2 = "Uh yeah, I sure hope it does."

# * can be used to concatenate these two strings
println(s1 * s2)

# the ^ also has a function, I'll leave it up to y'all to figure out when this would ever come in handy.
println(s2 ^ 5)

The equality operator can be used in some special ways, such as assigning two values to the same variable. You can also assign two variables at the same time, with this tuple syntax.

In [ ]:
# double assignment and tuple assignment
a = b = 10
c, d = 15, 20

print("a is $(a), b is $(b), c is $(c), and d is $(d)")

Datatypes in Julia

Basic data types

Julia has most of the data types you have run across in different languages, plus some. The types are capitalized and sometimes have a number after them to represent the number of bits stored (Int64 is a 64 bit integer), however when referencing a type you will be able to get away without the number (just Int). When defining a variable, Julia does a very good job of determining what type of variable you want and automatically assigns a type to it for you. This is great, but it does mean you need to be careful about type errors arising in your code.

In [ ]:
integer = 15 # My (Christian's) baseball number
# Will return "Int64"
println(typeof(integer))
In [ ]:
float = 15.0 # A float is any number with a decimal in it.
# Returns "Float64"
println(typeof(float))
In [ ]:
boolean = true # Notice that "true" is NOT capitalized
println(typeof(boolean))
In [ ]:
char = 'e' # Notice the use of single quotes
println(typeof(char))
In [ ]:
complex = 15im # im is tied to the imaginary number i
println(typeof(complex)) # no more coding an extra if statement into your quadratic formula function to account for imaginary numbers!
In [ ]:
big = 68^476
println(typeof(big))
println(big)

Woah, that last one didn't work. It took our big variable and stored it as zero since the number was too large. Sometimes this will happen. Thankfully there are other datatypes to help deal with these fringe cases, like BigInt (a datatype that can store infinitely many digits). Julia will not automatically convert to this datatype, however, so you must do it yourself (#DIY) using the convert function. To convert from one type to another try convert(type, var). So in our case:

In [ ]:
big = convert(BigInt, 68)^convert(BigInt,476)
println(typeof(big))
println(big)

Bigboy Datatypes

Obviously you have your ints and your chars, but Julia also provides larger datatypes. Today we will cover dictionary, tuples, and arrays.

Dictionarys

Like Python and Java (and also probably every other language), dictionaries are essentially an unordered list that maps unique keys to values. To create a dictionary in Julia, use the function Dict.

In [ ]:
mbmbam = Dict("Oldest Brother" => "Justin McElroy", "Middlest Brother" => "Travis McElroy", "Sweet Baby Brother" => "Griffin McElroy")

Notice the use of => instead of a : to assign a value to a key. The syntax to access a value, change a value, and add a key-value pair is the same as it is in Python or Java.

In [ ]:
# Acessing a value
println(mbmbam["Middlest Brother"])

# Changing a value
mbmbam["Oldest Brother"] = "Justin 'Juice' McElroy"

# Adding a key-value pair
mbmbam["30 Under 30 Media Luminary"] = "Griffin McElroy"

println(mbmbam)

We can assign two unique keys to the same value (ie. Griffin McElroy is both our sweet baby brother AND our 30 under 30 media luminary), but every key must be unique. This is the same behavior in Python and many other languages.

Tuples

Tuples are basically an ordered collection of elements. Their most common application in my experience is as coordinates or an index for a higher dimensional array (we will get to this later). To create one, simply enclose data in parentheses.

In [ ]:
fish = ("One Fish", "Two Fish", "Red Fish", "Blue Fish")

To access an element in the tuple, we would (again) do it simililarly to Python or Java (using brackets) except one major difference. Julia is a one indexed language. This means that in order to access the first element in the tuple we would need to write fish[1] NOT fish[0]. It seems like it would be incredibly annoying to adjust to this, but in my expirence it isn't that bad.

In [ ]:
# Prints "Red Fish"
println(fish[3])

# Throws bounds error
println(fish[0])

Missing dictionaries? Well miss no more because we can do a very similar thing using named tuples. With named tuples you can assign a variable to each item in the tuple via a =. To access an element of this named tuple, we use a . followed by the variable name.

In [ ]:
fishNames = (oneFish = "Ava", twoFish = "Browne", redFish = "Carmen", blueFish = "Daniel")
println(fishNames.twoFish)

# Accessing by index still works, though
println(fishNames[1])

Arrays

Although they may be considered one of the simpler datatypes in other languages, Julia is built to handle complex arrays, and what we cover today will only being to scratch the surface. In linear algebra, there are things called matrices, which are like 2-D arrays for math. A 1-D matrix (only one column) is called a vector, and are the type of arrays you are probably most comfortable with. There will be a more in depth crash course on linear algebra on a future date, so don't worry if you don't know much about matrices yet. We will start with vectors (1D arrays). There are several ways to make arrays, we will start with the simplest.

In [ ]:
staff = ["Spongebob", "Squidward", "Mr. Krabs"]

Notice how the type is Array{String, 1}. This tells us this is a 1 dimensional array of Strings. If we were to add an element that is not a string, say the number 15, we would see the type is Array{Any, 1} (you can try it on your own if you don't trust me). Let's access the first element, we can do so just as we do with a tuple.

In [ ]:
staff[1]

The main two functions used with arrays are pop! and push!. The ! denotes that calling the function will change the array it is called on. pop! returns the last element in the array while removing it from the array. push! adds an element to the end of the array.

In [ ]:
staff = ["Spongebob", "Squidward", "Mr. Krabs"] # reset the array
# pop! the array
pop!(staff)
println(staff) # see how the array has changed

push!(staff, "Mr. Krabs") #push!(array name, element to add)
println(staff) # added Mr. Krabs back to the end of the array.

Now, let's talk about the other ways to initialize arrays, ones that support higher dimensions. We can initialize an undefined array by using Array{T,N}(undef,x,y,...,z) where T denotes the type of the array, N denotes the dimensions, and x,y,...,z represent the size. So for example, if we want a 2 dimensional array of strings with a size 3x4, we do:

In [ ]:
nums = Array{String,2}(undef, 3,4)

There are other tricks too. We will see two of them here and will get to another (my favorite one) in the for loop section.

In [ ]:
# 3x4 matrix filled with random values between 0 and 1
rand(3,4)
In [ ]:
# 3x4 matrix of zeros
zeros(3,4)

There are so many ways to initialize and create arrays because Julia is built around matrix manipulation. We will get to cooler functions and applications later, but feel free to do some research on your own, especially during the Euler problems (Hint: if you "wish" a function that manipulated an array in a certain way existed, chances are it does, you just need to find it).

Conditionals

Julia has if statements. They are structured like this:

In [ ]:
x = 15
y = 15

if x > y
    println("$x is greater than $y")
elseif x < y
    println("$y is greater than $x")
else
    println("x is equal to $y")
end

Some major takeaways. First, unlike Python, there is no : after the ifs, elseifs or elses. This takes some getting used to. Secondly, to end the conditional, we write end. This will go for loops and functions as well. Julia also supports the ternary operator. I love the way it looks but it is kind of difficult to use and read, so I do not recommend using it most of the time. Below, we check if the thing before the ? is true, and if it is, we do the thing to the right of the ?. If not, we do the thing to the right of the :.

In [ ]:
a = true
a ? println("a is true") : println("a is false")

Logic

Julia contains mostly the same logic syntax as Java. && denotes and, which only is true if both things on either side of the conditional are true. || denotes or, which is true if one or both things on either side of the conditional are true. ! denotes not, which makes a true statement false and a false statement true. && has a higher precedence in the order of operations than || but if you are worried about messing up the order, feel free to use parentheses to denote precedence. It is easy to mess around with them, especially in the space I have provided below :).

In [ ]:
# change these to examine their behavior
x = true
y = true

println(!x) # not x

println(x && y) # x and y

println(x || y) # x or y

It can be helpful to know a few equalities as well (play around with the truth values of x, y, and z if you don't believe they will always be true):

In [ ]:
# DeMorgan's Law
println(!(x && y) == (!x || !y))
println(!(x || y) == (!x && !y))

# Distribution of and over or and vice versa
z = true
println((z && (x || y)) == ((z && x) || (x && y)))
println((z || (x && y)) == ((z || x) && (x || y)))

There is also a helpful tool called short-circut evaluation. Basically when Julia is presented with an ||, if the thing to the left is true, it marks the statement as true and doesn't bother evaluating the other side. This means we can avoid the out of bounds error in the following situation:

In [ ]:
arr = [1,2,3]

println(length(arr) < 4 || arr[4] == 4) # notice that arr[4] would throw an error

structs and consts

A struct in Julia is a lot like a struct in C, if you've seen those before. It's a custom data type with specific properties. Julia actually isn't an object-oriented language, so structs are as close as you get to objects in Julia.

In [ ]:
# Run this block once to declare a Person type
struct Person{X}
    name::String
    age::Int
    adult::Bool
    pocket::X  # we can specify a custom type here and attach it to person
end
In [ ]:
# Now run this block to create our person
jeff = Person{Rational}("Jeff", 30, true, 7)

Notice how 7 gets coerced into a Rational, because we declared the Person with type Rational. We can access the properties of jeff like so:

In [ ]:
println(jeff.name)
println(jeff.pocket)

A const keyword on a variable in Julia just means we can only define the variable once. See below for an example:

In [ ]:
const myConstant = 10

myConstant = "new value"

Loops

Let's talk about for loops and while loops. As you probably know, for loops run for every element in the set it is fed. while loops run while some condidition is true. For example, if we feed a for loop the range 1:5. Note: Ranges are inclusive on both ends in Julia, unlike Python.

In [ ]:
# for loops
for n in 1:5
    println(n)
end
In [ ]:
# while loops
n = 1
while n <= 5
    println(n)
    n += 1 # n++ does not work unfortunately
end

Obligatory LOOPS HAVE THEIR OWN SCOPE announcement

Scope is like the area of a program a variable is acessible in.

This one will get ya. If you're writing a Julia script (not in a notebook!), you need to specify if you're using a variable that isn't declared inside the foor loop. To do this, we use the global keyword.

In [ ]:
# Code in notebook
classes_christian_taught = 0
for i in 1:6
    classes_christian_taught += 1
end

# try running it as a script
run(`julia -e "classes_christian_taught = 0
for i in 1:6
    global classes_christian_taught
    classes_christian_taught += 1
end
    println(classes_christian_taught)"`)

Now for the great way to initialize an array I promised earlier. Julia supports a lot of single line things, especially when it comes to arrays. This is the most "Julia" way to initialize an array with the numbers 1 through 5 in it.

In [ ]:
nums = [n for n in 1:5]

Another "hacky" thing you can do in Julia is single line nested for loops. To illustrate this, lets create a simple mulitplication table represented as a matrix in 3 different ways, getting progressively more "Julia".

In [ ]:
# Method 1
method1 = Array{Int, 2}(undef, 12,12) # start with an undefined 12x12 matrix

# use classic nested for loops
for row in 1:12
    for col in 1:12
        method1[row, col] = row * col
    end
end

method1
In [ ]:
# Method 2
method2 = Array{Int, 2}(undef, 12,12) # start with an undefined 12x12 matrix

# use the more Julia, single line nested for loops
for row in 1:12, col in 1:12
    method2[row, col] = row * col
end

method2
In [ ]:
# Method 3

# use the *very* Julia single line array comprehension
method3 = [row * col for row in 1:12, col in 1:12] # so clean!

Now that's what I call a times table!

Functions

"Wow! I couldn't function without functions!" - Cameron, probably.

Say we want a function to square things. We would use the keyword function to tell Julia we want to write a function, return to tell her to return, and end to tell her we are done writing the function.

In [ ]:
# write and test squareMe
function squareMe(x)
    return x^2
end

println(squareMe(1))
println(squareMe(2))
println(squareMe(5))

Remember how Julia can square strings? What if we tried to throw in a string?

In [ ]:
# trying a string
squareMe("Big Green Tractor")

Julia can handle it! If we wanted to specify numbers only, we could add type definitions to our function definition. Let's make a method called squareMeNumbers and restrict it to numbers only, using Julia's abstract Number type.

In [ ]:
# limit squareMe to numbers only
function squareMeNumbers(x::Number)::Number
    return x^2
end

println(squareMeNumbers(7))
println(squareMeNumbers("Test"))

What's cool about these function definitions is that we can specify the same function multiple times and have it act differently based on what type it's given. Check this out:

In [ ]:
# write two versions of the same function with multiple dispatch
function guessNumber(x::Int)
    return x == 69 ? "Nice!" : "Nope that's not it."
end

function guessNumber(x) # generic function without type
    return "That's not a number!"
end

Julia figures out what method we want based on our input. This feature is called multiple dispatch.

In [ ]:
println(guessNumber(69))
println(guessNumber("abc"))

In fact, for many functions, Julia will accept all types of inputs, as long as they make sense.

More ways to define functions

There are three ways to define functions. Here they are below.

In [ ]:
function helloWorldNormal()
    println("Hello, World!")
end

helloWorldVariable = () -> println("Where is Christian today?")

helloWorldInline() = println("Nowhere to be found.")

f(x) = x^2
In [ ]:
helloWorldVariable()
helloWorldInline()

f(5)

What the ! means in Julia

Remember how ! denote that a function will change the array it is inputed? Here is a great example.

In [ ]:
toSort = [5, 2, 7]

println(sort(toSort))

toSort

Notice how the array was sorted when it was returned, but the array itself was not altered. Compare that to sort!.

In [ ]:
toSort = [5, 2, 7]

println(sort!(toSort))

toSort

The array is actually sorted now. Something to keep in mind while coding.

Euler Homework Problems

Attempt these problems for extra Julia practice for homework. After each problem, you're provided with a code cell to work on your code. You might be able to present your solutions in class.

Euler 1 (Multiples of 3 and 5)

https://projecteuler.net/problem=1

In [ ]:

Euler 2 (Even Fibonacci numbers)

https://projecteuler.net/problem=2

In [ ]:

Euler 4 (Largest palindrome product)

https://projecteuler.net/problem=4

Hint: use the digits() function

In [ ]:

Euler 6 (Sum square difference)

https://projecteuler.net/problem=6

In [ ]:

Euler 8 (Largest product in a series)

https://projecteuler.net/problem=8

In [ ]:

Euler 9 (Special Pythagorean triplet)

https://projecteuler.net/problem=9

Hint: Julia is fast! Just brute force this one.

In [ ]: