Content from Introduction


Last updated on 2024-05-21 | Edit this page

Overview

Questions

  • What can we except from this course?

Objectives

  • State what this lesson aims to teach.

This lesson is an introduction to the Julia programming language for persons who already have experience in other programming languages.

We start by placing Julia within the world of all programming languages by looking at properties usually used to classify them.

Then we go through the language types and other important constructs. We will learn about it’s syntax along the way and, with few exceptions, mostly implicitly. This part will be rather fast paced, with the assumptions that you learners will have gone through the same learning process for other languages before and draw analogies. Because of time constraint we will also only touch the surface of many topics. There is, however, extensive documentation.

Finally, we will have a look at how to idiomatically setup a Julia project, either as a library or an application.

We will teach and learn through participatory live coding, that means the instructors will write code in a Julia REPL so that you can see it. They will write slowly and will explain everything that they type, so that everytone can code along while understanding what they are doing.

REPL

REPL is an acronym for Read-Eval-Print-Loop. A REPL reads a string from the user, evaluates it in some form, prints the result of the evaluation and loops back to the start.

We we start, for example, Python in a console without arguments, we start a REPL; the prompt, >>>, waiting to read our commands in the form of strings we type.

Content from Programming Language Classification


Last updated on 2024-05-25 | Edit this page

Overview

Questions

  • How does Julia roughly compare to other programming languages?

Objectives

  • State important global properties of Julia.

In this episode we will place Julia within the world of programming languages.

High-Level


Julia is a high-level language, that is, it abstracts away the details of the hardware that runs software written in the language. Other high-level languages are, for example, Java and Python.

Dynamic


Julia is dynamically typed by default. This puts it in the company of Python and distinguishes it from Java and C++.

Even though it is dynamically typed by default, it does allow programmers to specify the type of a variable, for example for method parameters or method return types. This is mainly necessary to make use of method dispatch based on argument types. It also helps to ensure that no unintended uses of functions (seem to) work. And, in some cases, it can improve performance.

We will give it a try and define two methods, one with and one without specifying the parameter type:

JULIA

> identity(x) = x
identity (generic function with 1 method)

> identity(2.0)
2.0

> identity(2)
2

> float_identity(x::Float64) = x
float_identity (generic function with 1 method)

> float_identity(2.0)
2.0

> float_identity(2)
ERROR: MethodError: no method matching float_identity(::Int64)
Closest candidates are:
  float_identity(::Float64) at REPL[4]:1
Stacktrace:
 [1] top-level scope
   @ REPL[6]:1

Garbage Collected


Julia uses garbage collection for memory management.

Compiled


Julia is a compiled programming language. However, functions are compiled just-in-time for any given list argument types. For example, when we define a method, it does not get compiled.

JULIA

> foo(x) = x
foo (generic function with 1 method)

Only when we call it for the first time, this happens.

JULIA

> foo(3)
3

And it only compiles the function for arguments of type Int64. We can look at the compilation results for different argument types using the function code_native:

JULIA

julia> code_native(foo, (Int64,))
        .text
; ┌ @ REPL[1]:1 within `foo`
        endbr64
        movq    %rdi, %rax
        retq
        nopl    (%rax,%rax)
; └

julia> code_native(foo, (Float64,))
        .text
; ┌ @ REPL[2]:1 within `foo`
        endbr64
        movabsq $.rodata.cst8, %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopw    %cs:(%rax,%rax)
; └

Since Julia always compiles and never evaluates, the compiler is sometimes called a just-ahead-of-time compiler.

High-performance


The intention behind developing Julia was to create a fast dynamic language. And so it comes as no surprise that high-performance software can be written in Julia. The language is by design particularly well suited for dealing with (large) arrays of numbers. This makes it a good language for writing code for numerical analysis, optimisation, and many forms of machine learning.

Benchmarks on the language’s website place its performance close to that of languages like C or Rust.

With CUDA.jl there is an officially supported library that allows to write code for Nvidia GPUs directly in Julia.

Content from Types


Last updated on 2024-05-27 | Edit this page

Overview

Questions

  • What data types does Julia offer out of the box?
  • How can we define our own types?

Objectives

  • SOME OBJECTIVE

Even though Julia is by default dynamic, types play an important role. We need to specify them to make use of the method dispatch feature, where one of Julia’s distinguishing features is that it offers multiple dispatch. We also need it to write high performance code.

Method Dispatch Primer

Method dispatch is the process of choosing which method of a function (more on that in a later episode) to execute based on the argument count and argument types.

For example, adding two integers requires different code than adding to floating point numbers, and again different code than adding an integer to a floating point number.

Types and Type Hierarchy


Every value in Julia has a type. We can determine the type of a value using the function typeof.

JULIA

> typeof(1)
Int64

> typeof(2.0)
Float64

> typeof(typeof)
typeof(typeof) (singleton type of function typeof, subtype of Function)

We can see that the default type for integers as well as floats is the respective 64-bit version. We can also see that functions are types just like any other. This allows to write functional code, which is part of idiomatic Julia.

All types in Julia form a hierarchy with Any at the top. We can explore that hierarchy using the functions supertype and supertypes.

We determine the type of the value represented by the number literal 1.

JULIA

> typeof(1)
Int64

The Julia REPL offers a special variable, ans, that always holds the result of the previous evaluation. We use it with supertype to climb up the type hierarchy:

> supertype(ans)
Signed

> supertype(ans)
Integer

> supertype(ans)
Real

> supertype(ans)
Number

> supertype(ans)
Any

> supertype(ans)
Any

supertype returns Any for input Any which happens to be at the top of the type hierarchy. It is an ancestor of every data type and so every value is of type Any.

We can use supertypes to get the full inheritance chain of a type as a tuple of types.

> supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)

Number Types


Julia provides various common number types.

There are four floating point number types:

JULIA

> Float16(1.0) # This might be broken
Float16(0.0)

> Float32(1.0)
1.0f0

> Float64(1.0)
1.0

> supertypes(BigFloat)
(BigFloat, AbstractFloat, Real, Number, Any)

BigFloat is an arbitrary precision floating point number type.

And signed and unsigned integer types for 8, 16, 32, 64, and 128 bits, for example:

JULIA

> Int8(32)
32

> UInt32(42)
0x0000002a

As for floating point numbers, there is an arbitrary precision integer type:

JULIA

> supertypes(BigInt)
(BigInt, Signed, Integer, Real, Number, Any)

Bool is a number type in Julia:

JULIA

> supertypes(Bool)
(Bool, Integer, Real, Number, Any)

> false == 0
true

> 0 + true
1

There are types for complex and rational numbers that are parameterised on subtypes of Real and Integer respectively:

JULIA

> typeof(3//7)
Rational{Int64}

> Complex{Bool}(1, 0)
Complex(true, false)

> Complex(3//7, -1//5)
3//7 - 1//5*im

> im*im
-1 * 0im

Finally, Julia has a type to represent the exact values of irrational constants:

JULIA

> typeof(π)
Irrational{:π}

> typeof(pi)
Irrational{:π}

Strings


There are types for strings and characters:

JULIA

> typeof("Hello")
String

JULIA

> typeof("Hello"[3])
Char

Indexing in Julia

What does the following expression evaluate to:

[2, 3, 5][2]

In Julia indexing starts with 1 instead of the more common 0. So, for example, to get the second element of an array, one would use the index 2:

JULIA

> [2, 3, 5][2]
3

(Named) Tuples


Julia features a type for tuples, similar to tuples in C++ and Python or lists in many Lisp dialects. Tuples is a fixed length ordered container whose elements may have any type. The syntax is the same as in Python:

JULIA

> typeof((1, "a"))
Tuple{Int64, String}

> typeof((1))
Int64

> typeof((1,))
Tuple{Int64}

We can use tuples to return multiple values from a function:

JULIA

> t1() = (1, 2)
t1 (generic function with 1 method)

> t1()
(1, 2)

The elements of tuples can be accessed through decomposition and indexing:

JULIA

> a, b = t1()
(1, 2)

> a
1

> t1()[1]
1

In addition to plain tuples, there are named tuples, where every element has its own name:

JULIA

> v = (x = 1, y = -1)
(x = 1, y = -1)

> v[2]
-1

> v.y
-1

Dictionaries


Close to named tuples, but without the limitation of fixed length are dictionaries:

JULIA

> person = Dict("first name" => "Albert", "last name" => "Einstein", "age" => 44)
Dict{String, Any} with 3 entries:
  "first name" => "Albert"
  "age"        => 44
  "last name"  => "Einstein"

As seen from this example: key types can be mixed. However, unlike tuples, the order of keys is not preserved and you cannot get the entries by index:

JULIA

> person[1]
ERROR: KeyError: key 1 not found
Stacktrace:
 [1] getindex(h::Dict{String, Any}, key::Int64)
   @ Base ./dict.jl:498
 [2] top-level scope
   @ REPL[77]:1

But this works:

JULIA

> person["first name"]
"Albert"

Also, dictionaries are not fixed in length:

JULIA

> person["profession"] = "scientist"
"scientist"

> person
Dict{String, Any} with 4 entries:
  "first name" => "Albert"
  "profession" => "scientist"
  "age"        => 44
  "last name"  => "Einstein"

Arrays


Arrays and associated features are arguably one of the most important defining traits of the Julia programming language.

Julia supports arrays of arbitrary dimensions.

We have 0-dimensional arrays (scalars):

JULIA

> zeros(Int8, ()))
0-dimensional Array{Int8, 0}:
 0

1-dimensional arrays are called vectors:

JULIA

> ones(Int8, (2))
2-element Vector{Int8}:
 1
 1

2-dimensional arrays are called matrices:

JULIA

> zeros(Int8, (2, 2))
2×2 Matrix{Int8}:
 0  0
 0  0

And any higher dimensional arrays are just called arrays:

JULIA

> zeros(Int8, (2, 2, 2))
2×2×3 Array{Int8, 3}:
[:, :, 1] =
 0  0
 0  0

[:, :, 2] =
 0  0
 0  0

Initialising arrays can be done with and without comma:

JULIA

> [1, 2]
2-element Vector{Int64}:
 1
 2
> typeof([1, 2])
Vector{Int64} (alias for Array{Int64, 1})

> [1 2]
1×2 Matrix{Int64}:
 1  2
> typeof([1 2])
Matrix{Int64} (alias for Array{Int64, 2})

One is effectively the transpose of the other:

JULIA

> transpose([1 2])
2×1 transpose(::Matrix{Int64}) with eltype Int64:
 1
 2

This has further consequences for multidimensional arrays:

JULIA

# all commas
> [[0, 1], [2,3]]
2-element Vector{Vector{Int64}}:
 [0, 1]
 [2, 3]
> typeof([[0, 1], [2, 3]])
Vector{Vector{Int64}} (alias for Array{Array{Int64, 1}, 1})

# inner commas
> [[0, 1] [2, 3]]
2×2 Matrix{Int64}:
 0  2
 1  3
> typeof([[0, 1] [2, 3]])
Matrix{Int64} (alias for Array{Int64, 2})

# outer commas
> [[0 1], [2 3]]
2-element Vector{Matrix{Int64}}:
 [0 1]
 [2 3]
> typeof([[0 1], [2 3]])
Vector{Matrix{Int64}} (alias for Array{Array{Int64, 2}, 1})

# no commas
> [[0 1] [2 3]]
1×4 Matrix{Int64}:
 0  1  2  3
> typeof([[0 1] [2 3]])
Matrix{Int64} (alias for Array{Int64, 2})

# ... which is the same as this:
> [0 1 2 3]
1×4 Matrix{Int64}:
 0  1  2  3
> typeof([0 1 2 3])
Matrix{Int64} (alias for Array{Int64, 2})

# ... but is not the same as:
> [[0 1 2 3]]
1-element Vector{Matrix{Int64}}:
 [0 1 2 3]
> typeof([[0 1 2 3]])
Vector{Matrix{Int64}} (alias for Array{Array{Int64, 2}, 1})

# and finally, it is also different from this:
> [0, 1, 2, 3]
4-element Vector{Int64}:
 0
 1
 2
 3
> typeof([0, 1, 2, 3])
Vector{Int64} (alias for Array{Int64, 1})

Working with arrays

Julia supports matrix operations:

JULIA

> A = [[0, 1] [1, 0]]
2×2 Matrix{Int64}:
 0  1
 1  0

> b = [1, 2]
2-element Vector{Int64}:
 1
 2

> b+b
2-element Vector{Int64}:
 2
 4

> A-A
2×2 Matrix{Int64}:
 0  0
 0  0

> A * b
2-element Vector{Int64}:
 2
 1

> A*A
2×2 Matrix{Int64}:
 1  0
 0  1

Julia also supports a special syntax, the dot-operator, to element-wise apply functions to arrays:

JULIA

> [1, 4, 9, 16, 25] .> 7
5-element BitVector:
 0
 0
 1
 1
 1

> sqrt.([1, 4, 9, 16, 25])
5-element Vector{Float64}:
 1.0
 2.0
 3.0
 4.0
 5.0

With the @. notation we can turn every function call into an expression that is applied element-wise:

JULIA

> @. [1 1] * [1 1] + [1 1]
1×2 Matrix{Int64}:
 2  2

> [1 1] * [1 1] + [1 1]
ERROR: DimensionMismatch("matrix A has dimensions (1,2), matrix B has dimensions (1,2)")
Stacktrace:
 [1] _generic_matmatmul!(C::Matrix{Int64}, tA::Char, tB::Char, A::Matrix{Int64}, B::Matrix{Int64}, _add::LinearAlgebra.MulAddMul{true, true, Bool, Bool})
   @ LinearAlgebra /usr/share/julia/stdlib/v1.7/LinearAlgebra/src/matmul.jl:810
 [2] generic_matmatmul!(C::Matrix{Int64}, tA::Char, tB::Char, A::Matrix{Int64}, B::Matrix{Int64}, _add::LinearAlgebra.MulAddMul{true, true, Bool, Bool})
   @ LinearAlgebra /usr/share/julia/stdlib/v1.7/LinearAlgebra/src/matmul.jl:798
 [3] mul!
   @ /usr/share/julia/stdlib/v1.7/LinearAlgebra/src/matmul.jl:302 [inlined]
 [4] mul!
   @ /usr/share/julia/stdlib/v1.7/LinearAlgebra/src/matmul.jl:275 [inlined]
 [5] *(A::Matrix{Int64}, B::Matrix{Int64})
   @ LinearAlgebra /usr/share/julia/stdlib/v1.7/LinearAlgebra/src/matmul.jl:153
 [6] top-level scope
   @ REPL[2]:1

Element Types

We can determine the element types of an array type (and some other container types) using the function eltype:

JULIA

> eltype([1, 2])
Int64

Generic Array types

There are more types that behave similar to arrays. With strings we already saw one of them. Another are ranges:

JULIA

> 1:10
1:10

> typeof(ans)
UnitRange{Int64}

> eltype(ans)
Int64

> (1:10)[9]
9

> collect(1:10)
10-element Vector{Int64}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

> collect(1:3:10)
4-element Vector{Int64}:
  1
  4
  7
 10

> typeof(1:3:10)
StepRange{Int64}

> range(-0.1, 0.1, length=3)
-0.1:0.1:0.1

> typeof(range(-0.1, 0.1, length=3))
StepRangeLen{Float64, Base.TwicePrecision{Float64},
Base.TwicePrecision{Float64}, Int64}

> collect(range(-0.1, 0.1, length=3))
3-element Vector{Float64}:
 -0.1
  0.0
  0.1

Structs


Structs are what allows us to define our own types in Julia. In our first example we will created a parameterized type for 2-dimensional points:

JULIA

> struct Point{X}
         x::X
         y::X
  end

Mutability

Instances of structs are by default immutable.

JULIA

> p = Point(1, 2)
Point{Int64}(1, 2)

> p.x = 3
ERROR: setfield!: immutable struct of type Point cannot be changed
Stacktrace:
 [1] setproperty!(x::Point{Int64}, f::Symbol, v::Int64)
   @ Base ./Base.jl:43
 [2] top-level scope
   @ REPL[3]:1

Putting the keyword mutable in front of the definition changes that:

> mutable struct MPoint{X}
          x::X
          y::Y
  end

> mp = MPoint(1, 2)
MPoint{Int64}(1, 2)

> mp.x = 3
3

Constructors

We already used the default constructor of a struct. It takes as many arguments as the struct has fields and assigns them to the fields in the order they are specified.

We can also define further or alternate constructors; either as part of the struct definition, inner constructor, or separately, outer constructor.

We will start with a separate, parameterless constructor for our Point struct:

JULIA

> Point() = Point(0, 0)
Point

> Point()
Point{Int64}(0, 0)

In this case we get an additional constructor.

When we define constructors as part of the struct definition, the default constructor does not exist.

JULIA

> struct Foo
      x
      Foo() = new(0)
  end

> Foo()
Foo(0)

> Foo(1)
ERROR: MethodError: no method matching Foo(::Int64)
Closest candidates are:
  Foo() at REPL[11]:3
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

Note, that we need to use a preexisting constructor to define an outer constructor, while we use the keyword new as if we were calling the default constructor when defining inner constructors.

More Types


The Julia language provides a lot more types; for example CartesianIndex, IOStream, or Atomic{T}. We again refer to the documentation.

Content from Operators


Last updated on 2024-05-27 | Edit this page

Overview

Questions

  • What basic operators/operations does Julia define?

Objectives

  • SOME OBJECTIVE

Julia, like most other languages, defines a core set of basic arithmetic and bitwise operators for its primitive data types. Most of them are similar to most other languages, but there are some operators that might differ, like the division operator, which which is also differently defined between other languages or even language versions (like in Python 2 vs. 3).

Arithmetic Operators

The following table lists the basic arithmetic operators which are defined on all primitive numeric data types:

Expression Description Notes
-a additive inverse
a + b addition
a - b subtraction
a * b multiplication
a / b division division of integers always returns a floating point number
a \ b inverse divide same as b/a
a ÷ b integer division rounded towards zero; within Julia \div<TAB> produces the ÷ character, otherwise use div(a,b)
a % b remainder of a÷b
a ^ b a to the power of b

Julia respects the usual mathematical ordering of operators, e.g.:

JULIA

> 2+3*4
14

and

JULIA

> 3*2^2
12

Boolean Operators

Boolean operators work on the Bool type:

Expression Description
!a logical negation
a && b logical and
a || b logical or

Note that while these operations do not work on integers:

JULIA

> 1 && 0
ERROR: TypeError: non-boolean (Int64) used in boolean context
Stacktrace:
 [1] top-level scope
   @ REPL[38]:1

Bool is an Integer type and thus, boolean values can be treated as integers (false as 0 and true as 1) where necessary:

JULIA

> 1 + true
2

Bitwise Operators

Bitwise operators work on all primitive integer types:

Expression Description Notes
~a bitwise negation (not)
a & b bitwise and
a | b bitwise or
a ⊻ b bitwise xor exclusive or; type \xor<TAB>
a ⊼ b bitwise nand not and; type \nand<TAB>
a >>> b logical bitwise shift right does not preserve sign
a >> b arithmetic bitwise shift right preserves sign
a << b logical/arithmetic bitwise shift left does not necessarily preserve sign (overflow)

Assigning operators

binary arithmetic and bitwise operators can be appended by an equal sign to produce an operator that assigns the result of the operation to the left operand, e.g.:

JULIA

> a = 1
1

> a += 1
2

> a
2

Numeric Comparisons

Comparisons work like in most other programming languages, with the possible extension to allow also non-ASCII versions of some operators:

Operator Description Notes
=== equality
!= or inequality type \ne<TAB> to get
<, > less than, greater than
<= or less than or equal type \leq<TAB> to get
>= or greater than or equal type \geq<TAB> to get

Content from Loops and Conditionals


Last updated on 2024-05-28 | Edit this page

Overview

Questions

  • What basic loop constructs does Julia offer?
  • What is the syntax for conditionals in Julia?

Objectives

  • SOME OBJECTIVE

We only cover two basic loop types here: while and for loops, and both are similar to most other programming languages.

while loops

while loops in Julia look like this:

JULIA

while <condition>
    <do something>
end

For example, the following prints the first few squares:

JULIA

> n = 0
> while n < 5
      println(n^2)
      n += 1
  end

for loops

for loops in Julia do look similar to those in Python:

JULIA

for <variable> in <iterable>
    <do something>
end

The equvalent for loop to the previous example for a while loop would look like this:

JULIA

> for n in 0:4
      println(n^2)
  end

<iterable> can be anything that can be iterated over and <variable> will, one by one, take values from that iterable, e.g., an array.

Conditionals

using if

The probalbly most-used syntax for a conditional in programming uses the if keyword and Julia does the same. The general syntax of an if conditional is as follows:

JULIA

if <condition 1>
    <do something>
elseif <condition 2>
    <do something else>
else
    <do something entirely different>
end

The blocks elseif and else are optional, also as usual.

using the ternary operator

Conditionals can also be written differently in a number of other programming languages, using ternary operators, like this:

JULIA

a ? b : c

which is equivalent to

JULIA

if a
    b
else
    c
end

Note that ? binds rather strongly, so usually you will have to enclose the expression within parentheses:

JULIA

(4 > 0) ? "larger than 0" : "equal to or less than zero"

Content from Generic Functions


Last updated on 2024-05-27 | Edit this page

Overview

Questions

  • How does Julia implement functions?

Objectives

  • Julia uses generic functions and their methods to implement functions.
  • Julia implements multiple dispatch.

Julia’s functions deserve their own episodes. Julia implements generic functions. A generic function is implemented by zero or more methods. When a (generic) function is called, a method that best fits to the provided arguments is selected and executed.

Methods


In the case of Julia the method is chosen based on the number of arguments and their types.

This is used to provide implementations for specific argument types without having to litter the code with conditionals based on value types. An impressive example for the number of methods is the generic function +:

JULIA

> methods(+)
[1] +(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:87
[2] +(c::Union{UInt16, UInt32, UInt64, UInt8}, x::BigInt) in Base.GMP at gmp.jl:529
[3] +(c::Union{Int16, Int32, Int64, Int8}, x::BigInt) in Base.GMP at gmp.jl:535

Generic Methods on Steroids

For an example of a very complex generic function system you can have look at the Common Lisp Object System (CLOS). There methods can call less specific methods, there are so called before, after, and around methods, and all applicable methods can be combined in several ways (for example, summing the result of all applicable methods).

We can define a generic function without any methods:

JULIA

> function dist end
dist (generic function with 0 methods)

This is not necessary. If we provide an argument list and implementation in such a definition and the generic function did not exist before, it will exist after including the first method that will have been defined.

Multiple Dispatch

Julia uses all argument types—and not just the first—to determine, which method is most applicable. This is called multiple dispatch; in contrast to single dispatch, as Java and C++ implement it, where the called method is (during runtime) solely determined by the type of the object who’s method is called. The object is, in the form of this, effectively the first argument to the method.

JULIA

> function length(x, y)
      sqrt(x^2 + y^2)
  end
length (generic function with 1 method)

> function length(x, y::Int64)
      println("Not implemented yet")
  end
length (generic function with 2 methods)

> length(1.0, 2.0)
2.23606797749979

> length(1, 2)
Not implemented yet

More important is the number of arguments:

JULIA

> length(x) = x
length (generic function with 3 methods)

> length(1)
1

Multiple dispatch is similar to function overloading in languages like Java and C++. There the appropriate method (except for dispatch on the first argument) is determined at compile time based on the parameter types. In the case of Julia this happens at runtime and thus allows to add further methods to a generic functions in a running image of a Julia process.

Arguments


In Julia arguments are pass-by-sharing, also known as -by-pointer or -by-reference. For values of immutable types this may be optimized to pass-by-value if the values fit into a single register. But because of the immutability this is indistinguishable.

Julia also supports optional arguments:

JULIA

> with_optional(x = 1) = 1
with_optional (generic function with 2 methods)

> with_optional(3)
3

> with_optional()
1

Note that with_optional immediately has two methods:

JULIA

> methods(with_optional)
# 2 methods for generic function "with_optional":
[1] with_optional() in Main at REPL[1]:1
[2] with_optional(x) in Main at REPL[1]:1

This shows that optional arguments are implemented through multiple methods of a generic function with different parameter lists. In fact, with_optional() is defined as = with_optional(1). Thus, changing the definition of with_optional(x) will change the behavior of with_optional():

JULIA

> with_optional(x) = x + x
with_optional (generic function with 2 methods)

> with_optional()
2

Julia also supports keyword arguments:

JULIA

> with_keyword(;x) = x
with_keyword (generic function with 1 method)

> with_keyword(x = 1)
1

Keyword parameters are defined after the other parameters separated by ;. They can have default values as well. Keyword parameters are not involved in method dispatch.

Another, less common feature, is destructuring for tuple parameters:

JULIA

> destructured((a, b)) = a + b
destructured (generic function with 1 method)

> t = (1, 2)
(1, 2)

> destructured(t)
3

Note that any additional elements of a tuple with be silently ignored, but we get an error, when the tuple is too short:

JULIA

> destructured((1, 2, 3))
3

> destructured((1,))
ERROR: BoundsError: attempt to access Tuple{Int64} at index [2]
Stacktrace:
 [1] indexed_iterate
   @ ./tuple.jl:89 [inlined]
 [2] destructured(::Tuple{Int64})
   @ Main ./REPL[1]:1
 [3] top-level scope
   @ REPL[5]:1

Anonymous Functions


For short, one-off functions Julia implements anonymous functions:

JULIA

> map(x -> x + 1, [1, 2, ])
3-element Vector{Int64}:
 2
 3

There is also a long form:

JULIA

> (function(x)
       x
   end)(3)
3

For functions passed as a first argument to another function, there is special syntax that makes providing multiple line functions look clean:

JULIA

> map([1, 2, 3]) do x
      if x == 1
         println("one")
      else
         println("not one")
      end
  end
one
not one
not one
3-element Vector{Nothing}:
 nothing
 nothing
 nothing

Function Composition


Julia has an operator, , for function composition. You can write it in the Julia-REPL typing \circ<tab>. It is useful to create function arguments that are just compositions of other functions:

JULIA

> map(sqrt  sum, [[1, 1], [2, 2]])
2-element Vector{Float64}:
 1.4142135623730951
 2.0

An alternate method to f(g(x)) for composition with a concrete value is the operator |>:

JULIA

> [2, 2] |> sum |> sqrt
2.0

Content from Modules


Last updated on 2024-05-27 | Edit this page

Julia organizes code into modules and packages. A package is the distribution unit. Libraries and applications are usually distributed as packages. Modules are for organizing code within a package.

In this episode we will learn about modules. Modules are used for namespace management.

A module starts with the keyword module, a name end ends with end. Let us open a file called main.jl in an empty directory and add the following code:

JULIA

module OurVectors

end

Unlike in Python, a file can contain multiple modules, so module name and file name do not have to match.

But, for this example, we will put the actual contents of our module in another file. We create an empty file vectors.jl in the same folder and include it using the include function in our module, so that main.jl will look as follows:

JULIA

module OurVectors

include("vectors.jl")

end

When main.jl is loaded in any way all the code in vectors.jl will be evaluated in the scope of the OurVectors module. Code of a module can be split into several files this way.

Also note, that it is idiomatic to not indent the code in a module relative to the module statement. Module names are usually set in upper camel-case.

We will now add a function to vectors.jl:

JULIA

length(v) = sqrt(sum(v.^2))

Next, we try to use it in main.jl by appending

JULIA

println(length([3]))

and running the file with

BASH

$ julia main.jl
1

We expected 3 but got 1. Julia calls the standard library function length instead of our module’s function. To use our function outside the module, we need to first export it from the module and then use it outside the module:

JULIA

module OurVectors

export length

include("vectors.jl")

end

using .OurVectors

println(length([3]))

We need to add the . in front of the module name, because otherwise Julia would look for a package named OurVectors which it would not find. The . tells it to look for a module relative to the current one.

We put the export keyword at the top of the module, so that it is the first information a user of the module sees.

Running the example, we get

BASH

$ julia main.jl
WARNING: both OurVectors and Base export "length"; uses of it in module Main must be qualified
ERROR: LoadError: UndefVarError: length not defined
Stacktrace:
 [1] top-level scope
   @ ~/temp/main.jl:11
in expression starting at /home/user/project/main.jl:11

Base is a module provided by Julia and Base.length is the function that returns the element count of various container and iterator types. This conflicts with us using the exported length of our module. We have to write

JULIA

println(OurVectors.length([3]))

to get

JULIA

$ julia main.jl
3.0

Alternatively, we can import the function into the current module’s namespace:

JULIA



import .OurVectors: length

println(length([3]))

This, of course, is very unsafe, since it overrides a standard library function. Instead we could give it a new name in the current module:

JULIA



import .OurVectors: length as vlength

println(vlength([3]))

All this shows that each module has its own namespace, but we can import names directly or with a new name from other modules.

We can force users to always use our length prefixed by the module name OurVectors by not exporting it. It is still accessible, but it can no longer be imported by name or brought into another namespace via using.

The difference between using and import is that, by default, using make the module itself and all exported names available in the importing namespace, whereas import only makes the module available. From these defaults, using can be modified to only import specific names (including the module itself) and import can be modified to import additional names. In both cases the imported entities can be renamed using the keyword as.

Bare modules and standard modules


Whenever we create a module, the modules Core and Base are automatically contained as if imported with the using keyword. If a module is declared with the baremodule keyword instead, they are not contained.

In addition to Core and Base a Julia distribution comes with further standard modules; among them Test. In addition, Base has several submodules, e.g. Base.Threads.

Main is the top level module of a Julia process.

Content from Packages


Last updated on 2024-05-27 | Edit this page

While the module is an organizational unit within a project, packages are the distribution units of Julia.

Dependency Management


To install a package, we type ] in the Julia REPL to switch to package mode. The prompt changes from julia> to (@v1.10) pkg>. We can exit package mode by pressing C-c C-c or <Backspace>.

Pkg.jl

Package mode is implemented by the Julia package Pkg.jl. You can find Pkg.jl’s documentation online.

The prompt in the package mode tells us which environment is currently active (@v1.10). The default environment is named after the running Julia version. We can install packages into this environment for general experimentation. Our projects will have their own environments.

First, let us have a look at the status of the current environment:

(@v1.10) pkg> status
Status `~/.julia/environments/v1.10/Project.toml` (empty project)

This shows us the location of the Project.toml that describes the environment. It also lists explicitly installed packages, in our case currently none.

To install a package we run the command add:

(@v1.10) pkg> add Memoize
    Updating registry at `~/.julia/registries/General.toml`
    Installed Memoize ---- v0.4.4
    Updating `~/.julia/environments/v1.10/Project.toml`
  [c03570c3] + Memoize v0.4.4
    Updating `~/.julia/environments/v1.10/Manifest.toml`
  [1914dd2f] + MacroTools v0.5.13
  [c03570c3] + Memoize v0.4.4
  [2a0f44e3] + Base64
  [d6f4376e] + Markdown
  [9a3f8284] + Random
  [ea8e919c] + SHA v0.7.0
Precompiling project...
   2 dependencies successfully precompiled in 2 seconds
(@v1.10) pkg> status
Status `~/.julia/environments/v1.10/Project.toml`
    [c03570c3] Memoize v0.4.4

The command updates two files, Project.toml and Manifest.toml, and seems to install six packages, five of those dependencies of Memoize. Now status shows us that we have installed Memoize (but tells us nothing about its dependencies).

We can update a package using the update command:

(@v1.10) pkg> update Memoize
n    Updating registry at `~/.julia/registries/General.toml`
  No Changes to `~/.julia/environments/v1.10/Project.toml`
  No Changes to `~/.julia/environments/v1.10/Manifest.toml`

And we can remove installed packages again with rm:

(@v1.10) pkg> rm Memoize
    Updating `~/.julia/environments/v1.10/Project.toml`
  [c03570c3] - Memoize v0.4.4
    Updating `~/.julia/environments/v1.10/Manifest.toml`
  [1914dd2f] - MacroTools v0.5.13
  [c03570c3] - Memoize v0.4.4
  [2a0f44e3] - Base64
  [d6f4376e] - Markdown
  [9a3f8284] - Random
  [ea8e919c] - SHA v0.7.0

To import a package, we need to know what packages there are. There is a general package registry which can be searched through multiple front-ends, for example https://juliahub.com/ui/Packages .

If we want to install a package that is not in the general (or any other) registry, we can point to a git source instead:

(@v1.10) pkg> add https://github.com/JuliaLang/Example.jl#master
     Cloning git-repo `https://github.com/JuliaLang/Example.jl`
    Updating git-repo `https://github.com/JuliaLang/Example.jl`
   Resolving package versions...
    Updating `~/.julia/environments/v1.10/Project.toml`
  [7876af07] + Example v0.5.4 `https://github.com/JuliaLang/Example.jl#master`
    Updating `~/.julia/environments/v1.10/Manifest.toml`
  [7876af07] + Example v0.5.4 `https://github.com/JuliaLang/Example.jl#master`
Precompiling project...
   1 dependency successfully precompiled in 0 seconds

All commands discussed here have a lot more options. Particularly important might be the ability to install specific versions packages or branches of repositories containing packages. The shell provides documentation for every command when running help <command>, for example:

(@v1.10) pkg> help update
  [up|update] [-p|--project]  [opts] pkg[=uuid] [@version] ...
  [up|update] [-m|--manifest] [opts] pkg[=uuid] [@version] ...

  opts: --major | --minor | --patch | --fixed
        --preserve=<all/direct/none>

  Update pkg within the constraints of the indicated version specifications. These
  specifications are of the form @1, @1.2 or @1.2.3, allowing any version with a
  prefix that matches, or ranges thereof, such as @1.2-3.4.5. In --project mode,
  package specifications only match project packages, while in --manifest mode they
  match any manifest package. Bound level options force the following packages to
  be upgraded only within the current major, minor, patch version; if the --fixed
  upgrade level is given, then the following packages will not be upgraded at all.

  After any package updates the project will be precompiled. For more information
  see pkg> ?precompile.

Creating Packages


After seeing how to use a package, we will have a look into creating one.

Using the shell in the Julia REPL, activated by pressing ;, we navigate to the folder in which we want to create our project. For example

shell> cd ~/temp/
/home/user/temp

We leave the shell again by pressing C-c and check that we are in the correct directory, by running pwd():

JULIA

> pwd()
"/home/user/temp"

Now, we go into package mode (press ]) and generate a Julia package skeleton:

(@v1.10) pkg> generate OurVectors
  Generating  project OurVectors:
    OurVectors/Project.toml
    OurVectors/src/OurVectors.jl

The output tells us that two files were generated in a new directory with the project name that we provided. We leave the package mode again, to navigate Julia into the directory that was just created by pressing C-c:

JULIA

> cd("OurVectors")

> pwd()
"/home/user/temp/OurVectors"

We will have a look at what was created in the shell (press ;):

shell> ls -RF
.:
Project.toml  src/

./src:
OurVectors.jl

Julia generated two files for us. Project.toml is the project configuration file in TOML Format, while OurVectors.jl in the subdirectory src is an initial source file.

Let us first look at the configuration file:

shell> cat Project.toml
name = "OurVectors"
uuid = "53fd6d90-f326-4ab3-8600-de8f4fd33617"
authors = ["Some One <some.one@example.com>"]
version = "0.1.0"

It first states the name of the project (or package). A UUID randomly generated for the project follows. The authors field is based on your git configuration, falling back to a series of environment variables. Finally the initial version is given as 0.1.0.

The generated source file contains a module with an “Hello World” implementation:

shell> cat src/OurVectors.jl
module OurVectors

greet() = print("Hello World!")

end # module OurVectors

Tests


We edit the source file to contain our length function:

JULIA

module OurVectors

length(v) = sqrt(sum(v.^2))

end # module

Now, we want to write tests for our function. Idiomatically, tests are run by evaluating the file test/runtests.jl of a Julia project. We create such a file and make use of the Test package that comes bundled with Julia:

JULIA

using Test
using OurVectors

@test OurVectors.length([1]) == 1.0
@test OurVectors.length([3, 4]) == 5.0
@test_throws MethodError OurVectors.length([])

To run the script we can use the package mode again. But before switching to it (by pressing ]), we make sure that the working directory of our REPL is the project directory:

JULIA

> pwd()
"/home/user/temp/OurVectors"

This looks good. If we were in the wrong directory, we would use the function cd to change that.

In package mode, we first have to make our project the current environment. To do that, we run

(@v1.10) pkg> activate .
  Activating project at `~/temp/OurVectors`

(OurVectors) pkg>

Note, how the prompt changes when switching environments.

To run tests, we can type test in package mode:

(OurVectors) pkg> test
     Testing OurVectors
      Status `/tmp/jl_Bx3UOG/Project.toml`
  [53fd6d90] OurVectors v0.1.0 `~/temp/OurVectors`
      Status `/tmp/jl_Bx3UOG/Manifest.toml`
  [53fd6d90] OurVectors v0.1.0 `~/temp/OurVectors`
     Testing Running tests...
ERROR: LoadError: ArgumentError: Package Test not found in current path:
- Run `import Pkg; Pkg.add("Test")` to install the Test package.

Stacktrace:
 [1] include(fname::String)
   @ Base.MainInclude ./client.jl:489
 [2] top-level scope
   @ none:6
in expression starting at /home/user/temp/OurVectors/test/runtests.jl:1
ERROR: Package OurVectors errored during testing

This throws an error. Even though Test comes with the Julia installation, we still need to install it into our environment:

(OurVectors) pkg> add Test
   Resolving package versions...
    Updating `~/temp/OurVectors/Project.toml`
  [8dfed614] + Test
    Updating `~/temp/OurVectors/Manifest.toml`
  [2a0f44e3] + Base64
  [b77e0a4c] + InteractiveUtils
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [9a3f8284] + Random
  [ea8e919c] + SHA v0.7.0
  [9e88b42a] + Serialization
  [8dfed614] + Test

This installs Test and its dependencies. It also seems to make changes to Project.toml and a yet unknown file called Manifest.toml. We will have a look at them in a minute. But first, we try to again run the tests:

(OurVectors) pkg> test
     Testing OurVectors
      Status `/tmp/jl_3CRrFm/Project.toml`
  [53fd6d90] OurVectors v0.1.0 `~/temp/OurVectors`
  [8dfed614] Test `@stdlib/Test`
      Status `/tmp/jl_3CRrFm/Manifest.toml`
  [53fd6d90] OurVectors v0.1.0 `~/temp/OurVectors`
  [2a0f44e3] Base64 `@stdlib/Base64`
  [b77e0a4c] InteractiveUtils `@stdlib/InteractiveUtils`
  [56ddb016] Logging `@stdlib/Logging`
  [d6f4376e] Markdown `@stdlib/Markdown`
  [9a3f8284] Random `@stdlib/Random`
  [ea8e919c] SHA v0.7.0 `@stdlib/SHA`
  [9e88b42a] Serialization `@stdlib/Serialization`
  [8dfed614] Test `@stdlib/Test`
     Testing Running tests...
     Testing OurVectors tests passed

Now the test suite runs and our tests passed.

Tracking Dependencies


Getting back to the file that changed, when we add Test to the project, we first have look at Project.toml:

TOML


[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

The file got a new section, [deps], listing dependencies. We added Test, so it shows up in the list with its UUID.

Now for the content of the new file, Manifest.toml:

TOML

# This file is machine-generated - editing it directly is not advised

julia_version = "1.9.0"
manifest_format = "2.0"
project_hash = "71d91126b5a1fb1020e1098d9d492de2a4438fd2"

[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[deps.Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"

[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

This file lists not only the explicitly installed dependencies, but the whole dependency tree.

When we added the Test package, we did not specify a version. If we had done that, this would be recorded in Manifest.toml.

Content from Foreign Function Interface


Last updated on 2024-05-27 | Edit this page

Julia has built-in functions and data types to interface with C and Fortran code. There are also libraries to interface with Python and R. We will have a look at the interfaces for C and Python.

Before we continue, we return to the default environment in our Julia REPL:

(OurVectors) pkg> activate
  Activating project at `~/.julia/environments/v1.10`

C Interface


To call a C function we can either use the function ccall or the macro @ccall. For example:

JULIA

> ccall(:getenv, Cstring, (Cstring,), "LANG")
Cstring(0x00007ffe2e3eddd9)

> unsafe_string(ans)
"en_US.UTF-8"

ccall takes the function name, followed by the return type, a tuple of argument types, and finally the arguments themselves. We can see, that there are special types defined to interface with C, e.g. Cstring.

The macro @ccall offers an alternate syntax:

JULIA

> @ccall getenv("LANG"::Cstring)::Cstring
CString(0x00007ffe2e3eddd9)

The type annotations are found at the arguments and after the argument list for the return type, maybe making the intent a little clearer.

Other than C standard library functions, we can also call any function found on the path:

JULIA

> ccall((:g_basename, "libglib-2.0"), Cstring, (Cstring,), "/usr/bin/bash")
Cstring(0x00007f7173d0a681)

> unsafe_string(ans)
"bash"

Julia will automatically try to load libglib-2.0 in this case.

With the macro @ccall, the same call would look as follows

JULIA

> @ccall "libglib-2.0".g_basename("/usr/bin/bash"::Cstring)::Cstring
Cstring(0x00007f26ff113e81)

PyCall


To interface with python we can use the PyCall package. First, we need to install it to the current repository:

(@v1.10) pkg> add PyCall
    Installed PrecompileTools ----- v1.2.1
     Updating `~/.julia/environments/v1.10/Project.toml`
  [438e738f] + PyCall v1.96.4
    Updating `~/.julia/environments/v1.10/Manifest.toml`
  [8f4d0f93] + Conda v1.10.0
  …

pThen we can import any installed python library:

JULIA

> using PyCall

> datetime = pyimport("datetime")
PyObject <module 'datetime' from '/usr/lib/python3.10/datetime.py'>

> datetime.MAXYEAR
9999

Python Environments

We can control the version of Python that is used by Julia with the environment variable PYTHON in the Julia process.

We could, for example, start the Julia process from a shell in which we activated a particular environment of the venv Python package. This would give the Julia process access to that environments Python version and installed packages.

We can also execute Python code, through a special syntax:

JULIA

> py"[x for x in [1, 2, 3] if x % 2 == 1]"
2-element Vector{Int64}:
 1
 3

> py"""
import datetime

print(datetime.datetime.now())
"""
2022-06-03 14:38:05.903444

RCall


A similar package exists for R with RCall. It even gives access to an R-REPL from within the Julia REPL, similar to package and shell mode.

Content from Further Steps


Last updated on 2024-05-27 | Edit this page

Julia provides comprehensive documentation as well as examples.

One of the likely best entry points is Getting Started with Julia, which links to

The Julia ecosystem already provides over 10,000 additional packages, for all kinds of purposes and from all kinds of research. There are so many in fact, that it sometimes is best to search the database by topic.