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.
Only when we call it for the first time, this happens.
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
.
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:
As for floating point numbers, there is an arbitrary precision integer type:
Bool is a number type in Julia:
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:
Strings
There are types for strings and characters:
Indexing in Julia
What does the following expression evaluate to:
[2, 3, 5][2]
(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:
We can use tuples to return multiple values from a function:
The elements of tuples can be accessed through decomposition and indexing:
In addition to plain tuples, there are named tuples, where every element has its own name:
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:
Also, dictionaries are not fixed in length:
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):
1-dimensional arrays are called vectors:
2-dimensional arrays are called matrices:
And any higher dimensional arrays are just called arrays:
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:
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
:
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:
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:
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.:
and
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:
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.:
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:
For example, the following prints the first few squares:
for loops
for
loops in Julia do look similar to those in
Python:
The equvalent for
loop to the previous example for a
while
loop would look like this:
<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.
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:
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:
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:
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:
Anonymous Functions
For short, one-off functions Julia implements anonymous functions:
There is also a long form:
For functions passed as a first argument to another function, there is special syntax that makes providing multiple line functions look clean:
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:
An alternate method to f(g(x))
for composition with a
concrete value is the operator |>
:
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:
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:
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
:
Next, we try to use it in main.jl
by appending
and running the file with
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
to get
Alternatively, we can import the function into the current module’s namespace:
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:
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()
:
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
:
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:
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:
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
:
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:
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
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:
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 Academy: a list of free online courses
- the Julia Exercism: free online exercises
- and last but not least, the Julia manual
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.