An introduction to the Julia language, part 1


The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider accepting the trial offer on the right. Thank you for visiting LWN.net!

Julia is a young computer language aimed at serving the needs of scientists, engineers, and other practitioners of numerically intensive programming. It was first publicly released in 2012. After an intense period of language development, version 1.0 was released on August 8. The 1.0 release promises years of language stability; users can be confident that developments in the 1.x series will not break their code. This is the first part of a two-part article introducing the world of Julia. This part will introduce enough of the language syntax and constructs to allow you to begin to write simple programs. The following installment will acquaint you with the additional pieces needed to create real projects, and to make use of Julia's ecosystem.

Goals and history

The Julia project has ambitious goals. It wants the language to perform about as well as Fortran or C when running numerical algorithms, while remaining as pleasant to program in as Python. I believe the project has met these goals and is poised to see increasing adoption by numerical researchers, especially now that an official, stable release is available.

The Julia project maintains a micro-benchmark page that compares its numerical performance against both statically compiled languages (C, Fortran) and dynamically typed languages (R, Python). While it's certainly possible to argue about the relevance and fairness of particular benchmarks, the data overall supports the Julia team's contention that Julia has generally achieved parity with Fortran and C; the benchmark source code is available.

Julia began as research in computer science at MIT; its creators are Alan Edelman, Stefan Karpinski, Jeff Bezanson, and Viral Shah. These four remain active developers of the language. They, along with Keno Fischer, co-founder and CTO of Julia Computing, were kind enough to share their thoughts with us about the language. I'll be drawing on their comments later on; for now, let's get a taste of what Julia code looks like.

Getting started

To explore Julia initially, start up its standard read-eval-print loop (REPL) by typing julia at the terminal, assuming that you have installed it. You will then be able to interact with what will seem to be an interpreted language — but, behind the scenes, those commands are being compiled by a just-in-time (JIT) compiler that uses the LLVM compiler framework. This allows Julia to be interactive, while turning the code into fast, native machine instructions. However, the JIT compiler passes sometimes introduce noticeable delays at the REPL, especially when using a function for the first time.

To run a Julia program non-interactively, execute a command like:

 $ julia script.jl <args>

Julia has all the usual data structures: numbers of various types (including complex and rational numbers), multidimensional arrays, dictionaries, strings, and characters. Functions are first-class: they can be passed as arguments to other functions, can be members of arrays, and so on.

Julia embraces Unicode. Strings, which are enclosed in double quotes, are arrays of Unicode characters, which are enclosed in single quotes. The "*" operator is used for string and character concatenation. Thus 'a' and 'β' are characters, and 'aβ' is a syntax error. "a" and "β" are strings, as are "aβ", 'a' * 'β', and "a" * "β" — all evaluate to the same string.

Variable and function names can contain non-ASCII characters. This, along with Julia's clever syntax that understands numbers prepended to variables to mean multiplication, goes a long way to allowing the numerical scientist to write code that more closely resembles the compact mathematical notation of the equations that usually lie behind it.

 julia> ε₁ = 0.01 0.01 julia> ε₂ = 0.02 0.02 julia> 2ε₁ + 3ε₂ 0.08

And where does Julia come down on the age-old debate of what do about 1/2? In Fortran and Python 2, this will get you 0, since 1 and 2 are integers, and the result is rounded down to the integer 0. This was deemed inconsistent, and confusing to some, so it was changed in Python 3 to return 0.5 — which is what you get in Julia, too.

While we're on the subject of fractions, Julia can handle rational numbers, with a special syntax: 3//5 + 2//3 returns 19//15, while 3/5 + 2/3 gets you the floating-point answer 1.2666666666666666. Internally, Julia thinks of a rational number in its reduced form, so the expression 6//8 == 3//4 returns true, and numerator(6//8) returns 3.

Arrays

Arrays are enclosed in square brackets and indexed with an iterator that can contain a step value:

 julia> a = [1, 2, 3, 4, 5, 6] 6-element Array{Int64,1}: 1 2 3 4 5 6 julia> a[1:2:end] 3-element Array{Int64,1}: 1 3 5

As you can see, indexing starts at one, and the useful end index means the obvious thing. When you define a variable in the REPL, Julia replies with the type and value of the assigned data; you can suppress this output by ending your input line with a semicolon.

Since arrays are such a vital part of numerical computation, and Julia makes them easy to work with, we'll spend a bit more time with them than the other data structures.

To illustrate the syntax, we can start with a couple of 2D arrays, defined at the REPL:

 julia> a = [1 2 3; 4 5 6] 2×3 Array{Int64,2}: 1 2 3 4 5 6 julia> z = [-1 -2 -3; -4 -5 -6];

Indexing is as expected:

 julia> a[1, 2] 2

You can glue arrays together horizontally:

 julia> [a z] 2×6 Array{Int64,2}: 1 2 3 -1 -2 -3 4 5 6 -4 -5 -6

And vertically:

 julia> [a; z] 4×3 Array{Int64,2}: 1 2 3 4 5 6 -1 -2 -3 -4 -5 -6

Julia has all the usual operators for handling arrays, and linear algebra functions that work with matrices (2D arrays). The linear algebra functions are part of Julia's standard library, but need to be imported with a command like "using LinearAlgebra", which is, a detail omitted from the current documentation. The functions include such things as determinants, matrix inverses, eigenvalues and eigenvectors, many kinds of matrix factorizations, etc. Julia has not reinvented the wheel here, but wisely uses the LAPACK Fortran library of battle-tested linear algebra routines.

The extension of arithmetic operators to arrays is usually intuitive:

 julia> a + z 2×3 Array{Int64,2}: 0 0 0 0 0 0

And the numerical prepending syntax works with arrays, too:

 julia> 3a + 4z 2×3 Array{Int64,2}: -1 -2 -3 -4 -5 -6

Putting a multiplication operator between two matrices gets you matrix multiplication:

 julia> a * transpose(a) 2×2 Array{Int64,2}: 14 32 32 77

You can "broadcast" numbers to cover all the elements in an array by prepending the usual arithmetic operators with a dot:

 julia> 1 .+ a 2×3 Array{Int64,2}: 2 3 4 5 6 7

Note that the language only actually requires the dot for some operators, but not for others, such as "*" and "/". The reasons for this are arcane, and it probably makes sense to be consistent and use the dot whenever you intend broadcasting. Note also that the current version of the official documentation is incorrect in claiming that you may omit the dot from "+" and "-"; in fact, this now gives an error.

You can use the dot notation to turn any function into one that operates on each element of an array:

 julia> round.(sin.([0, π/2, π, 3π/2, 2π])) 5-element Array{Float64,1}: 0.0 1.0 0.0 -1.0 -0.0

The example above illustrates chaining two dotted functions together. The Julia compiler turns expressions like this into "fused" operations: instead of applying each function in turn to create a new array that is passed to the next function, the compiler combines the functions into a single compound function that is applied once over the array, creating a significant optimization.

You can use this dot notation with any function, including your own, to turn it into a version that operates element-wise over arrays.

Dictionaries (associative arrays) can be defined with several syntaxes. Here's one:

 julia> d1 = Dict("A"=>1, "B"=>2) Dict{String,Int64} with 2 entries: "B" => 2 "A" => 1

You may have noticed that the code snippets so far have not included any type declarations. Every value in Julia has a type, but the compiler will infer types if they are not specified. It is generally not necessary to declare types for performance, but type declarations sometimes serve other purposes, that we'll return to later. Julia has a deep and sophisticated type system, including user-defined types and C-like structs. Types can have behaviors associated with them, and can inherit behaviors from other types. The best thing about Julia's type system is that you can ignore it entirely, use just a few pieces of it, or spend weeks studying its design.

Control flow

Julia code is organized in blocks, which can indicate control flow, function definitions, and other code units. Blocks are terminated with the end keyword, and indentation is not significant. Statements are separated either with newlines or semicolons.

Julia has the typical control flow constructs; here is a while block:

 julia> i = 1; julia> while i < 5 print(i) global i = i + 1 end 1234

Notice the global keyword. Most blocks in Julia introduce a local scope for variables; without this keyword here, we would get an error about an undefined variable.

Julia has the usual if statements and for loops that use the same iterators that we introduced above for array indexing. We can also iterate over collections:

 julia> for i ∈ ['a', 'b', 'c'] println(i) end a b c

In place of the fancy math symbol in this for loop, we can use "=" or "in". If you want to use the math symbol but have no convenient way to type it, the REPL will help you: type "\in" and the TAB key, and the symbol appears; you can type many LaTeX expressions into the REPL in this way.

Development of Julia

The language is developed on GitHub, with over 700 contributors. The Julia team mentioned in their email to us that the decision to use GitHub has been particularly good for Julia, as it streamlined the process for many of their contributors, who are scientists or domain experts in various fields, rather than professional software developers.

The creators of Julia have published [PDF] a detailed “mission statement” for the language, describing their aims and motivations. A key issue that they wanted their language to solve is what they called the "two-language problem." This situation is familiar to anyone who has used Python or another dynamic language on a demanding numerical problem. To get good performance, you will wind up rewriting the numerically intensive parts of the program in C or Fortran, dealing with the interface between the two languages, and may still be disappointed in the overhead presented by calling the foreign routines from your original code.

For Python, NumPy and SciPy wrap many numerical routines, written in Fortran or C, for efficient use from that language, but you can only take advantage of this if your calculation fits the pattern of an available routine; in more general cases, where you will have to write a loop over your data, you are stuck with Python's native performance, which is orders of magnitude slower. If you switch to an alternative, faster implementation of Python, such as PyPy, the numerical libraries may not be compatible; NumPy became available for PyPy only within about the past year.

Julia solves the two-language problem by being as expressive and simple to program in as a dynamic scripting language, while having the native performance of a static, compiled language. There is no need to write numerical libraries in a second language, but C or Fortran library routines can be called using a facility that Julia has built-in. Other languages, such as Python or R, can also interoperate easily with Julia using external packages.

Documentation

There are many resources to turn to to learn the language. There is an extensive and detailed manual at Julia headquarters, and this may be a good place to start. However, although the first few chapters provide a gentle introduction, the material soon becomes dense and, at times, hard to follow, with references to concepts that are not explained until later chapters. Fortunately, there is a "learning" link at the top of the Julia home page, which takes you to a long list of videos, tutorials, books, articles, and classes both about Julia and that use Julia in teaching subjects such a numerical analysis. There is also a fairly good cheat-sheet [PDF], which was just updated for v. 1.0.

If you're coming from Python, this list of noteworthy differences between Python and Julia syntax will probably be useful.

Some of the linked tutorials are in the form of Jupyter notebooks — indeed, the name "Jupyter" is formed from "Julia", "Python", and "R", which are the three original languages supported by the interface. The Julia kernel for Jupyter was recently upgraded to support v. 1.0. Judicious sampling of a variety of documentation sources, combined with liberal experimentation, may be the best way of learning the language. Jupyter makes this experimentation more inviting for those who enjoy the web-based interface, but the REPL that comes with Julia helps a great deal in this regard by providing, for instance, TAB completion and an extensive help system invoked by simply pressing the "?" key.

Stay tuned

The next installment in this two-part series will explain how Julia is organized around the concept of "multiple dispatch". You will learn how to create functions and make elementary use of Julia's type system. We'll see how to install packages and use modules, and how to make graphs. Finally, Part 2 will briefly survey the important topics of macros and distributed computing.

Did you like this article? Please accept our trial subscription offer to be able to see more content like it and to participate in the discussion.

(Log in to post comments)