Julia: Fast as Fortran, easy as Python

Tech-Speak

The Package System

Julia has its own sophisticated package management system that allows you to import libraries of code from an official repository, manage personal or private group repositories, and bundle projects with their dependencies to avoid the dreaded "dependency hell" that so often besets the modern software developer. A "package" in Julia is a project of one or more files containing code with certain functions designed for export into other projects.

Here I'll concentrate on using the package system to import public libraries that add functionality. The package system is such an integral part of the Julia environment that the REPL has a special mode for working with packages, which you enter by typing a right square bracket (]). You can also use normal language commands, which is the style I cover here, because these are the commands that you can use in saved programs.

The package system is part of the standard library but must be imported before you can use it. To make it available in the REPL or in your programs, you must say using Pkg first. This is the case as well with many other parts of the included standard library that are not loaded by default, which helps the interpreter start up quickly and saves resources. For example, before you can use the linear algebra functions mentioned in the previous section, you must import them with using LinearAlgebra.

After importing the Pkg library, you can use it to download the packages you want to use in your projects but that are not yet installed on your system. Entering the command

Pkg.add("<packagename>")

will, by default, download the specified files from the official repository and install them on your system – or upgrade the package if it's already present.

The first time you import the new functions with the using command, you will experience a delay while they are precompiled, after which their names are available in the current namespace. If you prefer to keep the imported names in their own namespace, use the command import <modulename> instead, after which you must refer to the imported names as <modulename>.<function>.

You can browse more than 1,900 packages at a centralized registry [6], which contains an impressive variety of software, ranging from serious scientific and math packages to programs for automating Minecraft. However, the status of these packages varies widely: Although many of them are mature, others are experimental or in the early stages of development. Also, the recent release of the stable version of Julia contained some changes to syntax that broke many existing packages, and as of this writing, a good deal of software in the repository still will not compile.

Graphing

Most of the packages in the ecosystem serve specialized interests, but many users will eventually be interested in visualizing their results. Therefore, a closer look at some of Julia's graphing packages is likely to be of general interest. Although you have several from which to choose, the most widely used graphing package, and most flexible, is the package that you acquire with the command:

Pkg.add("Plots")

This plotting meta-system of sorts allows you to use several plotting back ends with a unified syntax and attempts, its author claims, to understand your intentions and produce the plot you want using the capabilities of the back end you specify. I'll show you how this works with some examples.

After fetching the package, you need to import it before you can use it. The command for this is simply using Plots. If you have not used Plots before, this command will initiate a precompilation, with messages on the terminal if you are using the REPL. Subsequent plot commands will be executed by the default plotting back end, which turned out in my recent tests to be GR, but the default seems to change over time and may be something else by the time you try this out.

GR is a very capable plotting system, providing fast generation of a wide variety of visualizations. To ask Plots which back end is currently selected, simply type backend() at the REPL prompt. Below are several examples showing how to use the Plots system, with the results as generated by the GR back end. The beauty of this package is that you can change the back end merely by invoking its name in lowercase, and these plot commands will all work unchanged, with the plots rendered by the back end you select.

Without changing your code, you can experiment with different back ends to find the one that produces the best output for your particular plot. You can also use, for example, the package UnicodePlots, which renders plots right to the terminal, for a quick look at your data, and switch to another back end when it's time to produce graphs for publication.

Most back ends don't come with the Plots package, which means you'll need to download and import them separately. For instance, to make terminal plotting available, download, import, and switch to the package with

Pkg.add("UnicodePlots")
using UnicodePlots
unicodeplots()

The basic command for creating a line plot from a function is:

With the Plots system activated, as described above (if you entered using UnicodePlots above, revert to Plots with using Plots before proceeding), a window should pop up resembling Figure 1; if you're using a back end different from GR, it will appear somewhat different. A considerable delay takes place before the first plot in a session appears, but subsequent plots appear more quickly. This example also serves to illustrate Julia's convenient syntax for defining an anonymous function used in the first argument; the second and third arguments define the domain along the x axis.

Figure 1: A line plot of a function using the Plots package with the GR back end.

If you execute additional plot commands, each new plot replaces the existing plot. If, instead, you want to add something to the current plot, use the version with an exclamation mark,

to add the new function to the existing plot (Figure 2).

Figure 2: Adding a curve to an existing plot.

In this way, you can incrementally build up the plot you want. When you decide to save your masterpiece in a file, use the command savefig("figure1.pdf"). This will save a PDF of the last plot you made, which is a good choice for high-resolution reproduction. Plots supports many file types, depending on the back end. GR can save plots as PNG, SVG, and PostScript, as well; specify your choice by the filename extension.

The Plots package with the GR back end supports many other types of graphs. The commands

produce the filled contour plot shown in Figure 3. To begin, create a set of equally spaced coordinates; the first command shows Julia's convenient syntax for doing that. The second command plots the contours with the two-variable version of anonymous function definition.

Figure 3: A contour plot from one line in Julia.

Multiple Dispatch

Rather than encouraging the programmer to use classes and objects to organize code, Julia combines its type system with function definition to create an organizing principle that pervades the language and its standard library. To understand how this works, you first need to learn how to define functions.

Listing 4 defines a function called addcat. In fact, as you can see, it defines it twice. Each definition uses type annotations in its list of arguments. The first version accepts two numbers (of any kind) and returns their sum. The second version accepts two strings and returns them joined together and separated by a space. When you call this function, the compiler looks at the type of arguments supplied and uses the version that is most specific to those arguments. This mechanism is called "multiple dispatch," because the version that is dispatched, or called, depends on all of the arguments supplied, rather than, for example, only the first, which is the usual way functions are dispatched in object-oriented languages.

Listing 4

Function Definition

function addcat(a::Number, b::Number)
  return a + b
end
function addcat(a::String, b::String)
  return a * " " * b
end

In this way, you can define a set of possible behaviors for your functions as wide as you need, without writing assertions or type-checking code. This method becomes even more powerful if you combine the multiple dispatch mechanism with user-defined types.

To see the entire list of versions, or "methods," defined for a function, you can type methods(<function>). If you ask, for example, for methods(sin), you will see a list of 12 methods defined for this trigonometric function, written to handle floats, complex numbers, and various types of arrays. If you want, you can also admire the list of 376 versions of the multiplication operator (*).

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus