About this ‘A Language a Day’ Advent Calendar 2019
Welcome to Day 3 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Julia programming language.
Facts about the language
Some facts about Julia:
- A dynamically typed language
- Compiles to LLVM
- Strong support of scientific and numeric computing (math, statistics, etc.)
- Appeared in 2012
- Website: julialang.org
Installing and running Julia
To install Julia, visit its official website and choose a package for your operating system. You may need to update your
PATH variable to point it to the installation directory:
To run a program, pass its name to the interpreter:
$ julia hello-world.jl
The Hello, World! program in Julia is very straightforward:
In Julia, you create variables when you assign a value to it:
answer = 42
An interesting feature of Julia is that you can use Unicode letters in identifiers, and, what is extremely exciting, its REPL shell allows you typing such symbols using TeX sequences. For example, to get
δ you can type
\delta and press the Tab key.
Variables can be interpolated in strings using the
$ prefix. In the following snippet, the parentheses are added to not to treat the exclamation sign as part of the variable name.
name = "Julien" println("Bonjour, $name !") println("Hello, $(name)!")
Another way to create strings out of separate parts is to use the
name = "Julia" greeting = string("Hello, ", name, "!") println(greeting)
A note about numbers
Let me make a small remark about a couple of interesting features that Julia offers to scientists. First, really cool mathematical notation when you can place a multiplier directly before the variable or an expression with no multiplication operator:
x = 21 y = 2x println(y) # 42 z = 3(1 + 2x) println(z) # 3 * (1 + 2 * x) = 129
Second, rational numbers. They keep the numerator and the denominator parts separate in memory, which allows precise calculations (they are separated by double slash in code). Compare the output:
println(0.1 + 0.2 - 0.3) # 5.551115123125783e-17 println(1//10 + 2//10 - 3//10) # 0//1
There are two ways to define a function. Here’s the first, more traditional form, which allows multiple statements in the function body:
function add(x, y) x + y end println(add(4, 5))
For one-liner functions, you can use the second variant, which does not even require the
add(x, y) = x + y println(add(4, 5))
Don’t forget that non-ASCII letters are also allowed:
Σ(x, y) = x + y println(Σ(4, 5))
To return a value earlier (for example, in a conditional instruction), use the
Function arguments can be extended with the type information after the two colons:
add(x::Int, y::Int) = x + y
Now, you cannot call this function with, say, floating-point numbers:
add(4.1, 3.2). It will be rejected at compile time.
Note that you cannot declare the type of global variables (looks like it may be changed in the future), but you can do it with local variables in a function.
function add(x::Int, y::Int) :: Int z::Int = x + y return z end
Anonymous functions can be defined either with the
end pair by omitting the name, or with a special syntactic tool:
f = x -> x ^ 2 println(f(3)) # 9
You can define more than one function that have the same name but differ in the types of accepted arguments. Consider the following two functions, one working with integers, the second one with floats.
f(x::Int) = x ^ 2 f(x::Float64) = x ^ 3 println(f(3)) println(f(3.0))
A Factorial example
Here is the complete program that prints factorial of 5:
println(factorial(5)) # 120
In Julia, this function is built-in and you don’t need to import any modules to use it.
Functions changing their arguments
There is a convention that if a function modifies its arguments, its name ends with the exclamation mark. The following example demonstrates a function that modifies the elements of an array.
function inc!(a) for i in 1:size(a) # range starting with 1 a[i] += 1 end end a = [10, 20, 30] inc!(a) println(a) # [11, 21, 31]
for loop in the above example can be replaced with a
foreach loop with lambda:
foreach(i -> a[i] += 1, 1:size(a))
The object-oriented system is not something standard. Instead, you have to deal directly with types and their hierarchy.
Among the built-in types, you find different kind of numbers, for example,
Float32. Composite types are effectively the types that contain a few data members, but there are no associated class methods. You define a structure with the
struct Person name::String age::Int32 end p = Person("Julia", 20) println("$(p.name) is $(p.age).") # Julia is 20.
The above structure is immutable, and you cannot change the values of its fields (unless they are arrays, for examples). To make data mutable, add the
mutable struct Person name::String age::Int32 end p = Person("Julia", 20) p.age += 1 # This is OK now println("$(p.name) is $(p.age).") # Julia is 21.
A Polymorphic example
Let us create a program that uses an array of dogs and cats, each being an animal, and then it loops over them to print a message depending on the actual type of a data item in hand.
We will define an abstract type
Animal, and derive two other types: structures
Cat. Inheritance is indicated by the
<: symbol. For simplicity, these types do not contain any data fields, but you may extend the example to include pet names, for example.
abstract type Animal end struct Dog <: Animal end struct Cat <: Animal end speak(::Cat) = println("Cat") speak(::Dog) = println("Dog") zoo = [Cat(); Dog(); Cat()] for x in zoo speak(x) end
As it is not possible to have what you would call class methods in other languages, you can use multiple dispatch and define the two functions
speak that behave differently depending on the type of the argument. If it is a
Cat, it prints Cat, and if the type is
Dog, it prints Dog.
Concurrency and parallelism
In Julia, they have tasks and channels (single-threaded), multi-threading (which is experimental so far), and real parallelism — both multi-core and distributed.
One of the methods to communicate between different parts of a concurrent program are channels. Let us try them out on a single-threaded program that first fills a channel with some numbers and then reads them.
ch = Channel(10) range = 1:5 foreach(n -> put!(ch, 2n), range) for _ in range println(take!(ch)) end
When you create a channel, you specify how spacious it can be: in the given example, it can keep a queue of 10 elements. You send data to the channel using the
put! function (it obviously modifies it, thus an exclamation mark). Reading is done with
take!, which waits until there is enough data in the channel.
Below, the two implementations of the algorithms are given.
In both of them, a separate task is scheduled for each number using the
In the first variant, the current number is printed directly from the task, and to wait for them to complete, the whole loop is wrapped by the
data = [10, 4, 2, 6, 2, 7, 1, 3] function sort_value(n) sleep(n / 10) println(n) end @sync for n in data @async sort_value(n) end
In the second implementation, channels are used to transfer the result. In this case, you do not need to sync the tasks, as reading from the channel will block the program until all items are passed through.
data = [10, 4, 2, 6, 2, 7, 1, 3] data_size = size(data) ch = Channel(data_size) function sort_value(n) sleep(n / 10) put!(ch, n) end for n in data @async sort_value(n) end for _ in 1:data_size n = take!(ch) println(n) end
To enjoy the language even more, I suggest you continue by taking a look at the following resources:
The source code of today’s examples are pushed to GitHub.
Next: Day 4. Kotlin