First Steps

Getting started

Tip

The julia manual is excellent!

At this point we assume that you have Julia 1.9 installed, VSCode language extension ready, and installed the VSCode Julia plugin. There are some more recommended settings in VSCode which are not necessary, but helpful.

We further recommend to not use the small β€œplay” button on the top right (which opens a new julia process everytime you change something), but rather open a new Julia repl (ctrl+shift+p => >Julia: Start Repl) which you keep open as long as possible.

Tip

VSCode automatically loads the Revise.jl package, which screens all your actively loaded packages/files and updates the methods instances whenever it detects a change. This is quite similar to %autoreload 2 in ipython. If you use VSCode, you dont need to think about it, if you prefer a command line, you should put Revise.jl in your startup.jl file.

Syntax differences Python/R/MatLab

Control Structures

Matlab User? Syntax will be very familiar.

R User? Forget about all the {} brackets.

Python User? We don’t need no intendation, and we also have 1-index.

myarray = zeros(6)
for k = 1:length(myarray)
    if iseven(k)
        myarray[k] = sum(myarray[1:k])
    elseif k == 5
        myarray = myarray .- 1
    else 
        myarray[k] = 5
    end
end
1
initialize a vector (check with typeof(myArray))
2
Control-Structure for-loop. 1-index!
3
MatLab: Notice the [ brackets to index Arrays!
4
Python/R: . always means elementwise
5
Python/R: end after each control sequence

Functions

function myfunction(a,b=123;keyword1="defaultkeyword")
    if keyword1 == "defaultkeyword"
        c = a+b
    else
        c = a*b
    end
    return c
end
methods(myfunction)
myfunction(0)
myfunction(1;keyword1 = "notdefault")
myfunction(0,5)
myfunction(0,5;keyword1 = "notdefault")
1
everything before the ; => positional, after => kwargs
2
List all methods with that function name - returns two functions, due to the b=123 optional positional argument
Tip

Terminology function vs. method: Methods are instantiations of an abstract function

 anonym = (x,y) -> x+y
 anonym(3,4)
myshortfunction(x) = x^2
function mylongfunction(x)
    return x^2
end
myfunction(args...;kwargs...) = myotherfunction(newarg,args...;kwargs...)

In the beginning there was nothing

nothing- but also NaN and also Missing.

Each of those has a specific purpose, but most likely we will only need a = nothing and b = NaN.

Note that NaN counts as a Float-Number, whereas nothing & missing does not.

Excourse: splatting & slurping

Think of it as unpacking / collecting something

a = [1,2,3]
+(a)
+(a...)
1
equivalent to +(1,2,3)

elementwise-function / broadcasting

Julia is very neat in regards of applying functions elementwise (also called broadcasting).

    a = [1,2,3,4]
    b = sqrt(a)
    c = sqrt.(a)
1
Error - there is no method defined for the sqrt of a Vector
2
the small . applies the function to all elements of the container a - this works as β€œexpected”
Important

Broadcasting is very powerful, as Julia can get a huge performance boost in chaining many operations, without requiring saving temporary arrays. For example:

    a = [1,2,3,4,5]
    b = [6,7,8,9,10]

    c = (a.^2 .+ sqrt.(a) .+ log.(a.*b))./5

In many languages (Matlab, Python, R) you would need to do the following:

1. temp1 = a.*b
2. temp2 = log.(temp1)
3. temp3 = a.^2
4. temp4 = sqrt.(a)
5. temp5 = temp3 .+ temp4
6. temp6 = temp5 + temp2
7. output = temp6./5

Thus, we need to allocate ~7x the memory of the vector (not at the same time though).

In Julia, the elementwise code above rather translates to:

    c = similar(a)
    for k = 1:length(a)
        c[k] = (a[k]^2 + sqrt(a[k]) + log(a[k]*b[k]))/5
    end
1
Function to initialize an undef array with the same size as a

The temp memory we need at each iteration is simply c[k]. And a nice sideeffect: By doing this, we get rid of any specialized β€œserialized” function, e.g. to do sum, or + or whatever. Those are typically the inbuilt C functions in Python/Matlab/R, that really speed up things. In Julia we do not need inbuilt functions for speed.

Linear Algebra

import LinearAlgebra
import LinearAlgebra: qr
using LinearAlgebra
1
Requires to write LinearAlgebra.QR(...) to access a function
2
LinearAlgebra is a Base package, and always available
Tip

Julia typically recommends to use using PackageNames. Name-space polution is not a problem, as the package manager will never silently overwrite an already existing method - it will always ask the user to specify in those cases (different to R: shows a warning, or Python: just goes on with life as if nothing happened)

A = Matrix{Float64}(undef,11,22)
B = Array{Float64,2}(undef,22,33)
qr(A*B)
1
equivalent to Array, as Matrix is a convenience type-alias for Array with 2 dimensions. Same thing for Vector.
2
the 2 of {Float64,2} is not mandatory

Much more on Wednesday in the lecture LinearAlgebra!

Style-conventions

variables lowercase, lower_case
Types,Modules UpperCamelCase
functions, macro lowercase
inplace / side-effects endwith!()1

Task 1

Ok - lot of introduction, but I think you are ready for your first interactive task. Follow Task 1 here.

Julia Basics - II

Strings

    character = 'a'
    str = "abc"
    str[3]
1
returns c

characters

    'a':'f'
    collect('a':'f')
    join('a':'f')
1
a StepRange between characters
2
a Array{Chars}
3
a String

concatenation

    a = "one"
    b = "two"
    ab = a * b
1
Indeed, * and not + - as plus implies from algebra that a+b == b+a which obviously is not true for string concatenation. But a*b !== b*a - at least for matrices.

substrings

    str = "long string"
    substr = SubString(str, 1, 4)
    whereis_str = findfirst("str",str)

regexp

    str = "any WORD written in CAPITAL?"
    occursin(r"[A-Z]+", str)
    m = match(r"[A-Z]+",str)
1
Returns true. Note the small r before the r"regular expression" - nifty!
2
Returns a ::RegexMatch - access via m.match & m.offset (index) - or m.captures / m.offsets if you defined capture-groups

Interpolation

    a = 123
    str = "this is a: $a; this 2*a: $(2*a)"  

Scopes

All things (excepts modules) are in local scope (in scripts)

a = 0
for k = 1:10
    a = 1
end
a
1
a = 0! - in a script; but a = 1 in the REPL!

Variables are in global scope in the REPL for debugging convenience

Tip

Putting this code into a function automatically resolves this issue

  function myfun()
  a = 0
    for k = 1:10
        a = 1
    end
    a
    return a
  end
  myfun()
1
returns 1 now in both REPL and include(β€œmyscript.jl”)

explicit global / local

a = 0
global b
b = 0
for k = 1:10
    local a 
    global b
    a = 1
    b = 1
end
a
b
1
a = 0
2
b = 1

Modifying containers works in any case

a = zeros(10)
for k = 1:10
    
    a[k] = k
end
a
1
This works β€œcorrectly” in the REPL as well as in a script, because we modify the content of a, not a itself

Types

Types play a super important role in Julia for several main reasons:

  1. The allow for specialization e.g. +(a::Int64,b::Float64) might have a different (faster?) implementation compared to +(a::Float64,b::Float64)
  2. They allow for generalization using abstract types
  3. They act as containers, structuring your programs and tools

Everything in julia has a type! Check this out:

typeof(1)
typeof(1.0)
typeof(sum)
typeof([1])
typeof([(1,2),"5"])

We will discuss two types of types:

  1. composite types
  2. abstract types.

There is a third type, primitive type - but we will practically never use them Not much to say at this level, they are types like Float64. You could define your own one, e.g.

primitive type Float128 <: AbstractFloat 128 end

And there are two more, Singleton types and Parametric types - which (at least the latter), you might use at some point. But not in this tutorial.

composite types

You can think of these types as containers for your variables, which allows you for specialization.

    struct SimulationResults
        parameters::Vector
        results::Vector
    end

    s = SimulationResults([1,2,3],[5,6,7,8,9,10,NaN])

   function print(s::SimulationResults)
        println("The following simulation was run:")
        println("Parameters: ",s.parameters)
        println("And we got results!")
        println("Results: ",s.results)
    end

    print(s)

    function SimulationResults(parameters)
        results = run_simulation(parameters)
        return SimulationResults(parameters,results)
    end

    function run_simulation(x)
        return cumsum(repeat(x,2))
    end

    s = SimulationResults([1,2,3])
    print(s)
1
in case not all fields are directly defined, we can provide an outer constructor (there are also inner constructors, but we will not discuss them here)
Warning

once defined, a type-definition in the global scope of the REPL cannot be re-defined without restarting the julia REPL! This is annoying, there are some tricks arround it (e.g. defining the type in a module (see below), and then reloading the module)

Task 2

Follow Task 2 here

Julia Basics III

Modules

module MyStatsPackage
    include("src/statistic_functions.jl")
    export SimulationResults
    export rse_tstat
end

using MyStatsPackage
1
This makes the SimulationResults type immediately available after running using MyStatsPackage. To use the other β€œinternal” functions, one would use MyStatsPackage.rse_sum.
    import MyStatsPackage
    
    MyStatsPackage.rse_tstat(1:10)

    import MyStatsPackage: rse_sum
    rse_sum(1:10)

Macros

Macros allow to programmers to edit the actual code before it is run. We will pretty much just use them, without learning how they work.

    @which cumsum
    @which(cumsum)
    a = "123"
    @show a

Debugging

Debug messages

@debug "my debug message"
ENV["JULIA_DEBUG"] = Main
ENV["JULIA_DEBUG"] = MyPackage

Debugger proper:

Cheatsheet for debugging

In most cases, @run myFunction(1,2,3) is sufficient.

Footnotes

  1. A functionname ending with a ! indicates that inplace operations will occur / side-effects are possible. This is convention only, but in 99% of cases adoptedβ†©οΈŽ