A Bez Bez Guide to Julia

When you are new, what can you do?

Alright, install Julia. Good job. Now run Julia.

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version x.y.z (YYYY-MM-DD)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia>

This is the REPL. You can use it to interact with Julia in real time! Type in 2+2:

2+2
4

Good good. You’ve made a calculator. What else can this calculator calculate?

total = 2+2+me+you
ERROR: UndefVarError: `me` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

Hmm, that didn’t work. Why not? It said “me not defined in Main”. What does that mean? What is Main? I think we should give up now. We got a little bit too ambitious, we bit off more than we can chew. I don’t think we can really learn this right now. I don’t think I can really write this right now.

Well ok, we should at least mention variables for a second. A variable, is a name for a thing. It’s good to give names to things. But we have to take care of both the name and the thing. Me and you are names. Lets say that I am the integer 1, and you are also the integer 1.

me = 1
you = 1
total = 2+2+me+you
6

Cool. I always wanted to be an integer. Integers a whole numbers, not fractions or decimals. Julia didn’t learn about commas in grade-school, so we can’t write 1,000,000. If you want, you can write 1_000_000. You also can’t name a variable with a space, so instead of the world you can use a _ there too to get the_world.

me = 1  #this is a comment, this is hidden to the computer!
you = 1 #only you and I can see it!
the_world = 10_000_000_000
total = me+you - the_world #bwahahahahaha
-9999999998

It’s me and you against the world baby! Alright, I don’t think we need to give up. We’ve got this. I think we can learn some more. Variables can be all sorts of things. A variable might be some text. An emoji. A decimal. A fraction. In Julia the variable names can also be fun little symbols. You can run this in the REPL, you can also save this as a .jl Julia file, and run it by typing “julia whatever.jl” into a terminal/command prompt. I like to use Visual Studio code for running these files because it has can show all the current variable values in the workspace menu. Very nifty.

our_slogan = "A slogan is half the battle!"
our_slogo = "A slogo is half a slogan and half a logo 🐔!"
our_logo = "🐔"
the_best_decimal = 6.28
τ = the_best_decimal #you can make this symbol by typing \tau and then tab
👶 = 1/3 #you can make this symbol by looking up "baby emoji" on the internet
0.3333333333333333

Some rules about variable names. They can’t start with the non-letter symbols on your keyboard. So no numbers, no % and absolutly no <. There are also a bunch of hidden secret words that you aren’t allowed to use. We’ll cover those soon, but generally try not to name your variables a really common word like “and”.

This section is getting pretty long though. We should do something here. Did you know, than an E. coli bacateria can divide every half hour? So about 50 times in a day. Everytime they divide you multiply by 2. So the number of_scary E. coli goes like


#= When you want to make bigger bits of secret comments
you can use comment blocks, like this =#
divisions = 50
the_world = 10_000_000_000
ecoli_world = 2 ^ divisions
1125899906842624

What a terrifying number. Not a single comma in it though, so I have no idea what number that is. Keno, can you fix this? Moving on.

Constants

Constanst made with the const keyword make a variable immutable. They never change, so they aren’t variable, they are constant! I like to scream constants in ALL CAPITAL LETTERS, so I would write

const COST_OF_COSTCO_HOTDOG = 1.50
1.5

Symbols

Symbols are variables that start with a : character. Like ‘:rubber_ducky’. Oh good, that was really a short section. What’s the point of these? Use a :Symbol when you need a unique identifier that you will use repeatedly as a value. They are often used for colors like :blue. Anyways on to the next section.

Ranges

A range is created with a colon :. You can access a specific value in these ranges using square brackets []. You can add another number in between a colon sandwich, to determine the interval of the range.

## You can use these double hash marks to create blocks of runable code in a code editor
one_to_five = 1:5
one_to_five[3]
a_to_z = 'a':'z'
a_to_z[3]

## In VSCode you can use alt-enter to run a double hash-marked block
tenth_interval = 1:.1:5 #and ctrl-enter to run a single line
tenth_interval[3]
backwards_wow = 1:-.1:0
backwards_wow[3]
0.8

Arrays: Vectors and the Matrix

A matrix is a variable made out of a grid of values. We put the grid in square brackets [ ]. Semicolons ; in julia mean vertical concatenation, you use them to seperate elements vertically. A 2x2 matrix of strings is just [“two” “too”; “to” “2”]. [1 2 4] is a simple 1x3 Matrix. A comma , is similar, it seperates out values into a vertical list, that becomes a special matrix we call a vector: ["me", "us", "y'all"] is a 3x1 vector of strings.

[1 2 4]
[1; 2; 4]
["me", "us", "y'all"]

T2 = ["two" "too"; "to" "2"]

matrix_3x3 = [1 2 4; 8 16 32; 64 128 256]
3×3 Matrix{Int64}:
  1    2    4
  8   16   32
 64  128  256

Julia handles normal math the way you learned in grade school. Matrix math is a little more complicated though. You use the math symbol to get the matrix version of the operation. If you want a pair by pair version of the operation, you need to start with the broadcast symbol . , so for instance .*.

matrix_2x2 = [1 2; 3 4]

element_wise_multiple = matrix_2x2 .* matrix_2x2 #BROADCAST
matrix_wise_multiple = matrix_2x2 * matrix_2x2

matrix_2x2 .^2 #BROADCAST, EACH BIT GETS SQUARED!
matrix_2x2 ^ 2 #The square matrix is multiplied by the square matrix, squared square

#You can use a ' symbol to transpose a matrix
transpose_2x2 = matrix_2x2'
conjugate_multiple = matrix_2x2 * matrix_2x2'
2×2 Matrix{Int64}:
  5  11
 11  25

Once the matrix is built, access a specific value in these ranges using square brackets []. A[3] means: get the third value of A. Commas now seperate dimensions, so the row, the column. You can use the range notation with integers inside the matrix to call up a bit of the matrix. If you just put the colon : , then you get the whole range. You can use the end keyword to select the end of a range without knowing what it is.

matrix_3x3 = [1 2 4; 8 16 32; 64 128 256]
first_row = matrix_3x3[1,:]
first_column = matrix_3x3[:,1]
some_numbers = matrix_3x3[2,2:end]

#if you wanted the output horizontal, instead of vertical
first_column_transpose = matrix_3x3[:,1]'
1×3 adjoint(::Vector{Int64}) with eltype Int64:
 1  8  64

Finally, you can smoosh or nest arrays and vectors as you please. Usually you want to use a ; not a , when smooshing.

A = [1   2;  3  4]
B =  [10 20; 30 40]
C = [A B]
D = [A B; B A]

#Note a comma here, creates a list of A, then B
vector_of_matrices = [A,B]
#While a semicolon here, vertically concatonates A and B
vector_of_matrices = [A;B]
4×2 Matrix{Int64}:
  1   2
  3   4
 10  20
 30  40

Whew! That was tense!

Types and Structs

You may have noticed that Julia keeps giving you some extra information about your variables you are making. You made a matrix A, but Julia called a 2x2 Matrix{Int64}. The curly brackets {} always specify types in Julia. Here is a very short list of some types, I’ve only included the absolute most important ones such as the Type type.

Int, Int8, Int16, skip a few Int128, UInt8 skip some more UInt128, Bool, Float16, Float32, Float64, Char, String, Symbol, Regex, Array{T, N}, Vector{T}, Matrix{T}, Dict{K, V}, Set{T}, Tuple, NamedTuple, Any, Number, Real, Integer, Function, Type, Nothing, Missing, Some{T}, IO, Task, Expr, Module, Date, Time

Note that types always start with a capital letter. I forget that a lot.

A struct is a composite of multiple variables. When you make a variable that is a struct, you access the different subvalues using the period . You build a struct like this

struct Planet
  mass
  color
  rings
end
Saturn = Planet(5,"yellow",true)
Saturn.mass
#Now that you've built saturn, you can't change saturn!
#So this won't work: Saturn.Mass = 6
5

You can use the double colon :: type assertion, to enforce that these subvariables are certain types. This gives Julia a lot more information to help it help you make your code faster and more robust. Once you have made a struct it is immutable, unless you say it is mutable, then you can change it.

struct PickyPlanet
  mass::Float64 #in kg
  color::String #in food colors
  rings::Bool #true/false
end

Saturn = PickyPlanet(5.68e26,"Buttery Yellow",true)
Saturn.rings

mutable struct Plant
  mass::Float64 #in kg
  color::String #in food colors
  numOfLeaves::Int
end
Satureja = Plant(0.01, "Herb Green",31)
Satureja.numOfLeaves = 33 #New leaves!
33

The New Tuples

A tuple is a newfangled word them kids are using like mid or fire. The word comes from a generalization of double triple quadruple etc, into “tuple”. Its a mini one-off struct, it has some multiple of hetergenous ordered and immutable values. I think the word tuple is mid. But maybe I’m just old and not keeping up with the new math kids. Don’t worry if you don’t see the point of this just yet, tuples will return.

Saturn = (5.68e26,"Buttery Yellow",true)
Saturn[2]
"Buttery Yellow"

A named tuple, is essentially a one-time-use struct. You create these by assigning names to each value of the tuple.

Saturn = (mass=5.68e26,color="Buttery Yellow",rings=true)
Saturn.mass
5.68e26

Function, it’s what you do!

Functions are the verbs of Julia programming. They can take in variables as a list of comma seperated “arguments”, perform some code, and they can return a list of comma seperated variables with the return keyword. You name a function and define it’s arguments as a comma-seperated list. If a function has type assertion ::, then another function of the same name can be built that takes in different types. This lets you build a function with one name, but a series of methods that handles different input variables

function becomeLessNew(age::Int, increment::Int)
  new_age = age + increment
  return new_age
end

#Same function, different input variable
function becomeLessNew(age::Int)
  new_age = age + 1
  return new_age
end

Age = 0
Age = becomeLessNew(Age,1)
becomeLessNew(Age)
#Because we specified variable type as Int, this wouldn't work:
#becomeLessNew(0,0.5)
2

Whenever you see parenthesis in Julia, that means a function is being defined or called. Look at our stuct code! A function! That was the construsctor function of the programming language, constructing a new variable based on the arguments you gave it. The Tuple also has those constructor function parenthesis. If there is a ! in the function, that means the function is going to modify some data.

Functions are really at the heart of Julia, and are what make it so amazing. It’s worth talking about functions a lot. But before we get into the wild wonderous layers of functions Julia, we should fill out the last of the basics.

Some handy functions

Here are some handy functions. You can always press ? then type in the function name to learn more.

  • sort!(A) sorts A, while B=sort(A) can be used to create a new array sorted collection.
  • length(A) and lastindex(A) can be used to get the length of a collection. size(A) works better for a multidimensional array
  • reshape(A,i,j) reshapes collection A into an ixj matrix. This could always reshape a matrix into a vector or vice versa.
  • findfirst(b,V), findall(b,V), findlast(b,V) help you find a sequence b in vector V
  • rand(S,d), creates a random number of type S in d dimensions. Note Julia likes tensor-like dimensions represnted by tuples (i,j,k).

If/For/While: Control the Flow

There are special keywords in Julia that direct the order in which the computer executes your code, instead of just running line by line.

For Loop

A for loop takes an index variable which it repeats for a range of values. You can nest them.

#we can supress output on a line, with a semicolon at the end
mississippi = ["mis", "sis", "sip", "pi"];
for counter = 1:3
#you can use a $ in a string to get a variable
  print(" $counter ")
  for patter = mississippi
    print(patter)
  end
  print('\n')
end

#You can nest multiple for loops into each other
for i = 1:2 , j = 1:0.1:1.2 , k = 'a':'c'
    print("$i$j$k ")
end
 1 mississippi
 2 mississippi
 3 mississippi
11.0a 11.0b 11.0c 11.1a 11.1b 11.1c 11.2a 11.2b 11.2c 21.0a 21.0b 21.0c 21.1a 21.1b 21.1c 21.2a 21.2b 21.2c 

You can also use the keyword in instead of the equals sign, or the mathmatical notation for in which is (then press tab!) to get ∈ . They are all identical to Julia.

If Else

An if statement checks a logical condition. It goes if, then a chain of elseifs, then else, then end. You give each if and else if portion a bunch true or false question, and that dicates what it does. The elseif and else are optional.

struct Animal
  Mass::Float64 #in kg
  Color::String #in food colors
  Eggs::Bool #true/false
end

chicken = Animal(2.0,"Many Colors",true);
secret_animal = chicken;

if secret_animal.Mass < 10^-24
  print("quark")
end

if secret_animal.Mass > 1000
  print("elephant")
elseif secret_animal.Color == "Red"
  print("red panda")
elseif secret_animal.Eggs == true
  print("🐔ITS A CHICKEN🐔")
else
  print("Too difficult :(")
end
🐔ITS A CHICKEN🐔

The “If Else End” logic has a shorthand that fits it into one line called a conditional expression using ? and :. The use of the : symbol like this is a bit of a one-off in Julia, and comes from the fact that many other popular programming languages have all agreed to write conditional expressions in this one manner. While annoying to memorize here, it is nice that once you memorize this in one language, it works in all of them.

#using end line symbol ; we fit multiple short statements on one line
a = 3; b = 2; c = 1;
#if a == 3, then b, else c
a == 3 ? b : c
2

While Loop

A while loop keeps repeating a code until a condition is false. Conditions include but are not limited to: less than < and greater than > , equal to == and not equal to != or if you are feeling fancy. You can add together conditions using and && or or ||. I guess you could write that && && || || ||.

x=1
y=6
while (x < 3) && (y > 4)
  x += 1 #shothand for x = x+1
  y -= 1
  println("$x , $y")
end
2 , 5
3 , 4

Try-Catch Blocks

If your worried a set of code might fail, and you want to create fail-safes, use a Try Catch block. We’ll show a lot of practical examples of this later.

#A tuple, for mixed types!
data_to_process = (5, 10, "potato", 15)

for item in data_to_process
    try # A risky operation
        println(item::Int) #only print Ints!
    catch # When we fail
        println("--> '$item' was not an Int, skipping.")
    end
end
5
10
--> 'potato' was not an Int, skipping.
15

There are also trivial begin-end blocks, which have some interesting uses that will come up later, as well as do-blocks.

Broadcasting

We’ve talked about this before, and we will talk about it again, but broadcasting is so cool it should get its own spot. The . put between a function and some collection, makes that function operate on every bit of that collection. This takes a lot of things that would be gnarley nested for loops in other programming languages, and turns them into a tiny little bit of easily understood code.

my_numbers = [1, 2, 3, 4]

#broadcast sin onto all of them
sines = sin.(my_numbers)

#broadcast sin ont all the cosines of the numbers
result = sin.(cos.(my_numbers))
4-element Vector{Float64}:
  0.5143952585235492
 -0.4042391538522658
 -0.8360218615377305
 -0.6080830096407656

Dictionaries

A dictionary (Dict) is a collection of words matched to their definition. You do so with these cute little arrows =>. When you make a Dict, you put it in parenthesis. But when you call a Dict, you call it with square brackets [ ]. This is because Dict() is a funciton, and you call it with parenthesis. But the variable is an array, and you access arrays with square barckets.

example_dict = Dict("The Arrows" => "Stupid", "Baby" => "👶", "Fukuwarashishende" => "Vanilla wafer cookie", "Age at Which New" => 0)
example_dict["Age at Which New"]
0

Oh, Main is the default module you are in before you specify what module you are in. What’s a module you ask? Oh we don’t have time for that, we have to talk about functions!

Terrific Functions

Say you are William Rowan Hamilton, and you’ve just disocvered how to multiply quaternions. You race to your computer, open up Julia, and type

struct Quaternion{T<:Real}
    w::T # scalar component
    x::T # i component
    y::T # j component
    z::T # k component
end

i = Quaternion(0, 1, 0, 0)
j = Quaternion(0, 0, 1, 0)
k = Quaternion(0, 0, 0, 1)

println("ijk = ", i * j * k)
ERROR: MethodError: no method matching *(::Quaternion{Int64}, ::Quaternion{Int64})
The function `*` exists, but no method is defined for this combination of argument types.

Oh no! You’ve invented multiplication for quaternions, but Julia doesn’t know it yet. How can we tell Julia how to do this multiplication? One solution:

struct Quaternion{T<:Real}
    w::T # scalar component
    x::T # i component
    y::T # j component
    z::T # k component
end

function multiplyQuaternion(a::Quaternion, b::Quaternion)
    w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
    x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y
    y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x
    z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w
    Quaternion(w, x, y, z)
end

i = Quaternion(0, 1, 0, 0);
j = Quaternion(0, 0, 1, 0);
k = Quaternion(0, 0, 0, 1);

ij = multiplyQuaternion(i,j);
ijk = multiplyQuaternion(ij,k)
Quaternion{Int64}(-1, 0, 0, 0)

That… works, but it’s going to get pretty cumbersome if this is the new math we are working with. A better approach is to redefine the multiplication function so that it has a method that can handle quaternions. Oh, ahem:

Methods

A method is an implementation of a function for a specific combination of argument types. This is one of Julia’s super powers. We can make any function act differently depending on what variables it takes in. A simple example is.

function slogo(input_string::String)
  println("Our slogan is: $input_string")
end

function slogo(input_char::Char)
  println("Our logo is: $input_char")
end

slogo("Cheep cheep!")
slogo('🐔')
Our slogan is: Cheep cheep!
Our logo is: 🐔

Going back to our bizzarly complicated example, this is Julia, which means we can do anything we set our minds to. Lets add a method to the multiplication function to support quaternions. As multiplication is part of the base of Julia, we have to refer to as Base., but because it is also a special operator symbol, we need to add a colon to inform Julia we are dealing with a the function itself. If you type Base.:, you’ll see that as of right now this function has 221 methods! It’s about to have 222!

function Base.:*(a::Quaternion, b::Quaternion)
    w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
    x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y
    y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x
    z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w
    Quaternion(w, x, y, z)
end

# Base.show lets us define output of specific types
# We don't need to use the : here, because show isn't a symbol, it's unambiguously a function.
function Base.show(io::IO, q::Quaternion)
    print(io, q.w, " + ", q.x, "i + ", q.y, "j + ", q.z, "k")
end

#Lets take it out for a spin!
= i * i
= j * j
= k * k
i*k #oooooh
ijk = i*j*k #wow!
-1 + 0i + 0j + 0k

Scope

My daughter is sick right now with foot and mouth disease. I don’t know what that means, and I’m scared. In just 36 hours it will be her first birthday, and we’ve had to cancel the birthday party. The doctor says it is fine. My mother says it is fine. The daycare says it is fine, but not to bring her in. The internet says it is fine. But I’m still scared. Believe it or not, she has never been really sick before. This has been her first fever above 100 degrees. We’ve been so lucky in that regard. She can’t sleep with the fever, so I stay up with her and write this guide to Julia. It’s a bezbez guide to Julia. My nickname is Bez, but bez also means “without” in a lot of slavic languages. So it’s the guide on how to code in Julia without me, bez Bez. I’m worried that scope of a tutorial about a whole language is too large.

In Julia, scope is where a variable is visible and can be accessed. Functions create local scopes, so the variables inside a function, are different from the variables outside a function.

function help_my_baby()
  #TODO: Make this function help my baby
  x = 7 
  println(x)
end #this function returns nothing

x = 3
help_my_baby()
println(x)
7
3

We can modify the global scope variable by saying inside the function we want to access it, using the global keyword

function help_my_baby()
  #TODO: Make this function help my baby
  x = 7 
  println(x)
end #this function returns nothing

x = 3
help_my_baby()
println(x)
7
3

In a function, loops also create local scopes. The loop can see variables of the function, but the function cannot see new variables created in the loop. I would read this example twice if I were you.

function loop_scope_visibility()
    visible_to_loop = 100
    for i in 1:5
        loop_only_variable = i * 10
        visible_to_loop += 1 
    end
    println("The function can only see: ", visible_to_loop) 
    #calling loop_only_variable would cause a "not defined" error
end

loop_scope_visibility()
The function can only see: 105

Beautiful functions

In Julia you can write smaller functions in one line, like you might in a maths class.

times_ten(x) = x*10;
times_ten(7)

(x,y) = x+y; #You can make this character by typing \Sum then hitting tab
([1 2 3],[4 5 6])
1×3 Matrix{Int64}:
 5  7  9

Anonymous functions

Anynonymous functions are functions that you don’t bother to name, that are used one time, in the moment. You denote them with this little arrow -> between the input and output . This is particularly helpful with the broadcast symbol . which lets you apply a function to each element in an array.

test_array = [1 2 3; 4 5 6; 7 8 9] ;
#here x is the input, 2 is the output, test_array is the argument
twos_array = (x->2).(test_array)
#note the parenthesis around test_array are necessary
divided_array = (x->1/x).(test_array)

#we can broadcast functions onto anything right, so
(name -> "Hello, $name !").(["🐔" "👶" "bez"])
1×3 Matrix{String}:
 "Hello, 🐔 !"  "Hello, 👶 !"  "Hello, bez !"

Piping hot

The pipe operator, |> so called because it looks like a flag on a flagpole, takes the result of an expression on the left, and passes it as the first argument to the function on the right. This can make it easy to follow code that has too many nested functions.

#these two processes are the same
sin(cos(π/3))
π/3 |> cos |> sin
0.4794255386042031

In a terminal the pipe is just the vertical line | , that’s the pipe. I can see it. You can use it to pass the output of one thing to another thing. So on a mac/linux computer you might type “ls | grep .txt” to list everything with ls, which is read by the grep .txt, resulting in an output of all the files that have .txt as an ending. Nifty. I think in Julia the arrow is to remind you that it is going from left to right, but I think we should call this flagging. Also it reminds me of the output redirection operator > from the terminal.

Composing functions

I think function composition is the first time using Julia where I was like “Whoa”. I don’t know if that was a good or bad moment, but it was a moment. You compose two functions together using the key. Oh, you don’t have a key? Yeah… come to think of it I don’t either. I think what I might do for my next birthday is go get myself one of those giant novelty keyboard buttons, and make it auto-type + tab, which is how you get the symbol in Julia. If you aren’t using the REPL or VS Code, but are using something like Vim, I’m not sure what you do. Memorize the unicode? Get the novelty keyboard button? I think probably the obvious course of action is just stop composing functions. After all, I’ve never seen this feature in any other programming language before, I’ve gotten by without it for a long time, I’m sure I would love this language even if it didn’t have this feature.

gurple(x::Float64,y::Float64) = x^(1/y)
ingurple(x::Float64) = x^(1/x)

# Create a new function that first gurples, then ingurples
ingurple_after_gurpling = ingurple  gurple

# For a one time use, we could have piped or nested function calls
# But for building a new function that we will use many times, this can be convienent.
ingurple_after_gurpling(3.0,2.0) 
ingurple_after_gurpling(7.0,5.0)
ingurple_after_gurpling(9.0,1.0)
1.2765180070092417

You can vaguely remember the order of the composition as if there were parenthesis. It will go func_A(func_B(variables)) in the same way it will go func_A ∘ func_B.

Arguments

When you make more complicated functions, you might want to start making the arguments they can take a little more flexible. To make an argument optional, all you have to do is specify a default value.

function optional_example(x, y=10)
    return x + y
end
optional_example(3)
optional_example(3,4)
7

The first row of arguments is called by position, not name. You can add in a semicolon ; to start a second row of arguments which can be called by name instead of position. You can always use a bit of both.

function gurple(; number::Float64, power::Float64)
    return number ^ (1/power)
end
#when you add a decimal place, Julia defaults to Float64
gurple(number=3.0,power=2.0)
1.7320508075688772

Comprehension and the Generator

A comprehension is a for loop inside array brackets, that builds an array immedietly. A generator is same-same but different, you put the loop inside the function-style paranthesis, and you get a generator object that can make the array when you ask for it.

#My favorite tensor, the levi-civita
#I just which I knew how to pronounce it
levi_civita = [sign((i-j)*(j-k)*(k-i)) for i in 1:3, j in 1:3, k in 1:3]

#Here it is as a generator
# The values are only computed when you loop over the generator
gen_levi_civita = (sign((i-j)*(j-k)*(k-i)) for i in 1:3, j in 1:3, k in 1:3)
for index in gen_levi_civita 
  if index != 0 print("$index ") end
end
-1 1 1 -1 -1 1 

Tuples splat back

Remember the tuple? The newfangled word that comes from a generalization of double triple quadruple etc, into “tuple”? You can use it with ellipses ... to create a splat operator. The splat operator can only be used in a function call or a function definition. In a function call, it takes a tuple, and turns it into individual variables. In a function definition, it takes individual variables, and collects them into a tuple. Now when you see it in young people’s amazing Julia code, you can understand it.


#dist should require two inputs
dist(x, y) = sqrt(x^2 + y^2)

A = (i=2,j=-3) #we have a coordinate named tuple A 
dist_2D = dist(A...) #And splat it into two during the pass
 
#highD_dist unpacks it's input as a splat
highD_dist(nums...) = sqrt(sum(n^2 for n in nums))

#we can call it with an arbitarary number of inputs
dist_6D = highD_dist( 2,-3, 6,-7, 1, 2)
10.14889156509222

A word about functions. Julia is a Just-In-Time compiled language. That’s one of the big things that makes it so special, it is very flexible about when it is going to stop talking to you, and start talking to the computer. If you write in code, line by line, and execute it, then Julia is just sending that code one line at a time to the computer. That works well, but there is some magic you are missing out on. Julia will take functions, and compile them, making the code in a function super-snappy. But if you put all your code in one giant function, then Julia has to compile the whole code, and that might not be a great experience for you. I find Julia is at it’s best, when you are writing a script, with a lot of functions that you call. Julia compiles the functions to make it really fast, but you still get to talk quickly with the computer in a line-by-line style outside of those functions. You don’t need to engage with this, that’s the terrific beauty of Julia and it’s terrific functions, you get to do things your way here.

The Julia Enviornment

Now it’s time to introduce some big flexible concepts in Julia that extend its ability to do everything you want to do with it.

Using Packages

First, Julia uses packages that wicked smart people have written, to do things not available in the very base language. One of these packages that comes with the Julia install, but isn’t in your code by default, is the Statistics package. When you use the Statistics package with the keywork Statistics from then on you have access to the functions in Statistics. You can use names(Statistics) in the REPL to learn what functions are available from that package. using brings in the whole package and the functions in the namespace. import does similar, but does not bring in the functions into the namespace, instead you must always qualify them with the packages name followed by a dot. The dot here is just like in a structure, it’s the member access operator. The dot when used like this gets the value/function that belongs to the structure/module/tuple it is place after.

using Statistics
miney_mo = mean([1.01 3.1 5.3 7.2 9.1])

#you can use import and . to keep your namespace cleaner
import LinearAlgebra
determined = LinearAlgebra.det([1 2 3; 3 2 1; 2 3 1])
12.0

To install new packages, such as GLMakie, you can press ] in the REPL, which puts you in package manager mode. Then add GLMakie where GLMakie is an example package for plotting that I like. Another way to do this, is to use the Pkg package.

try 
  using GLMakie 
catch
  using Pkg
  Pkg.add("GLMakie")
end

That’s less meta than you think, you can use this in code you are sending to friends to ensure that they have packages they will need to run your code.

Modules

Packages are organized into modules. Every package has at least one module, and might have additional helper modules. Modules are not necessarily part of a package. A module is a container for functions, types and variables in an enclose namespace. Start a module with the module keyword, then write what functions, structures, types and constants the module will export in the first line.

module Gurpler
    export gurple, BEST_ANIMAL
    function gurple(x,y)
        return x^(1/y)
    end
    const BEST_ANIMAL = "🐔"
end

# The dot is for a module in the same file that is not part of a package.
# I imagine there is a blank package name ahead of the dot.
import .Gurpler #could do using instead
coords = (x=3,y=7) 
println(Gurpler.gurple(coords...)) #SPLAT!
println("The best animal is still ", Gurpler.BEST_ANIMAL)

Project Manifest

Now that we know about packages, we can talk about Julia’s environment. When you start Julia, you are in a default enviornment, with no packages loaded up. If you go into the package manager ] and use the keyword activate . or activate ./SomeSubFolder Julia will load up two files the Project.toml and Manifest.toml files to set up your enviornment. toml files were made by Tom, and stand for Tom’s Language files. If you open it you can see that Tom speaks a pretty weird language.

The first time you write activate . in the ] package mode, Julia will create those files for you. This lets you seperate out environments for your different projects. The . here is a universal shorthand for “this current folder” that goes beyond just Julia, and isn’t related to other uses of the . we’ve seen before.

The Project.toml and Manifest.toml can seem quirky at first, but if you are coming from another programming language, this is a true unsung hero of the Julia language. The Project.toml file says what packages your project needs to function. The Manifest.toml file says exactly what version of each package you were using when you wrote this code. This alleviates just a tremendous amount of headaches that are present in other programming languages. Programming languages, like real languages, are constantly evolving. Packages are more like websites than books, in that they are constantly changing as well. The changes might be slow, but not slow enough that they don’t cause confusion. This is ineffable and yet this is mid. But Julia does a wildly good job of mitigating the effects of it.

Load up your enviornment with activate . everytime you get going on a project, and it will save you and everyone working with you a ton of heartache. Or don’t do it, live fast and dangerously. Add all your packages to the default environment and rip and tear. Julia lets you work at the tempo you want to work at.

@ Macros

Children. Look away. Don’t read this part. You’re too young for macros. Anything that starts with an @ symbol is a macro. Macros are special functions that rewrite your code before it runs. They change the rules by which Julia works. For instance, let us say you want to look at the native assembly machine code being run by Julia. You can use the @code_native macro to see really low-level assembly code, or @code_llvm to see intermediary llvm code

2+2 #normal style
@code_llvm 2+2 #with the macro...
; Function Signature: +(Int64, Int64)
;  @ int.jl:87 within `+`
; Function Attrs: uwtable
define i64 @"julia_+_14108"(i64 signext %"x::Int64", i64 signext %"y::Int64") #0 {
top:
  %0 = add i64 %"y::Int64", %"x::Int64"
  ret i64 %0
}

Here are some macros:

  • @time will time an expression that follows it. It works well with a begin/end block wrapped around the lines of code you want to measure.
  • @edit will open the source code to a function that follows it.
  • @sync around a function or block such as begin/end waits for all lines started with @async inside that block to complete. This allows for concurrency.
  • @cuda will launch code onto a CUDA-endabled GPU. You’ll need to bring it into the namespace with the CUDA.jl package via using CUDA

@threads

I’m giving macros so much time, because of one useful macro that might change the way you write code. The @threads macro. When you start Julia, be default it is running on one thread, one process on one bit of your computer’s CPU. You can use up all of your computer’s CPU by starting Julia with all the threads. Try starting julia with julia -t 4 to start it with four threads. To run a script julia -t 4 SuperCoolProgram.jl. If you are using an IDE like Visual Studio Code to run Julia, each IDE has it’s own way to auto-configure this. Now that you are running Julia on multiple threads, the Threads module that is auto-loaded make use of this by putting Threads.\@threads before for loops

Threads.@threads for i in 1:6
    println("Iteration $i is running on thread number ", Threads.threadid())
end
Iteration 1 is running on thread number 1
Iteration 2 is running on thread number 1
Iteration 3 is running on thread number 1
Iteration 4 is running on thread number 1
Iteration 5 is running on thread number 1
Iteration 6 is running on thread number 1

You can also place @spawn macros, to assign certain sections of code to individual threads.

@sync begin
    Threads.@spawn println("Task A is on thread ", Threads.threadid())
    Threads.@spawn println("Task B is on thread ", Threads.threadid())
end
Task A is on thread 1
Task B is on thread 1
Task (done) @0x0000020eb2337c20

If having to constantly type Threads. is annoying, you can fully import the module into the namespace with using Threads

Object-Oriented with Structures

One way I code is in terms of functions. This is very verb based, I’m trying to make a sequence of things happen in a particular way. Another way to code is to focus on structures, and have the objects undergo a series of changes. It’s very noun based. When you use structs this way it becomes important to help Julia by defining methods of a function that are type-checked :: specifically for your structs. You can use methodswith(StructureName) to find what methods exist for a given struct.

Inheretence with <: (“is a”)

If you want to be work with objects, in which one object is a sub-type of another object, for-example a chicken is a type of dinosaur, then you need inheretence. The subtype oeprator <: says a struct is a type. At the tops of these logic chains use an abstract type. The abstract type here means it is a type that you cannot create an instance of. This is very handy because you can write functions for all structs of the abstract type all at once.

A little bit on the naming heirarchy here when disccusing your code with others:

  • Types
    • abstract type
    • primitive type (e.g. Float64)
    • struct

When it comes time to get the values of a structure, you use the . symbol to access the values.

#We want to group dinosaurs
#but we don't want anyone making a dinosaur without specifying what kind
abstract type Dinosaur end

#The simple chicken *is a* dinosaur
struct simpleChicken <: Dinosaur
    breed::String
end

#Define dinosaur behaviors by creating functions for the abstract type dinosaur using ::
#You can use the typeof() function to find the type of dinosaure we have.
begin_nesting(d::Dinosaur) = println("The $(typeof(d)) dinosaur has begun nesting behavior")
#Only chickens have breeds though
state_breed(c::simpleChicken) = println("The $(typeof(c)) is a $(c.breed)")

chickydoo = simpleChicken("Buff Orpington") #Create our chicken!
begin_nesting(chickydoo) #Works because chickydoo the chicken *is a* dinosaur
state_breed(chickydoo)
The Main.Notebook.simpleChicken dinosaur has begun nesting behavior
The Main.Notebook.simpleChicken is a Buff Orpington

Composiiton (“has a”)

Let’s run the chicken-dinosaur example one more time. A chicken is a dinosaur, so it has a set of attributes that all dinosaurs have in a common. We build this with a DinosaurData struct which we place inside the Chicken struct with a new name . This is structure composition, similar to function composition in concept.

# Define a struct for common data among all dinosaurs
struct DinosaurData
    era::String
    roar::String
end

# The chicken struct *has a* set of dinosaurData called "data"
struct Chicken <: Dinosaur
    data::DinosaurData 
    breed::String
end

#Dino function
roar(d::Dinosaur) = println("A dinosaur roars: $(d.data.roar)")
#Chicken-Chicken function
cross_breed(c1::Chicken,c2::Chicken) = println("New Breed: $(c1.breed)-$(c2.breed)")

chickydoo = Chicken(DinosaurData("Modern","Bwaaak"), "Buff")
chickytwo = Chicken(DinosaurData("Modern","Cockadoodledoo!"), "Wyandotte")
roar(chickydoo) # Works because a chicken *is a* dinosaur
cross_breed(chickydoo,chickytwo)
A dinosaur roars: Bwaaak
New Breed: Buff-Wyandotte

Multiple Dispatch of Functions

Polymorphism (many forms), is accomplished through multiple dispatch. We can program our functions to act differently with different structs using the :: type assertion operator. Let’s get some more dinosaurs in there, and see how this goes.

#Ok, we already have one chicken, lets add a dinosaur
struct TRex <: Dinosaur
    data::DinosaurData
    tooth_count::Int
end

# We can create a more specific method just for the TRex!
roar(d::TRex) = println("A T-Rex roars with $(d.tooth_count) teeth: $(d.data.roar)!!!")

# Let's create a collection of our dinosaurs.
dino_pen = [
    Chicken(DinosaurData("Modern", "Bwaaak"), "Buff Orpington"),
    TRex(DinosaurData("Cretaceous", "ROOOOAR"), 58)
]

for dino in dino_pen
    roar(dino)
end
A dinosaur roars: Bwaaak
A T-Rex roars with 58 teeth: ROOOOAR!!!