--- marp: true --- # Julia for Data Analysis ## Chapter 2: Getting started with Julia ### Bogumił Kamiński https://github.com/bkamins/JuliaForDataAnalysis --- # Outline 1. Values 2. Variables 3. Control flow 3.1. Conditional evaluation 3.2. Loops 3.3. Compound expressions 4. Defining functions 5. Scoping rules --- # Values * A *value* is a representation of some entity that is stored in computer's memory and can be manipulated by Julia program. * Every value is a result of evaluation of some Julia expression. * Example values created by evaluation of *literals*: ``` julia> 0.1 0.1 julia> "Hello world!" "Hello world!" julia> [1, 2, 3] 3-element Vector{Int64}: 1 2 3 ``` --- # Types * Each value has a type, which you can check using the `typeof` function. * When you define a function you optionally can define the types of arguments that the function accepts. * Example types of values: ``` julia> typeof(0.1) Float64 julia> typeof("Hello world!") String julia> typeof([1, 2, 3]) Vector{Int64} (alias for Array{Int64, 1}) ``` --- # Checking memory layout of numbers * Numbers in Julia can have different types (e.g. `Int64`, `Float64`, `Int8`). * Each such type can use a different memory layout to represent the same mathematical value. * Examples of representation of *one*: ``` julia> bitstring(1) "0000000000000000000000000000000000000000000000000000000000000001" julia> bitstring(1.0) "0011111111110000000000000000000000000000000000000000000000000000" julia> bitstring(Int8(1)) "00000001" ``` --- # Integer type On 64-bit machines integers in Julia by default use 64-bit representation (`Int64` type). As a shorthand you can use `Int` alias type name instead: ``` julia> Int Int64 ``` --- # Container types typically have parameters in Julia ``` julia> typeof([1, 2, 3]) Vector{Int64} (alias for Array{Int64, 1}) ``` * `Vector{Int64}` tells us that `[1, 2, 3]` is a vector that can store integer numbers; the `Int64` part is a parameter of `Vector`; * `Array{Int64, 1}` is another way to write the same; now we have two parameters: 1. element type that our array can store (`Int64`, an integer); 2. dimension of our array (`1`, a vector). --- # Checking if value has some type You can check if some value has some type using the `isa` operator: ``` julia> [1, 2, 3] isa Vector{Int} true julia> [1, 2, 3] isa Array{Int64, 1} true ``` --- # Variables * You can bind a value to a variable name using the assignment operator `=`: ``` julia> x = 1 1 julia> y = [1, 2, 3] 3-element Vector{Int64}: 1 2 3 ``` * The process of binding does not involve copying of values. Python also follows this approach. In R this is not the case. --- # Only values have types in Julia You can, in general assign values of different types to the same variable name: ``` julia> x = 1 1 julia> typeof(x) Int64 julia> x = 0.1 0.1 julia> typeof(x) Float64 ``` Although this is allowed it is usually not recommended. --- # You can use Unicode characters in variable names ``` julia> Kamiński = 1 1 julia> x₁ = 0.5 0.5 julia> ε = 0.0001 0.0001 ``` In Julia REPL, VS Code ect., you can easily type such characters: ``` help?> ₁ "₁" can be typed by \_1 help?> ε "ε" can be typed by \varepsilon ``` --- # The `if` statement ``` julia> x = -7 -7 julia> if x > 0 println("positive") elseif x < 0 println("negative") elseif x == 0 println("zero") else println("unexpected condition") end negative ``` --- # Conditions must be Boolean values ``` julia> x = -7 -7 julia> if x println("condition was true") end ERROR: TypeError: non-boolean (Int64) used in boolean context ``` --- # Be careful with numeric comparisons of floats ``` julia> NaN > 0 false julia> NaN < 0 false julia> NaN == 0 false julia> NaN != 0 true julia> NaN != NaN true julia> 0.1 + 0.2 == 0.3 false ``` --- # Combining logical conditions with `&&` and `||` ``` julia> x = -7 -7 julia> x > 0 && x < 10 false julia> x < 0 || log(x) > 10 true ``` --- # Short-circut evaluation `&&` and `||` evaluate only as many conditions (starting from the leftmost) as is needed to determine the logical value of the whole expression. ``` julia> x = -7 -7 julia> log(x) ERROR: DomainError with -7.0: log will only return a complex result if called with a complex argument. Try log(Complex(x)). julia> x < 0 || log(x) > 10 true ``` --- # One-line conditional evaluation using `&&` and `||` The codes below use short-circut evaluation: ``` julia> x = -7 -7 julia> x < 0 && println(x^2) 49 julia> iseven(x) || println("x is odd") x is odd ``` --- # Ternary operator Julia supports the *ternary operator* borrowed from the C programming language. ``` x > 0 ? sqrt(x) : sqrt(-x) ``` is equivalent to writing: ``` if x > 0 sqrt(x) else sqrt(-x) end ``` --- # Conditional statements return a value ``` julia> x = -4.0 -4.0 julia> y = if x > 0 sqrt(x) else sqrt(-x) end 2.0 julia> y 2.0 ``` --- # Example `for` loop ``` julia> for i in [1, 2, 3] println(i, " is ", isodd(i) ? "odd" : "even") end 1 is odd 2 is even 3 is odd ``` --- # Example `while` loop ``` julia> i = 1 1 julia> while i < 4 println(i, " is ", isodd(i) ? "odd" : "even") global i += 1 end 1 is odd 2 is even 3 is odd ``` We use `global` keyword in the loop as we want to update the `i` variable which is in global scope. --- # Standard `break` and `continue` keywords are supported in loops ``` julia> i = 0 0 julia> while true global i += 1 i > 6 && break isodd(i) && continue println(i, " is even") end 2 is even 4 is even 6 is even ``` --- # Compound expression using `begin`-`end` block ``` julia> x = -7 -7 julia> x < 0 && begin println(x) x += 1 println(x) 2 * x end -7 -6 -12 ``` The value of the compound expression is the value of the last expression inside it (`-12` in our case). --- # Compound expression using semicolon `;` If your code is short you can wrap several expressions in parentheses and separate them using semicolon `;`: ``` julia> x = -7 -7 julia> x > 0 ? (println(x); x) : (x += 1; println(x); x) -5 -5 ``` --- # You can use the `function` keyword to define a function ``` julia> function times_two(x) return 2 * x end times_two (generic function with 1 method) julia> times_two(10) 20 ``` --- # Functions allow positional and keyword arguments separated by `;` with optional default values ``` julia> function compose(x, y=10; a, b=10) return x, y, a, b end compose (generic function with 2 methods) julia> compose(1, 2; a=3, b=4) (1, 2, 3, 4) julia> compose(1, 2; a=3) (1, 2, 3, 10) julia> compose(1; a=3) (1, 10, 3, 10) julia> compose(1) ERROR: UndefKeywordError: keyword argument a not assigned julia> compose(; a=3) ERROR: MethodError: no method matching g(; a=3) ``` --- # Passing arguments to functions in Julia If you pass a value to a function Julia performs a binding of the function argument name to this value. This feature is called *pass-by-sharing* and means that Julia never copies data when arguments are passed to a function. This is a behavior that you might know from Python, but is different from e.g., R, where copying of function arguments is performed. --- # Short syntax for creation simple functions You can use the assignment operator to create one-line functions: ``` julia> times_two(x) = 2 * x times_two (generic function with 1 method) julia> compose(x, y=10; a, b=10) = x, y, a, b compose (generic function with 2 methods) ``` --- # Functions are first class objects in Julia You can pass functions as arguments to other functions: ``` julia> map(times_two, [1, 2, 3]) 3-element Vector{Int64}: 2 4 6 ``` --- # You can define anonymous functions using the `->` operator ``` julia> map(x -> 2 * x, [1, 2, 3]) 3-element Vector{Int64}: 2 4 6 julia> sum(x -> x ^ 2, [1, 2, 3]) 14 ``` --- # Julia supports `do` blocks If a function takes another function as its first argument you can conveniently define it using a `do` block: ``` julia> sum([1, 2, 3]) do x println("processing ", x) return x ^ 2 end processing 1 processing 2 processing 3 14 ``` --- # The `!` character in function names Often you will see an exclamation mark (`!`) at the end of the function name, e.g., `sort!`. There is a convention that developers are recommended to add `!` at the end of the functions they create if such functions modify their arguments. ``` julia> x = [5, 1, 3, 2]; julia> sort(x); # returns a new vector; x is not changed julia> sort!(x); # changes x in-place ``` --- # Variable scoping rules in Julia (simplified) The following constructs we have learned till now create a new scope (*local scope*): * functions, anonymous functions, `do`-`end` blocks; * `for` and `while` loops. Notably the `if` blocks and the `begin`-`end` blocks do not introduce a new scope. This means that variables defined in such blocks leak out to the enclosing scope.