There's a couple reasons we have identified to motivate studying Julia.
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.
# I'll write my solution here
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:
println("Hello 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.
# 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 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.
# 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.
# String replacement
println("The cowboy goes $🤠")
println("5 + 5 is equal to $(5 + 5)")
Julia supports all the operators you would expect.
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.
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.
# 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)")
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.
integer = 15 # My (Christian's) baseball number
# Will return "Int64"
println(typeof(integer))
float = 15.0 # A float is any number with a decimal in it.
# Returns "Float64"
println(typeof(float))
boolean = true # Notice that "true" is NOT capitalized
println(typeof(boolean))
char = 'e' # Notice the use of single quotes
println(typeof(char))
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!
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:
big = convert(BigInt, 68)^convert(BigInt,476)
println(typeof(big))
println(big)
Obviously you have your int
s and your char
s, but Julia also provides larger datatypes. Today we will cover dictionary, tuples, and arrays.
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
.
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.
# 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 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.
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.
# 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.
fishNames = (oneFish = "Ava", twoFish = "Browne", redFish = "Carmen", blueFish = "Daniel")
println(fishNames.twoFish)
# Accessing by index still works, though
println(fishNames[1])
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.
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.
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.
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:
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.
# 3x4 matrix filled with random values between 0 and 1
rand(3,4)
# 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).
Julia has if
statements. They are structured like this:
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 if
s, elseif
s or else
s. 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 :
.
a = true
a ? println("a is true") : println("a is false")
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 :).
# 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):
# 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:
arr = [1,2,3]
println(length(arr) < 4 || arr[4] == 4) # notice that arr[4] would throw an error
struct
s and const
s¶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 struct
s are as close as you get to objects in Julia.
# 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
# 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:
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:
const myConstant = 10
myConstant = "new value"
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.
# for loops
for n in 1:5
println(n)
end
# while loops
n = 1
while n <= 5
println(n)
n += 1 # n++ does not work unfortunately
end
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.
# 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.
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".
# 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
# 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
# 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!
"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.
# 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?
# 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.
# 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:
# 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.
println(guessNumber(69))
println(guessNumber("abc"))
In fact, for many functions, Julia will accept all types of inputs, as long as they make sense.
There are three ways to define functions. Here they are below.
function helloWorldNormal()
println("Hello, World!")
end
helloWorldVariable = () -> println("Where is Christian today?")
helloWorldInline() = println("Nowhere to be found.")
f(x) = x^2
helloWorldVariable()
helloWorldInline()
f(5)
!
means in Julia¶Remember how !
denote that a function will change the array it is inputed? Here is a great example.
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!
.
toSort = [5, 2, 7]
println(sort!(toSort))
toSort
The array is actually sorted now. Something to keep in mind while coding.
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.
https://projecteuler.net/problem=4
Hint: use the digits()
function
https://projecteuler.net/problem=9
Hint: Julia is fast! Just brute force this one.