Home Julia in 15 Minutes
Post
Cancel
Julia programming language

Julia in 15 Minutes

Julia is a fast and elegant programming language. In this post, we record some of its syntax, and we talk about advanced topics like types, methods and metaprogramming with real examples. We also discuss some challenges that new users face for this language.

In scientific computing, researchers need to use computers to simulate large and complex systems, from interaction of molecules to modeling of climate and the universe. This is distinct from data analytics or econometrics, where one often deals with small amount of data. Although Python is easy to use, it is too slow for such purposes that require a huge amount of computation. Using Python to simulate such large systems is often impractical. On the other hand, lower level languages like C/C++, while being fast, is very difficult to learn and master. So the two language problem exists for the community for a long time. There is often no available tool for cutting edge research problems. Julia was born in this context. It is as fast as C, while as easy as MATLAB/Python.

In this post, we briefly introduce the Julia syntax, highlighting its unique aspects. The introduction is referenced from the official documentation.

To install Julia, just go to official website and download it. After installation, you can open your shell, type julia and start the interactive shell (also called “REPL”, a shorthand for “read–eval–print loop”). To install a package, press ] to enter package mode, then type add [pkg name]. Press Ctrl+C to quit the package mode. The recommended IDE is VS Code, with the Julia extension. Alternatively, if you are used to Jupyter Lab, you can press ] and add IJulia to install the Julia kernel for Jupyter. After that, open Jupyer Lab and you should see that Julia notebooks can now be created.

Basic Syntax

Variables

Different from Python, some Unicode characters are allowed for variable names. You can use latex in Julia to name variables!

1
2
julia> σ = 1.0
1.0

The above sigma symbol is typed with \sigma followed by a tab.

Below is a list of naming conventions from official docs.

Julia Types Figure 1: variable naming conventions in Julia

Strings

Strings should be double quoted in Julia (Single quoted expressions belong to Char type which is something different than strings).

In Julia, the multiplication symbol * is used for string concatenation, as opposed to + in Python.

1
2
julia> "foo" * "bar"
"foobar"

Use dollar sign for interpolation:

1
2
3
4
julia> name = "Josh"
"Josh"
julia> "His name is $name"
"His name is Josh"

Numbers

Rational numbers are constructed using the // operator:

1
2
julia> 4//8
1//2

Complex number $i$ is represented using im.

1
2
julia> (1 + 2im)*(2 - 3im)
8 + 1im

Operators

^ is the power operator, x^3 means raise x to the 3rd power, just as in latex. Other operators are similar to those in Python.

Boolean operators:

ExpressionName
!xnegation
x && yand
x || yor

[❗] The Vectorized “dot” operators is a unique feature in Julia. Prefix an operator with a dot turns that operator into an elementwise operator that can be applied to each element of an array. For example, [1,2,3] .^ 3 means [1^3, 2^3, 3^3]. Equivalently, we can put @. in front of an expression to perform elementwise computation.

1
2
3
4
5
6
7
8
9
10
julia> x = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3
julia> @. x^2 + sin(x)
3-element Vector{Float64}:
 1.8414709848078965
 4.909297426825682
 9.141120008059866

Functions

Functions can be defined using the function keyword, enclosed by an end. Conveniently, We can also use the = sign to define functions.

1
2
3
4
5
6
7
8
9
10
julia> f(x) = x^3 - 2x + 7
f (generic function with 1 method)

julia> f(5)
122

julia> function f(x, y)
          cos(x) + y
       end
f (generic function with 2 methods)

Control Flows

As in many other languages, there are conditional evaluation if-elseif-else, ternary operator ?:, while and for loops, and some other control flows. Note that in Julia if blocks also return a value.

1
2
3
4
5
6
7
if x < y
    "x is less than y"
elseif x > y
    "x is greater than y"
else
    "x is equal to y"
end
1
2
3
julia> x = 1; y = 5;
julia> println(x < y ? "less than" : "not less than")
less than
1
2
3
4
5
6
7
8
9
10
11
12
13
14
julia> for i = 1:5
           println(i)
       end
1
2
3
4
5
julia> for s  ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

Types

Type declaration is very important for Julia, in distinct contrast with Python. You may already see a lot of them with the :: operator in Julia code. As a real example, let’s see how the convolutional layer is defined in Flux.jl: Julia Types Figure 2: types in Julia

Although Julia’s type system is dynamic, by explicitly telling the compiler the types of your variables or expressions, program execution can speed up. Another reason for doing so is to take advantage of Julia’s multiple-dispatch mechanism, i.e. invoking different function definitions for different argument types. This is a very important reason why Julia is fast.

Types are organized in a type graph, with concrete types like Float64 as leaves and more abstract types as nodes above the leaves. The <: operator declares that the left side is a subtype of the right side, e.g. Real <: Number.

A type union is an abstract type that includes as objects all instances of any of its argument types, constructed using the special Union keyword. For example, any type T that is either a subtype of Tuple, or NamedTuple, or AbstractVector, is a subtype of Union{Tuple, NamedTuple, AbstractVector}.

Julia struct Figure 3: definition of Chain in Flux.jl

A composite type is a collection of types. It is declared with the struct keyword.

Julia struct Figure 4: struct in Julia. This is the struct returned in the first function in Figure 2 above.

We can also give a parameter to types. This makes type system more flexible and powerful.

1
2
3
4
struct Point{T}
    x::T
    y::T
end

T can be anything: Float64, Int etc. Why stop at one parameter? This

1
2
3
4
struct Tuple2{A,B}
    a::A
    b::B
end

makes the struct more like an abstraction of the arguments of a function.

Methods

In Julia, we can make different definitions of a function for different argument types, all under the same function name. A definition of one possible behavior for a function is called a method. Figure 2 above is a such example.

Just like struct, method definition can also be parametric.

1
f(x::T, y::T) where {T} =  # do something

The above means the method is only defined to do something when all of its argument have the same type T.

We see from Figure 2 that methods and types are not so distinct from each other. After we defined a type, we can add methods to that type, using the exact same name. We can further make the type callable.

Julia functor Figure 5: make the convolutional layer callable on input data

Metaprogramming

In your previous programming experience, have you encountered a situation where you have the need to programmatically generate code? To make an example, suppose you need to create a large number of variables a1=2, a2=4, a3=6 …… a100=200 at once, all with some specific values. How do you do that? You may wish to have something like this (in pseudocode)

1
2
3
for i = 1 to 100
    assign a{i} = i * 2
end

Metaprogramming is about programing a program. Without metaprogramming, we might have to write many repetitive code, doing a lot of copy and paste. Metaprogramming is a central feature in Julia. It enpowers the development process.

Every program is just a piece of text (string). We can turn a string into an expression, and manipulate that expression. There are many ways to do the conversion. Take a + b as an example:

  • Meta.parse("a + b")
  • :(a + b)
  • 1
    2
    3
    
     quote 
         a + b
     end
    
  • Expr(:call, :+, a, b)

An expression has two parts: head and its arguments.

1
2
3
4
5
6
7
8
9
10
11
julia> ex = :(a + b)
:(a + b)

julia> ex.head
:call

julia> ex.args
3-element Vector{Any}:
 :+
 :a
 :b

In this example, all of them are Symbol type, denoted with : at the front. Symbols can also be constructed.

1
2
julia> Symbol("a", 99)
:a99

To manipulate expression, we can interpolate values into an expression with the dollar sign:

1
2
3
4
julia> a = 335;

julia> ex = :($a + b)
:(335 + b)

To evaluate an expression, we use the eval function.

1
2
3
julia> a = 70; b = 50;
julia> eval(ex)  # recall that ex = :(a + b)
120

So, the solution to the problem proposed at the beginning of this subsection would be

1
2
3
for i = 1:100
    eval(Meta.parse("a$i = $i * 2"))
end

To directly manipulate and run Julia code without resorting to eval every time, we use macros. Macro statements start with an @ sign. It is another characteristic feature of Julia.

1
2
3
4
5
6
7
julia> macro sayhello(name)
           return :(println("Hello, ", $name))
       end
@sayhello (macro with 1 method)

julia> @sayhello "Josh"
Hello, Josh

A simplified definition of the @assert macro that appeared in Figure 2:

1
2
3
4
5
6
7
8
9
julia> macro assert(ex)
           return :( $ex ? nothing : throw(AssertionError($(string(ex)))) )
       end
@assert (macro with 1 method)

julia> @assert 1 == 1.0

julia> @assert 1 == 0
ERROR: AssertionError: 1 == 0

In Figure 5, @functor Conv means Functors.jl is allowed to look into the fields of the instances of the Conv struct and modify them. The purpose of Functors.jl is to easily operate over all neural network model parameters at once.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
julia> using Functors

julia> struct Foo
         x
         y
       end

julia> @functor Foo

julia> model = Foo(5, [1, 1, 1])
Foo(5, [1, 1, 1])

julia> fmap(float, model)
Foo(5.0, [1.0, 1.0, 1.0])

The term “functor” comes from category theory in pure mathematics. A category consists of a class of objects as well as functions between those objects. For example, Top is the category of topological spaces and continuous functions between topological spaces. A functor

\[F: C\to D\]

is a mapping from category $C$ to category $D$ such that relations of functions in $C$ are preserved into mapped functions in $D$. Specifically, for any two functions $f$ and $g$ in $C$, we have $F(g \circ f) = F(g) \circ F(f)$ in $D$. See here for full definition. The subject arises from the study of algebraic topology, where topological spaces are studied by mapping them to algebraic structures like groups.

When marked with @functor, fmap can change “leaves” of the more abstract convolutional layer type, like its numerical type for weights. So the analogy is that, the convolutional layer now acts like a functor that maps from one numerical type (like a “category”) to another type (like another “category”), while preserving its own (convolutional layer) type structure (i.e. being consistent for different types).

Perspectives

Julia is a very promising language, but as of 2022, it is still not mature enough and it is still too early to say if it will become the language of choice for data science. Many people immediately fall in love with it after first encounter, while others find it hard to make shift to this new language. Here we discuss some challenges for users coming from languages like Python.

First, let’s be clear, Julia is not really object oriented, and it is NOT MEANT to be so. You may be used to something like np.mean(...) or nn.Linear(...) in Python. After all, such OOP patterns are “well behaved” and “predictable”, in the sense that methods and data are all bundled under classes. But in Julia, you import packages with using XXX and after that you can just call functions in that package; there is no “classes with dots” thing. It leans more toward functional programming. At first, you may think it is not as well organized as an OOP language. But if you think about it, models in data science, and mathematics in general, are not related to OOP in any intrinsic way. A neural network model is a function, taking inputs and produce outputs. It is actually more natural to implement it as a function, rather than as some classes. It may be more annoying to write something like class ...., super(..., self).__init__() every time you implement a model, or something like np.linspace for making a plot. In Julia, you focus on your goal with less distractions from something like OOP that is not directly related to it: to implement models and perform calculations.

Second, Julia leaves many with the impression that its syntax is full of symbols (!, ., @, $…) that makes the code hard to read. Having to explicitly declare types with double colon sign :: is also frawned upon by new users from Python. Indeed, the ease of learning for Python greatly lowered the entry bar for programming, and it is one reason why data science and machine learning have become so popular in recent years. Many people with no previous computer science and math training are now able to learn a programming language and find relevant jobs. This greatly expanded the Python community and brought more funding and package development and everything. Nowadays with the Python ecosystem, people really don’t have to worry about lower level details like types when they do machine learning. Julia seems to counter this trend. But let’s not ignore the fact that, as the field of data science grows, models are becoming larger and larger. We need large capacities to process more data than before. Speed is becoming more and more important, and it is even likely to become a crucial factor in applications and research alike. We can say that, for languages that are fast, Julia is the easiest to learn and use.

Summary

In the post we introduced the Julia syntax and many of its unique features. Multiple dispatch is its most distinct feature than other languages. It will take some time to see whether Julia can become the dominant language of choice for deep learning.


Cite as:

1
2
3
4
5
6
7
@article{lifei2022julia,
  title   = "Julia in 15 Minutes",
  author  = "Li, Fei",
  journal = "https://lifeitech.github.io",
  year    = "2022",
  url     = "https://lifeitech.github.io/posts/julia/"
}
This post is licensed under CC BY 4.0 by the author.
Contents

The REINFORCE Algorithm

Google's three big data technologies: GFS, Bigtable and MapReduce

Comments powered by Disqus.