Julia at a Glance — A Language a Day, Advent Calendar Day 3/24

About this ‘A Language a Day’ Advent Calendar

Andrew Shitov. A Language a Day

This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.

Learn more where to get the book

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:

export PATH=$PATH:/Applications/Julia-1.3.app/Contents/Resources/julia/bin/

To run a program, pass its name to the interpreter:

$ julia hello-world.jl

Hello, World!

The Hello, World! program in Julia is very straightforward:

println("Hello, World!")

Variables

In Julia, you create variables when you assign a value to it:

answer = 42

Identifiers

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 string function:

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

Functions

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 function keyword:

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 return keyword.

Type annotation

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

Lambdas

Anonymous functions can be defined either with the function/end pair by omitting the name, or with a special syntactic tool:

f = x -> x ^ 2
println(f(3)) # 9

Multiple dispatch

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)[1] # range starting with 1
        a[i] += 1
    end
end

a = [10, 20, 30]
inc!(a)
println(a) # [11, 21, 31]

The for loop in the above example can be replaced with a foreach loop with lambda:

foreach(i -> a[i] += 1, 1:size(a)[1])

Composite types

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, Int8, or UInt64, or 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 keyword.

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 keyword:

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 Dog and 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.

Channels

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.

Sleep Sort

Below, the two implementations of the algorithms are given.

In both of them, a separate task is scheduled for each number using the @async macro.

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 @sync macro.

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)[1]
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

Get more

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Retype the CAPTCHA code from the image
Change the CAPTCHA codeSpeak the CAPTCHA code