Jump to content

Introducing Julia/print

0% developed
From Wikibooks, open books for an open world
Introducing Julia

Introduction

[edit | edit source]

The Julia programming language is easy to use, fast, and powerful. This wikibook is intended as an introduction to the language for the less experienced and occasional programmer. For more learning materials, including links to books, videos, articles/blogs and notebooks, refer to the learning section at Julia's official site.

The official Julia documentation is the authoritative guide, and you should refer to it as often as possible as you learn. It's the "reference" guide both for the langugage itself and for the set of standard packages (the "standard library") that are provided as part of the basic installation.

A feature of Julia is the extensive use of add-on packages to add functionality and features, and to extend the syntax of built-in functions. Good places to look for packages (which are mostly free to download from github.com) include the JuliaHub and the Julia Packages sites. Packages provide their own documentation and many provide extensive tutorials.

The Julia community has established a good ethos of encouraging participation in the development of the language on github. The advantage of this wikibook is that it's made and edited by the Julia community – you can edit anything at any time. If you find something that's wrong, or unclear, feel free to correct it, or add examples. (Your first few edits are reviewed, just in case you have less than good intentions. And, as with the Wikipedia, you should expect your writing to be edited by others!) The focus should be largely on the new user, rather than the computer science expert.

[edit | edit source]

Getting Started

[edit | edit source]
Previous page
Contents
Introducing Julia Next page
The REPL
Getting Started

Getting started

[edit | edit source]

To install Julia on your computer, visit http://julialang.org/downloads/ and follow instructions. You can then run the Julia interpreter using a terminal app on your computer. This is known as using the REPL.

Alternatively, you can use Julia online, in your browser, at sites such as NextJournal and Repl.it.

If you’d prefer to work locally, you can also use free but more powerful (and complicated) software packages such as Juno (based on Atom) and VisualStudio Code. Another popular way to run Julia is from a Jupyter notebook, via the IJulia.jl package. Jupyter is the interactive notebook technology that lets you run code in Julia, Python, and R in a browser window. Setting these up for working with Julia is usually straightforward, but you’ll probably have to follow a list of instructions carefully. The simplest way to start is to fire up the REPL.

On macOS

[edit | edit source]

On a Mac, you download the Julia DMG, double-click to open it, and drag the icon to the Applications folder. To run Julia, you can double-click the icon of the Julia package in the /Applications folder. This opens the terminal application, and starts a new window. This is the REPL, introduced in the next section:

$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.2 (2020-09-23)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia>

Alternatively, you can type, in a terminal, something like this:

$ /Applications/Julia-1.5.app/Contents/Resources/julia/bin/julia

Here you’re specifying the path name of the Julia binary executable that lives inside the Julia application bundle. The exact version name might be different — check it using this command, which shows all available versions:

$ ls /Applications/Julia*/Contents/Resources/julia/bin/julia
/Applications/Julia-0.4.5.app/Contents/Resources/julia/bin/julia
/Applications/Julia-0.4.7.app/Contents/Resources/julia/bin/julia
/Applications/Julia-0.5.app/Contents/Resources/julia/bin/julia
/Applications/Julia-0.6.app/Contents/Resources/julia/bin/julia
/Applications/Julia-0.7.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.0.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.2.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.3.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.4.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.5.app/Contents/Resources/julia/bin/julia
/Applications/Julia-1.6.app/Contents/Resources/julia/bin/julia

Running directly from terminal

[edit | edit source]

Typically, Julia is installed in /Applications, which isn't included in your PATH, and so the shell can't find it when you type julia on the command line.

But there are clever things you can do with paths and profiles, so that you can log in to a terminal and type julia with immediate success.

For example, after you find out the location of the Julia binary executable file (see above), you can define the following alias:

alias julia="/Applications/Julia-1.5.app/Contents/Resources/julia/bin/julia"

Obviously this will have to be updated every time the version number changes.

As an alternative, you could add the /Applications/Julia... path to the PATH variable (the mechanism that the OS uses to find executable programs on your system):

PATH="/Applications/Julia-1.5.app/Contents/Resources/julia/bin/:${PATH}"
export PATH

A different approach is to create a link to the executable and put it into the /usr/local/bin directory (which should already be in your path), so that typing julia is the exact equivalent of typing /Applications/Julia/.../julia. This command does that:

ln -fs "/Applications/Julia-1.5.app/Contents/Resources/julia/bin/julia" /usr/local/bin/julia

Whichever method you choose, you can add the relevant command to your ~/.bash_profile or ~/.zprofile files, which run every time you start a new shell.

You can add the 'shebang' line at the top of a text file ('script') containing Julia code, so that the shell can find Julia and execute the file:

#!/usr/bin/env julia

This also works in a lot of text editors, where you can choose Run to run the file. This works if the editor reads the user's environment variables before running the file. (But not all do!)

Running a Julia program

[edit | edit source]

If you have a text file containing Julia code, you can run it from the command line:

$ julia hello-world.jl

or from within the Julia REPL:

$ julia
julia> include("hello-world.jl")

If the first line specifies a Julia interpreter:

#!/Applications/Julia-1.2.app/Contents/Resources/julia/bin/julia

or

#!/usr/bin/env julia

you can run the file like this:

$ ./hello-world.jl

Running a script with Julia

[edit | edit source]

If you want to write Julia code in an editor and run it, in true scripting-language fashion, you can. At the top of the script file, add a line like the following:

#!/Applications/Julia-1.2.app/Contents/Resources/julia/bin/julia

where the pathname points to the right place on your system, somewhere inside the relevant Julia application bundle, or:

#!/usr/bin/env julia

This is called the shebang line.

Now you can run the script from inside the editor in the same way that you'd run any other script, such as a shell or Perl script.

Using Homebrew

[edit | edit source]

If you're a fan of homebrew, you should be able to install Julia with:

$ brew install julia

On Windows

[edit | edit source]

On a Windows machine, you download the Julia Self-Extracting Archive (.exe) 32-bit or 64-bit. Double-click to start the installation process.

By default, it will install to your AppData folder. You may keep the default or choose your own directory (eg. C:\Julia).

After the installation has finished, you should create a System Environment variable called JULIA_HOME and set its value to the \bin directory under the folder where you installed Julia.

It is important to point JULIA_HOME to the /bin directory instead of the JULIA directory.

Then you can append ;%JULIA_HOME% to your PATH System Environment variable, so you can run scripts from any directory. Make sure that the registry key HKEY_CURRENT_USER\Environment\Path is of type REG_EXPAND_SZ, so %JULIA_HOME% gets expanded properly.

On FreeBSD

[edit | edit source]

To install Julia on FreeBSD (including TrueOS) or DragonFly BSD you can use either a binary package or using the ports system.

Installing from package

[edit | edit source]

Installing the Julia package is straightforward. Open up a terminal and type:

$ pkg install julia

To remove the package again you can use:

$ pkg remove julia

Installing from ports

[edit | edit source]

If you have the ports collection installed on your system (you can do so using running the command portsnap auto) the following is the canonical way to compile and install Julia onto your system:

$ cd /usr/ports/lang/julia/ && make install clean

On Linux

[edit | edit source]

Using Binaries

[edit | edit source]

You can use Julia direct from the binaries, without installing it on your machine. This is useful if you have old Linux distributions or if you don't have administrator's access to the machine. Just download the binaries from the website, and extract to a directory. Go into this directory (using cd), then into the bin folder. Now type:

$ ./julia

If the program doesn't have permission to run, use the following command to give this permission:

$ chmod +x julia

In principle, this method could be used on any Linux distribution.

A better setup

[edit | edit source]

If you want to run it by just typing julia in the terminal, you can set it up as follows. We'll assume you've downloaded the binary and extracted it to the folder /home/user/julia13.

Do one or more of the following:

1: add the following line to your ~/.bashrc file:

alias julia="/home/user/julia13/bin/julia"

(You'll have to re-run this file, either by restarting the terminal or using ~/.bashrc.

2: Add /home/user/julia13/bin/julia to your PATH environment variable.

3: Create a symlink to the julia executable to somewhere that is in PATH. For example:

sudo ln -s /home/user/julia13/bin/julia /usr/local/bin/julia

Installing from package

[edit | edit source]

This is the easiest way to install Julia if you're using Linux distributions based on RedHat, Fedora, Debian or Ubuntu. To install, download the respective package from the website (or JuliaPro), and install using your favorite way (double-clicking on the package file usually works). After doing this, Julia will be availabe from command line. On a terminal you can do:

$ julia
               _
   _       _ _(_)_     |  Documentation: http://docs.julialang.org 
  (_)     | (_) (_)    |  
   _ _   _| |_  __ _   |  Type "?" for "help()", "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version xxxxxxxxxxx
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |  

julia>


Arch Linux

[edit | edit source]

On Arch Linux, Julia is available from community repository, and can be installed running:

$ sudo pacman -S julia

To remove Julia package and it's dependencies (if not used by any other software on your system), you can run:

$ sudo pacman -Rsn julia

Void Linux

[edit | edit source]

On Void Linux distributions, Julia is available from main repository, and can be installed running:

$ sudo dpkg-install -Sy julia

Fedora

[edit | edit source]

On Fedora distributions, Julia is available from the updates repository (a default repository) and can be installed running:

$ sudo dnf install julia

To remove Julia package and its dependencies (again, if not used by other software on your system), you can run:

$ sudo dnf remove julia

Note that this applies only to Fedora, downstream distributions such as RHEL or CentOS must check their own repositories to see if Julia is available.

Running a script with Julia

[edit | edit source]

To tell your operating system that it should run the script using Julia, you can use what is called the shebang syntax. To do this, just use the following line on the very top of your script:

#!/usr/bin/env julia

With this as the first line of the script, the OS will search for "julia" on the path, and use it to run the script.

The REPL

[edit | edit source]
Previous page
Getting started
Introducing Julia Next page
Array and tuples
The REPL

The REPL

[edit | edit source]

The julia program starts the interactive REPL, the Read/Evaluate/Print/Loop, by default. It lets you type expressions in Julia code and see the results of the evaluation printed on the screen immediately. It:

  • Reads what you type;
  • Evaluates it;
  • Prints out the return value; then
  • Loops back and does it all over again.

The REPL is a great place to start experimenting with the language. But it's not the best environment to do serious programming work of any scale – for that, a text editor, or interactive notebook environment (e.g. IJulia/Jupyter) is a better choice. But there are advantages to using the REPL: it's simple, and should work without any installation or configuration. There's a built-in help system, too.

Using the REPL

[edit | edit source]

You type some Julia code and then press Return/Enter. Julia evaluates what you typed and returns the result:

julia> 42 <Return/Enter>
42

julia>

If you're using the Jupyter (IPython) notebook, you probably have to type Control-Enter, or Shift-Enter.

If you don't want to see the result of the expression printed, use a semicolon at the end of the expression:

julia> 42;

julia>

Also, if you want to access the value of the last expression you typed on the REPL, it's stored within the variable ans:

julia> ans
42

If you don't complete the expression on the first line, continue typing until you finish. For example:

julia> 2 +  <Return/Enter>

now Julia waits patiently until you finish the expression:

2  <Return/Enter>

and then you'll see the answer:

4

julia>


Help and searching for help

[edit | edit source]

Type a question mark ?

julia> ?

and you'll immediately switch to Help mode, and the prompt changes to yellow (in the terminal):

help?>

Now you can type the name of something (function names should be written without parentheses):

help?> exit
search: exit atexit textwidth process_exited method_exists indexin nextind IndexLinear TextDisplay istextmime
   
exit(code=0)
   
Stop the program with an exit code. The default exit code is zero, indicating that the 
program completed successfully. In an interactive session, exit() can be called with the 
keyboard shortcut ^D.
   
julia>

Notice that the help system has tried to find all the words that match the letters you typed, and shows you what it found.

If you want to search the documentation, you can use apropos and a search string:

julia> apropos("determinant")
LinearAlgebra.det
LinearAlgebra.logabsdet
LinearAlgebra.logdet

You'll see a list of functions whose names or descriptions contain the string.

julia> apropos("natural log")
Base.log
Base.log1p

help?> log
search: log log2 log1p log10 logging logspace Clong Clonglong Culong Culonglong task_local_storage
 
log(b,x)

Compute the base b logarithm of x. Throws DomainError for negative Real arguments.

and so on.

Shell mode

[edit | edit source]

If you type a semicolon

julia> ;

you immediately switch to shell mode:

shell>

(And the prompt changes to red). The commands available within this mode are the ones used by your system's command-line shell. In shell mode you can type any shell (i.e., non-Julia) command and see the result:

shell> ls
file.txt   executable.exe   directory file2.txt

How you leave shell mode depends on your Julia version:

  • In Julia 1.6 and later, shell mode is "sticky" (persistent). Press Backspace as the first character, or CTRL+C, to go back to the julia> prompt
  • In earlier Julia versions, the prompt switches immediately back to julia>, so you have to type a semicolon every time you want to give a shell command.

Package mode

[edit | edit source]

If you type a right bracket as the first character:

julia> ]

you immediately switch to Package mode:

(v1.1) pkg>

This is where you carry out package management tasks such as adding packages, testing them, and so on.

To leave package mode press Backspace or CTRL+C on an otherwise empty line.

Orientation

[edit | edit source]

Here are some other useful interactive functions and macros available at the REPL-prompt:

  • varinfo() – prints information about the exported global variables in a module
julia> varinfo()
name                    size summary    
–––––––––––––––– ––––––––––– –––––––––––
Base                         Module     
Core                         Module     
InteractiveUtils 222.893 KiB Module     
Main                         Module     
ans                1.285 KiB Markdown.MD


  • @which – tells you which method will be called for a function and particular arguments:
julia> @which sin(3)
sin(x::Real) in Base.Math at special/trig.jl:53
  • versioninfo() – gets Julia version and platform information:
julia> versioninfo()
Julia Version 1.1.0
Commit 80516ca202 (2019-01-21 21:24 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, ivybridge)


There's also a quick way to find out the version:

julia> VERSION
v"1.1.0"
  • edit("pathname") – launch the default editor and open the file pathname for editing
  • @edit rand() – launch the default editor and open the file containing the definition of the built-in function rand()
  • less("filename-in-current-directory") – displays the file in the pager
  • clipboard("stuff") – copies "stuff" to the system clipboard
  • clipboard() – pastes the contents of the clipboard into the current REPL line
  • dump(x) – displays information about a Julia object x on the screen
  • names(x) – gets an array of the names exported by the module x
  • fieldnames(typeof(x)) – gets an array of the data fields that belong to a symbol of type x

The <TAB> key: autocompletion

[edit | edit source]

The TAB key is usually able to complete – or suggest a completion for – something whose name you start typing. For example, if I type w and then press the TAB key (press twice when there are multiple options), all the functions that are currently available beginning with 'w' are listed:

julia> w <TAB>
wait    walkdir  which    while    widemul  widen    withenv  write

This works both for Julia entities and in shell and package modes. Here, for example, is how I can navigate to a directory from inside Julia:

shell> cd ~
/Users/me

shell> cd Doc <TAB>
shell> cd Documents/

shell> ls
...

Remember you can get help about functions using ? and typing in its full name (or using TAB-completion).

TAB-completion also works for Unicode-symbols: eg type \alp and press TAB to get \alpha and then press TAB again to get α. And for Emoji: eg type \:fe and press TAB to get \:ferris_wheel: and then press TAB again to get 🎡.

History

[edit | edit source]

You can look back through a record of your previous commands using the Up and Down arrow keys (and you can quit and restart without erasing that history). So you don't have to type a long multi-line expression again, because you can just recall it from history. And if you've typed loads of expressions, you can search through them, both back and forwards, by pressing Ctrl-R and Ctrl-S.

Fancy editing

[edit | edit source]

Julia's REPL supports features that make command-line entry more efficient, and these features are bound to particular key combinations. For example, Alt+bgoes back one word, while Alt+fgoes forward one word. A complete list of key bindings, together with instructions for customizing them, can be found in the REPL documentation. Note that some editors, like VS Code, may override certain key combinations.

Scope and performance

[edit | edit source]

One warning about the REPL. The REPL operates at the global scope level of Julia. Usually, when writing longer code, you would put your code inside a function, and organise functions into modules and packages. Julia's compiler works much more effectively when your code is organized into functions, and your code will run much faster as a result. There are also some things that you can't do at the top level – such as specify types for the values of variables.

Changing the prompt and customising your Julia session

[edit | edit source]

The following Julia file runs every time you start up Julia (unless you use the startup-file=no option).

~/.julia/config/startup.jl

This lets you load any package that you know you are going to want later. For example, if you want to customise your REPL session automatically, you can install the Julia package OhMyREPL.jl (https://github.com/KristofferC/OhMyREPL.jl) which lets you customize the REPL's appearance and behaviour, then, in the startup file:

using OhMyREPL

If you just want to set the prompt every time you start a Julia session, you could just add these instructions:

using REPL
function myrepl(repl)
    repl.interface = REPL.setup_interface(repl)
    repl.interface.modes[1].prompt = "julia-$(VERSION.major).$(VERSION.minor)> "
    return
end

atreplinit(myrepl)

This just sets the current REPL prompt to show the Julia version number that your session is using.

Julia and mathematics

[edit | edit source]

You can use Julia as a powerful calculator, using the REPL. It's good practice, too. (This is a tradition in introductions to interactive programming languages, and it's a good way to meet the language.)

julia> 1000000 / 7 
142857.14285714287

Typing numbers

[edit | edit source]

Half the world uses a comma (,) to divide long numbers into groups of three, the other half uses a period (.). (And the rest of us use scientific notation...) In Julia you can use an underscore (_) to separate groups of digits:

julia> 1_000_000 - 2_015
997985

although you won't see one in the response.

To use scientific notation, just type "e" (or "E") and don't add any spaces:

julia> planck_length = 1.61619997e-34

To type imaginary numbers, use im:

julia> (1 + 0.5im) * 2
2.0 + 1.0im

Operators as functions

[edit | edit source]
julia> 2 + 2
4

julia> 2 + 3 + 4
9

An equivalent form for adding numbers is:

julia> +(2, 2)
4

The operators that you usually use between values are ordinary Julia functions, and can be used in the same way as other functions. Similarly:

julia> 2 + 3 + 4
9

can be written as

julia> +(2, 3, 4)
9

and

julia> 2 * 3 * 4
24

can be written as

julia> *(2,3,4)
24

Some maths constants are provided:

julia> pi
π = 3.1415926535897...

You can find other maths constants in the MathConstants module:

julia> Base.MathConstants.golden
φ = 1.6180339887498...

julia> Base.MathConstants.e
e = 2.7182818284590...

All the usual operators are available:

julia> 2 + 3 - 4 * 5 / 6 % 7
1.6666666666666665

Notice the precedence of the operators. In this case it's:

((2 + 3) - ((4 * 5) / 6) % 7)

If you want to check the precedence of operators, enclose the expression in :( and ):

 julia> :(2 + 3 - 4 * 5 / 6 % 7)
 :((2 + 3) - ((4 * 5) / 6) % 7)

(More on this in Metaprogramming).

Multiplication is usually written *, but this can be omitted when multiplying a variable by a number literal:

julia> x = 2
2

julia> 2x + 1
5
julia> 10x + 4x - 3x/2 + 1
26.0

This makes equations much easier to write.

You'll sometimes need parentheses to control the evaluation order:

julia> (1 + sqrt(5)) / 2
1.618033988749895

Some others to watch out for include:

  • ^ power
  • % remainder

To make rational numbers, use two slashes (//):

julia> x = 666//999
2//3

Technically, / means "right division." There's also left division "\". For numbers, x/y = y\x. However, for vectors and matrices, x = A\y solves A*x = y and x = y/A solves x*A = y.

The standard arithmetic operators also have special updating versions, which you can use to update variables quickly:

  • +=
  • -=
  • *=
  • /=
  • \=
  • %=
  • ^=

For example, after defining a variable x:

julia> x = 5
5

you can add 2 to it:

julia> x += 2
7

then multiply it by 100:

julia> x *= 100
700

and then reduce it to its modulus 11 value:

julia> x %= 11
7

There are element-wise operators which work on arrays. This means that you can multiply two arrays element by element:

julia> [2,4] .* [10, 20]
2-element Array{Int64,1}:
 20
 80

Arrays are fundamental to Julia, and so have their own chapter in this book.

If you divide two integers using /, the answer is always a floating-point number. If you've used Python version 2, you'll remember that Python returns an integer result. Python (3) returns a float now.

Julia offers an integer division operator ÷ (type \div TAB, or use the function version div(). This should be used when you want an integer result rather than the floating-point returned by /.

julia> 3 ÷ 2
1

julia> div(3, 2)
1

Notice that Julia doesn't convert the answer to an integer type, even if the result is effectively an integer.

julia> div(3, 2.0)
1.0

This is to avoid type instability problems, which can slow down your code.

Integer overflow

[edit | edit source]

If you think your calculations are going to burst out of the 64-bit restriction, choose Big Integers by applying the big function to store the operands as big numbers:

julia> 2^64 # oops
0

julia> big(2)^64 # better
18446744073709551616

julia> 2^big(64) # equally better
18446744073709551616

To get the fastest execution speeds for your Julia programs, you should be aware of how your data and variables can be stored without introducing 'type instability'.

Number bases

[edit | edit source]

These handy utility functions might come in useful when using the REPL as a calculator.

The bitstring() function shows the literal binary representation of a number, as stored:

julia> bitstring(20.0)
"0100000000110100000000000000000000000000000000000000000000000000"

julia> bitstring(20)
"0000000000000000000000000000000000000000000000000000000000010100"

Notice that the floating point 'version' is, as you would expect, stored differently.

To go from a binary string back to decimal, you can use parse(), which accepts a target type and number base:

julia> parse(Int, "0000011", base=2)
3 
julia> parse(Int, "DecaffBad", base=16)
59805531053

For working in number bases other than the default 10, use the string function to convert integers to strings:

julia> string(65535, base=16)
"ffff"
julia> string(64, base=8)
"100"

Whereas digits(number, base=b) returns an array of the digits of number in the given base:

julia> digits(255, base=16)
2-element Array{Int64,1}:
 15
 15

Variables

[edit | edit source]

In this expression:

julia> x = 3

x is a variable, a named storage location for a data object. In Julia, variables can be named pretty much how you like, although don't start variable names with numbers or punctuation. You can use Unicode characters, if you want.

To assign a value, use a single equals sign.

julia> a = 1
1

julia> b = 2
2

julia> c = 3
3

To test equality, you should use the == operator or isequal() function.

In Julia, you can also assign multiple variables at the same time:

julia> a, b = 5, 3
(5,3)

Notice that the return value of this expression is a parenthesis-bounded comma-separated ordered list of elements: tuple for short.

julia> a
5

julia> b
3
Multiplying numbers and variables
[edit | edit source]

It's worth repeating that you can preface a variable name with a number to multiply them, without having to use an asterisk (*). For example:

julia> x = 42
42

julia> 2x
84

julia> .5x
21.0

julia> 2pi
6.283185307179586

Special characters

[edit | edit source]

The Julia REPL provides easy access to special characters, such as Greek alphabetic characters, subscripts, and special maths symbols. If you type a backslash, you can then type a string (usually the equivalent LATEX string) to insert the corresponding character. For example, if you type:

julia> \sqrt<TAB>

Julia replaces the \sqrt with a square root symbol:

julia> 

Some other examples:

\pi π
\Gamma Γ
\mercury
\degree °
\cdot
\in

There's a full list in the Julia source code. As a general principle, in Julia you're encouraged to look at the source code, so there are useful built-in functions for looking at Julia source files. For example, on macOS these symbols are stored in:

julia> less("/Applications/Julia-1.0.app/Contents/Resources/julia/share/julia/stdlib/v1.0/REPL/src/latex_symbols.jl")

less runs the file through a pager (ie the less command in Unix). If you're brave, try using edit() rather than less(). This launches an editor and opens the file.

It's also possible to use Emoji and other Unicode characters in the REPL.

For emoji, type the Emoji character name, between colons, after the backslash, then press <TAB>:

julia> \:id: <TAB>

which changes to:

julia> 🆔

You can find a list at https://docs.julialang.org/en/latest/manual/unicode-input/#Unicode-Input-1.

Entering Unicode symbols that aren't in this list is possible but more OS-dependent: on macOS you 'hold down' the Ctrl/Alt key while typing the Unicode hex digits (with the Unicode Hex Input keyboard enabled); on Windows it's Ctrl+Shift+u followed by the hex digits.)

julia> ✎ = 3
3

julia> 
3

Maths functions

[edit | edit source]

Because Julia is particularly suited for scientific and technical computing, there are many mathematical functions that you can use immediately, and you often don't have to import them or use prefixes – they're already available.

The trigonometry functions expect values in radians:

julia> sin(pi / 2)
1.0

but there are degree-based versions too: sind(90) finds the sine of 90 degrees. Use deg2rad() and rad2deg() to convert between degrees and radians.

There are also lots of log functions:

julia> log(12)
2.4849066497880004

and the accurate hypot() function:

julia> hypot(3, 4)
5.0

The norm() function (after loading via "using LinearAlgebra") returns the "p"-norm of a vector or the operator norm of a matrix. Here's divrem():

julia> divrem(13, 3) # returns the division and the remainder
(4,1)

There are dozens of others.

There's a system-wide variable called ans that remembers the most recent result, so that you can use it in the next expression.

julia> 1 * 2 * 3 * 4 * 5
120

julia> ans/10
12.0
Exercise
[edit | edit source]

Guess, then find out using the help system, what mod2pi() and isapprox() do.

Descriptions of all the functions provided as standard with Julia are described here: [1]

Random numbers

[edit | edit source]

rand() – gets one random Float64 between 0 and 1

julia> rand()
0.11258244478647295

rand(2, 2) – an array of Float64s with dimensions 2, 2

rand(type, 2, 2) – an array of values of this type with dims 2, 2

rand(range, dims) – array of numbers in a range (including both ends) with specified dimensions:

julia> rand(0:10, 6)
6-element Array{Int64,1}:
 6
 7
 9
 6
 3
 10

(See the Arrays chapter for more about range objects.)

The rand() function can generate a true or false value if you tell it to, by passing the Bool keyword:

julia> rand(Bool)
false

or a bunch of trues and falses:

julia> rand(Bool, 20)
20-element Array{Bool,1}:
 false
 true
 false
 false
 false
 true
 true
 false
 false
 false
 false
 false
 false
 false
 true
 true
 false
 true
 true
 false


Random numbers in a distribution
[edit | edit source]

randn() gives you one random number in a normal distribution with mean 0 and standard deviation 1. randn(n) gives you n such numbers:

julia> randn()
0.8060073309441075

julia> randn(10)
10-element Array{Float64,1}:
  1.3261528248041754 
  1.9203896217047232 
 -0.17640138484904164
  1.0099294365771374 
 -0.9060606885634369 
  1.739192165935964  
  1.375790854463711  
 -0.6581841725500879 
  0.11190904953985797
  2.798450557786332 

If you've installed the Plots plotting package, you can plot this:

julia> using Plots; gr()
julia> histogram(randn(10000), nbins=100)

histogram plot created in Julia using Plots

Seeding the random number generator

[edit | edit source]

The Random package contains many more random functions, such as randperm(), shuffle(), and seed!.

Before you use random numbers, you can seed the random number generator with a specific value. This ensures that subsequent random numbers will follow the same sequence, if they start from the same seed. You can seed the generator using the seed!() or MersenneTwister() functions.

Once you've added the Random package, you can do:

julia> using Random
julia> Random.seed!(10);

julia> rand(0:10, 6)
6-element Array{Int64,1}:
 6
 5
 9
 1
 1
 0
julia> rand(0:10, 6)
6-element Array{Int64,1}:
 10
 3
 6
 8
 0
 1

After restarting Julia, the same seed guarantees the same random numbers.

Simple keyboard input

[edit | edit source]

Here's an example of how you'd write and run a function that reads input from the keyboard:

julia> function areaofcircle() 
            print("What's the radius?")
            r = parse(Float64, readline(stdin))
            print("a circle with radius $r has an area of:")
            println(π * r^2)
        end
areaofcircle (generic function with 1 method)

julia> areaofcircle()
What's the radius?
42
a circle with radius 42.0 has an area of:
5541.769440932395
julia>

This works in a Julia REPL session; when called, the function waits for the user to type a string on the keyboard and press Return/Enter.

Arrays and Tuples

[edit | edit source]
Previous page
The REPL
Introducing Julia Next page
Types
Arrays and tuples

Storage: Arrays and Tuples

[edit | edit source]

In Julia, groups of related items are usually stored in arrays, tuples, or dictionaries. Arrays can be used for storing vectors and matrices. This section concentrates on arrays and tuples; for more on dictionaries, see Dictionaries and Sets.

Arrays

[edit | edit source]

An array is an ordered collection of elements. It's often indicated with square brackets and comma-separated items. You can create arrays that are full or empty, and arrays that hold values of different types or restricted to values of a specific type.

In Julia, arrays are used for lists, vectors, tables, and matrices.

A one-dimensional array acts as a vector or list. A 2-D array can be used as a table or matrix. And 3-D and more-D arrays are similarly thought of as multi-dimensional matrices.

Creating arrays

[edit | edit source]

Creating simple arrays

[edit | edit source]

Here's how to create a simple one-dimensional array:

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

Julia informs you ("5-element Array{Int64,1}") that you've created a 1-dimensional array with 5 elements, each of which is a 64-bit integer, and bound the variable a to it. Notice that intelligence is applied to the process: if one of the elements looks like a floating-point number, for example, you'll get an array of Float64s:

julia> a1 = [1, 2, 3.0, 4, 5]
5-element Array{Float64,1}:
1.0
2.0
3.0
4.0
5.0

Similarly for strings:

julia> s = ["this", "is", "an", "array", "of", "strings"]
6-element Array{String,1}:
"this"
"is"
"an"
"array"
"of"
"strings"

returns an array of strings, and:

julia> trigfuns = [sin, cos, tan]
3-element Array{Function,1}:
sin
cos
tan

returns an array of Julia functions.

There are many different ways to create arrays: you can make them empty, uninitialised, full, based on sequences, sparse, dense, and more besides. It depends on the task in hand.

Uninitialized

[edit | edit source]

You can specify the type and the dimensions of an array using Array{type}(dims) (notice that upper-case "A"), putting the type in curly braces, and the dimensions in the parentheses. The undef means that the array hasn't been initialized to known values.

julia> array = Array{Int64}(undef, 5)
 5-element Array{Int64,1}:
 4520632328
 4614616448
 4520668544
 4520632328
 4615451376

julia> array3 = Array{Int64}(undef, 2, 2, 2)
2×2×2 Array{Int64,3}:
[:, :, 1] =
 4452254272  4452255728
 4452256400  4456808080

[:, :, 2] =
 4456808816  4452255728
 4456808816  4452254272

The random-looking numbers are a reminder that you've created an uninitialized array but haven't filled it with any sensible information.

Arrays of anything

[edit | edit source]

It's possible to create arrays with elements of different types:

julia> [1, "2", 3.0, sin, pi]
5-element Array{Any, 1}:
 1
  "2"
 3.0
  sin
π = 3.1415926535897...

Here, the array has five elements, but they're an odd mixture: numbers, strings, functions, constants — so Julia creates an array of type Any:

julia> typeof(ans)
Array{Any,1}

Empty arrays

[edit | edit source]

To create an array of a specific type, you can also use the type definition and square brackets:

julia> Int64[1, 2, 3, 4]
4-element Array{Int64,1}:
1
2
3
4

If you think you can fool Julia by sneaking in a value of the wrong type while declaring a typed array, you'll be caught out:

julia> Int64[1, 2, 3, 4, 5, 6, 7, 8,  9, 10.1]
ERROR: InexactError()

You can create empty arrays this way too:

julia> b = Int64[]
0-element Array{Int64,1}
julia> b = String[]
0-element Array{String,1}
julia> b = Float64[]
0-element Array{Float64,1}

Creating 2D arrays and matrices

[edit | edit source]

If you leave out the commas when defining an array, you can create 2D arrays quickly. Here's a single row, multi-column array:

julia> [1 2 3 4]
1x4 Array{Int64,2}:
1  2  3  4

Notice the 1x4 {...,2} in the first row of the response.

You can use a semicolon to add another row:

julia> [1 2 3 4 ; 5 6 7 8]
2x4 Array{Int64,2}:
1  2  3  4
5  6  7  8

Row and column vectors

[edit | edit source]

Compare these two: [1,2,3,4,5] and [1 2 3 4 5].

With the commas, this array could be called a "column vector", with 5 rows and 1 column:

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

But with the spaces, this array could be called a "row vector", with 1 row and 5 columns:

julia> [1 2 3 4 5]
1x5 Array{Int64,2}:
1  2  3  4  5

- notice the {Int64,2} here, which tells you that this is a 2D array of Int64s (with 1 row and 5 columns). In both cases, they're standard Julia arrays.

Arrays created like this can be used as matrices:

julia> [1 2 3; 4 5 6]
2x3 Array{Int64,2}:
1  2  3
4  5  6

And of course you can create arrays/matrices with 3 or more dimensions.

There are a number of functions which let you create and fill an array in one go. See Creating and filling an array.

Notice how Julia distinguishes between Array{Float64,1} and Array{Float64,2}:

julia> x = rand(5)
5-element Array{Float64,1}:
 0.4821773161183929 
 0.5811789456966778 
 0.7852806713801641 
 0.23626682918327369
 0.6777187748570226 
julia> x = rand(5, 1)
5×1 Array{Float64,2}:
 0.0723474801859294 
 0.6314375868614579 
 0.21065681560040828
 0.8300724654838343 
 0.42988769728089804

Julia provides the Vector and Matrix constructor functions, but these are simply aliases for uninitialized one and two dimensional arrays:

julia> Vector(undef, 5)
5-element Array{Any,1}:
 #undef
 #undef
 #undef
 #undef
 #undef

julia> Matrix(undef, 5, 5)
5x5 Array{Any,2}:
 #undef  #undef  #undef  #undef  #undef
 #undef  #undef  #undef  #undef  #undef
 #undef  #undef  #undef  #undef  #undef
 #undef  #undef  #undef  #undef  #undef
 #undef  #undef  #undef  #undef  #undef

Creating arrays using range objects

[edit | edit source]

In Julia, the colon (:) has a number of uses. One use is to define ranges and sequences of numbers. You can create a range object by typing it directly:

julia> 1:10
1:10

It may not look very useful in that form, but it provides the raw material for any job in Julia that needs a range or sequence of numbers.

You can use it in a loop expression:

julia> for n in 1:10 print(n) end
12345678910

Or you can use collect() to build an array consisting of those numbers:

julia> collect(1:10)
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

You don't have to start and finish on an integer either:

julia> collect(3.5:9.5)
7-element Array{Float64,1}:
3.5
4.5
5.5
6.5
7.5
8.5
9.5

There's also a three-piece version of a range object, start:step:stop, which lets you specify a step size other than 1. For example, this builds an array with elements that go from 0 to 100 in steps of 10:

julia> collect(0:10:100)
11-element Array{Int64,1}:
  0
 10
 20
 30
 40
 50
 60
 70
 80
 90
100

To go down instead of up, you have to use a negative step value:

julia> collect(4:-1:1)
4-element Array{Int64,1}:
 4
 3
 2
 1

Instead of using collect() to create an array from the range, you could use the ellipsis (...) operator (three periods) after the last element:

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

(The ... ellipsis is sometimes called the splat operator. It represents a sequence of arguments.)

However, collect() is faster and the recommended method of converting ranges to arrays. But you can use range objects in many situations in Julia, and you don't always need to expand them into arrays.

More range objects

[edit | edit source]

Another useful function is range(), which constructs a range object that goes from a start value to an end value taking a specific number of steps of a certain size. You don't have to calculate all the information, because Julia calculates the missing pieces for you by combining the values for the keywords step, length, and stop. For example, to go from 1 to 100 in exactly 12 steps:

julia> range(1, length=12, stop=100)
1.0:9.0:100.0

or take 10 steps from 1, stopping at or before 100:

julia> range(1, stop=100, step=10)
1:10:91

If you really want it in array form, you can use the range object to build an array:

julia> collect(range(1, length=12, stop=100))
12-element Array{Float64,1}:
  1.0
 10.0
 19.0
 28.0
 37.0
 46.0
 55.0
 64.0
 73.0
 82.0
 91.0
100.0

Notice that it provided you with a Float64 array, rather than an Integer array, even though the values could have been integers.

For logarithmic ranges (sometimes called 'log space'), you can use simple range objects and then broadcast the exp10 function (10^x) to every element of the range.

julia> exp10.(range(2.0, stop=3.0, length=5))
5-element Array{Float64,1}:
  100.0             
  177.82794100389228
  316.22776601683796
  562.341325190349  
 1000.0            

See Broadcasting and dot syntax.

Use step() on a range object to find out what the step size is:

julia> step(range(1, length=10, stop=100))
11.0

Use range() if you know the start and step, but not the end, and you know how many elements you want:

julia> range(1, step=3, length=20) |> collect
20-element Array{Int64,1}:
  1
  4
  7
 10
 13
 16
 19
 22
 25
 28
 31
 34
 37
 40
 43
 46
 49
 52
 55
 58

Collecting up the values in a range

[edit | edit source]

As you've seen, if you're not using your range object in a for loop, you can, if you want, use collect() to obtain all the values from a range object directly:

julia> collect(0:5:100)
21-element Array{Int64,1}:
  0
  5
 10
 15
 20
 25
 30
 35
 40
 45
 50
 55
 60
 65
 70
 75
 80
 85
 90
 95
100

However, you don't always have to convert ranges to arrays before working on them — you can usually iterate over things directly. For example, you don't have to write this:

for i in collect(1:6)
    println(i)
end
 1
 2
 3
 4
 5
 6

because it works just as well (and probably faster) if you leave out the collect():

for i in 1:6
    println(i)
end
 1
 2
 3
 4
 5
 6

Using comprehensions and generators to create arrays

[edit | edit source]

A useful way to create arrays where each element can be produced using a small computation is to use comprehensions (described in Comprehensions).

For example, to create an array of 5 numbers:

julia> [n^2 for n in 1:5]
5-element Array{Int64,1}:
 1
 4
 9
16
25

With two iterators, you can easily create a 2D array or matrix:

julia> [r * c for r in 1:5, c in 1:5]
5x5 Array{Int64,2}:
1   2   3   4   5
2   4   6   8  10
3   6   9  12  15
4   8  12  16  20
5  10  15  20  25

You can add an if test at the end to filter (keep) values that pass a test:

julia> [i^2 for i=1:10  if i != 5]
9-element Array{Int64,1}:
   1
   4
   9
  16
  36
  49
  64
  81
 100

Generator expressions are similar, and can be used in a similar way:

julia> collect(x^2 for x in 1:10)
10-element Array{Int64,1}:
   1
   4
   9
  16
  25
  36
  49
  64
  81
 100
julia> collect(x^2 for x in 1:10 if x != 1)
9-element Array{Int64,1}:
   4
   9
  16
  25
  36
  49
  64
  81
 100

The advantage of generator expressions is that they generate values when needed, rather than build an array to hold them first.

Creating and filling an array

[edit | edit source]

There are a number of functions that let you create arrays with specific contents. These can be very useful when you're using 2D arrays as matrices:

- zeros(m, n) creates an array/matrix of zeros with m rows and n columns:

julia> zeros(2, 3)
2x3 Array{Float64,2}:
0.0  0.0  0.0
0.0  0.0  0.0

You can specify the type of the zeros if you want:

julia> zeros(Int64, 3, 5)
3×5 Array{Int64,2}:
 0  0  0  0  0
 0  0  0  0  0
 0  0  0  0  0

- ones(m, n) creates an array/matrix of ones with m rows and n columns

julia> ones(2, 3)
2x3 Array{Float64,2}:
 1.0  1.0  1.0
 1.0  1.0  1.0

- rand(m, n) creates an m-row by n-column matrix full of random numbers:

julia> rand(2, 3)
2×3 Array{Float64,2}:
 0.488552   0.657078   0.895564
 0.0190633  0.0120305  0.772106

- rand(range, m, n) creates a matrix full of numbers in the supplied range:

julia> rand(1:6, 3, 3)
3x3 Array{Int64,2}:
 4  4  1
 3  2  3
 6  3  3

- randn(m, n) creates an m-row by n-column matrix full of normally-distributed random numbers with mean 0 and standard deviation 1.

As well as the zeros(), ones() functions, there are trues(), falses(), fill(), and fill!() functions as well.

The trues() and falses() functions fill arrays with the Boolean values true or false:

julia> trues(3, 4)
3x4 BitArray{2}:
true  true  true  true
true  true  true  true
true  true  true  true

Notice how the result is a BitArray.

You can use fill() to create an array with a specific value, i.e. an array of repeating duplicates:

julia> fill(42, 9)
9-element Array{Int64,1}:
42
42
42
42
42
42
42
42
42

julia> fill("hi", 2, 2)
2x2 Array{String,2}:
"hi"  "hi"
"hi"  "hi"

With fill!(), the exclamation mark (!) or "bang" is to warn you that you're about to change the contents of an existing array (a useful indication that's adopted throughout Julia).

julia> a = zeros(10)
10-element Array{Float64,1}:
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0

julia> fill!(a, 42)
10-element Array{Float64,1}:
 42.0
 42.0
 42.0
 42.0
 42.0
 42.0
 42.0
 42.0
 42.0
 42.0 

Let's change an array of falses to trues:

julia> trueArray = falses(3,3)
3x3 BitArray{2}:
false  false  false
false  false  false
false  false  false
julia> fill!(trueArray, true)
3x3 BitArray{2}:
true  true  true
true  true  true
true  true  true
julia> trueArray
3x3 BitArray{2}:
true  true  true
true  true  true
true  true  true

You can use the range() function to create vector-like arrays, followed by reshape() to change them into 2D arrays:

julia> a = reshape(range(0, stop=100, length=30), 10, 3)
10×3 reshape(::StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}, 10, 3) with eltype Float64:
  0.0      34.4828   68.9655
  3.44828  37.931    72.4138
  6.89655  41.3793   75.8621
 10.3448   44.8276   79.3103
 13.7931   48.2759   82.7586
 17.2414   51.7241   86.2069
 20.6897   55.1724   89.6552
 24.1379   58.6207   93.1034
 27.5862   62.069    96.5517
 31.0345   65.5172  100.0   

The result is a 10 by 3 array featuring evenly-spaced numbers between 0 and 100.

Repeating elements to fill arrays

[edit | edit source]

A useful function for creating arrays by repeating smaller ones is repeat().

The first option for its syntax is repeat(A, n, m), the source array is repeated by n times in the first dimension (rows), and m times in the second (columns).

You don't have to supply the second dimension, just supply how many rows you want:

julia> repeat([1, 2, 3], 2)
6-element Array{Int64,1}:
 1
 2
 3
 1
 2
 3

julia> repeat([1 2 3], 2)
2x3 Array{Int64,2}:
 1  2  3
 1  2  3

The second option specifies the extra columns:

julia> repeat([1, 2, 3], 2, 3)
6x3 Array{Int64,2}:
 1  1  1
 2  2  2
 3  3  3
 1  1  1
 2  2  2
 3  3  3

julia> repeat([1 2 3], 2, 3)
2x9 Array{Int64,2}:
 1  2  3  1  2  3  1  2  3
 1  2  3  1  2  3  1  2  3

The repeat() function also lets you create arrays by duplicating rows and columns of a source array. The inner and outer options determine whether rows and/or columns are repeated. For example, inner = [2, 3] makes an array with two copies of each row and three copies of each column:

julia> repeat([1, 2], inner = [2, 3])
4x3 Array{Int64,2}:
 1  1  1
 1  1  1
 2  2  2
 2  2  2 

By contrast, here's outer = [2,3]:

julia> repeat([1, 2], outer = [2, 3])
4x3 Array{Int64,2}:
 1  1  1
 2  2  2
 1  1  1
 2  2  2

Note that the latter is equivalent to repeat([1, 2], 2, 3). A more meaningful example of the outer keyword is when it is combined with inner. Here, each element of each line of the initial matrix is line-duplicated and then, each line slice of the resulting matrix is column-triplicated:

 julia> repeat([1 2; 3 4], inner=(2, 1), outer=(1, 3))
 4×6 Array{Int64,2}:
  1  2  1  2  1  2
  1  2  1  2  1  2
  3  4  3  4  3  4
  3  4  3  4  3  4

Array constructor

[edit | edit source]

The Array() function we saw earlier builds arrays of a specific type for you:

julia> Array{Int64}(undef, 6)
6-element Array{Int64,1}:
 4454517776
 4454517808
 4454517840
 4454517872
 4454943824
 4455998977

This is uninitialized; the odd-looking numbers are simply the old contents of the memory before it was assigned to hold the new array.

Arrays of arrays

[edit | edit source]

It's easy to create an array of arrays. Sometimes you want to specify the original contents:

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

The Array constructor can also construct an array of arrays:

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

With the reshape() function, you could of course just create a simple array and then change its shape:

julia> reshape([1, 2, 3, 4, 5, 6, 7, 8], 2, 4)
2x4 Array{Int64,2}:
1  3  5  7
2  4  6  8

The same techniques can be used to create 3D arrays. Here's a 3D array of strings:

julia> Array{String}(undef, 2, 3, 4)
2x3x4 Array{String,3}:
[:, :, 1] =
#undef  #undef  #undef
#undef  #undef  #undef
[:, :, 2] =
#undef  #undef  #undef
#undef  #undef  #undef
[:, :, 3] =
#undef  #undef  #undef
#undef  #undef  #undef
[:, :, 4] =
#undef  #undef  #undef
#undef  #undef  #undef

Each element is set to 'undefined' — #undef.

The push!() function pushes another item onto the back of an array:

julia> push!(a, rand(1:100, 5))
3-element Array{Array,1}:
[1, 2]
[3, 4]
[4, 71, 82, 60, 48]

julia> push!(a, rand(1:100, 5))
4-element Array{Array,1}:
[1,2]
[3,4]
[4, 71, 82, 60, 48]
[4, 22, 52, 5, 14]

or you might want to create them empty:

julia> a = Array{Int}[]
0-element Array{Array{Int64,N} where N,1}

julia> push!(a, [1, 2, 3])
1-element Array{Array{Int64,N} where N,1}:
[1, 2, 3]

julia> push!(a, [4, 5, 6])
2-element Array{Array{Int64,N} where N,1}:
[1, 2, 3]
[4, 5, 6]

You can use Vector as an alias for Array:

julia> a = Vector{Int}[[1, 2], [3, 4]]
2-element Array{Array{Int64,1},1}:
[1, 2]
[3, 4]

julia> push!(a,  rand(1:100, 5))
3-element Array{Array{Int64, 1},1}:
[1, 2]
[3, 4]
[12, 65, 53, 1, 82]
    
julia> a[2]
2-element Array{Int64,1}:
3
4
    
julia> a[2][1]
3

Copying arrays

[edit | edit source]

If you have an existing array and want to create another array having the same dimensions, you can use the similar() function:

julia> a = collect(1:10); # hide the output with the semicolon
julia> b = similar(a)
10-element Array{Int64,1}:
 4482975872
 4482975792
          1
 4482975952
 4482976032
 4482976112
          3
          3
          2
 4520636161

Notice that the array dimensions are copied, but the values aren't, they've been copied from random bits of memory. You can, though, change the type and dimensions anyway, so they don't have to be that similar:

julia> c = similar(b, String, (2, 2))
2x2 Array{String,2}:
#undef  #undef
#undef  #undef

And in any case there's a copy() function.

Matrix operations: using arrays as matrices

[edit | edit source]

In Julia, a 2-D array can be used as a matrix. All the functions available for working on arrays can be used (if the dimensions and contents permit) as matrices.

A quick way of typing a matrix is to separate the elements using spaces (to make rows) and to use semicolons to separate the rows. So:

julia> [1 0 ; 0 1]
  2x2 Array{Int64,2}:
  1  0
  0  1

You could also do this:

julia> id  = reshape([1, 2, 3, 4], 2, 2)
2×2 Array{Int64,2}:
 1  3
 2  4

which takes a standard array and reshapes it to run in two rows and two columns. Notice that the matrix is filled column by column.

If you don't use commas or semicolons:

 julia> [1 2 3 4]

you'll create a single row array/matrix:

1x4 Array{Int64,2}:
1  2  3  4

In each case, notice the 2 in the braces ({Int64,2}) following the type value. This indicates a 2-dimensional array.

You can create an array of arrays by sticking two arrays next to each other, like this:

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

When you omit the comma, you're placing columns next to each and you'll get this:

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

Accessing the contents of arrays

[edit | edit source]

To access the elements of an array or matrix, follow the name of the array by the element number in square brackets. Here's a 1D array:

julia> a = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

Here's the fifth element:

julia> a[5]
50

The first element is index number 1. Julia is one of the languages that starts indexing elements in lists and arrays starting at 1, rather than 0. (And thus it's in the elite company of Matlab, Mathematica, Fortran, Lua, and Smalltalk, while most of the other programming languages are firmly in the opposite camp of 0-based indexers.)

The last element is referred to as end (not -1, as in some other languages):

julia> a[end]
100

Similarly, you can access the second to last element with

julia> a[end-1]
90

(with similar syntax for the third to last element and so on).

You can provide a bunch of index numbers, enclosed in a pair of brackets at each end:

julia> a[[3,6,2]]
3-element Array{Int64,1}:
 30
 60
 20

or supply a range of index numbers:

julia> a[2:2:end]
5-element Array{Int64,1}:
  20
  40
  60
  80
 100

You can even select elements using true and false values:

julia> a[[true, true, false, true, true, true, false, true, false, false]]
6-element Array{Int64,1}:
 10
 20
 40
 50
 60
 80

Here's a 2D array, with the rows separated by semicolons:

julia> a2 = [1 2 3; 4 5 6; 7 8 9]
3x3 Array{Int64,2}:
1  2  3
4  5  6
7  8  9

julia> a2[1]
1

If you just ask for one element of a 2D array, you'll receive it as if the array is unwound column by column, i.e. down first, then across. In this case you'll get 4, not 2:

julia> a2[2]
4

Asking for row then column works as you expect:

julia> a2[1, 2]
2

which is row 1, column 2. Here's row 1, column 3:

julia> a2[1, 3]
3

but don't get the row/column indices the wrong way round:

julia> a2[1, 4]
ERROR: BoundsError: attempt to access 3×3 Array{Int64,2} at index [1, 4]
Stacktrace:
 [1] getindex(::Array{Int64,2}, ::Int64, ::Int64) at ./array.jl:498

By the way, there's an alternative way of obtaining elements from arrays: the getindex() function:

julia> getindex(a2, 1, 3)
3
 
julia> getindex(a2, 1, 4)
ERROR: BoundsError: attempt to access 3×3 Array{Int64,2} at index [1, 4]
Stacktrace:
 [1] getindex(::Array{Int64,2}, ::Int64, ::Int64) at ./array.jl:498

Use the colon to indicate every row or column. For example, here's "every row, second column":

julia> a2[:, 2]
3-element Array{Int64,1}:
2
5
8

and here's "second row, every column":

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

Elementwise and vectorized operations

[edit | edit source]

Many Julia functions and operators are designed specifically to work with arrays. This means that you don't always have to work through an array and process each element individually.

A simple example is the use of the basic arithmetic operators. These can be used directly on an array if the other argument is a single value:

julia> a = collect(1:10);
julia> a * 2
10-element Array{Int64,1}:
  2
  4
  6
  8
 10
 12
 14
 16
 18
 20

and every element of the new array is the original multiplied by 2. Similarly:

julia> a / 100
10-element Array{Float64,1}:
0.01
0.02
0.03
0.04
0.05
0.06
0.07
0.08
0.09
0.1

and every element of the new array is the original divided by 100.

These operations are described as operating elementwise.

Many operators can be used preceded with a dot (.). These versions are the same as their non-dotted versions, and work on the arrays element by element. For example, the multiply function (*) can be used elementwise, using .*. This lets you multiply arrays or ranges together element by element:

julia> n1 = 1:6;
julia> n2 = 100:100:600;
julia> n1 .* n2
6-element Array{Int64,1}:
 100
 400
 900
1600
2500
3600

and the first element of the result is what you get by multiplying the first elements of the two arrays, and so on.

As well as the arithmetic operators, some of the comparison operators also have elementwise versions. For example, instead of using == in a loop to compare two arrays, use .==. Here are two arrays of ten numbers, one sequential, the other disordered, and an elementwise comparison to see how many elements of array b happened to end up in the same place as array a:

julia> a = 1:10; b=rand(1:10, 10); a .== b
10-element BitArray{1}:
 true
false
 true
false
false
false
false
false
false
false

Broadcasting: dot syntax for vectorizing functions

[edit | edit source]

This technique of applying functions elementwise to arrays with the dot syntax is called broadcasting. Follow the function name with a dot/period before the opening parenthesis, and supply an array or range as an argument. For example, here's a simple function which multiplies two numbers together:

julia> f(a, b) = a * b
f (generic function with 1 method)

It works as expected on two scalars:

julia> f(2, 3)
6 

But it's easy to apply this function to an array. Just use the dot syntax:

julia> f.([1, 4, 2, 8, 7], 10)
5-element Array{Int64,1}:
 10
 40
 20
 80
 70
julia> f.(100, 1:10)
10-element Array{Int64,1}:
  100
  200
  300
  400
  500
  600
  700
  800
  900
 1000

In the first example, Julia automatically treated the second argument as if it was an array, so that the multiplication would work correctly.

Watch out for this when combining ranges and vectorized functions:

julia> 0:10 .* 0.5 |> collect
6-element Array{Float64,1}:
 0.0
 1.0
 2.0
 3.0
 4.0
 5.0

julia> 0.5 .* 0:10  |> collect
11-element Array{Float64,1}:
  0.0
  1.0
  2.0
  3.0
  4.0
  5.0
  6.0
  7.0
  8.0
  9.0
 10.0

The first example is equivalent to 0:(10 .* 0.5), and you might have intended (0:10) .* 0.5.

min() and max()

[edit | edit source]

Watch out for max() and min(). You might think that max() can be used on an array, like this, to find the largest element:

julia> r = rand(0:10, 10)
10-element Array{Int64,1}:
 3
 8
 4
 3
 2
 5
 7
 3
10
10 

but no…

julia> max(r)
LoadError: MethodError: no method matching max(::Array{Int64,1})
...

The max function returns the largest of its arguments. To find the largest element in an array, you can use the related function maximum():

julia> maximum(r)
10

You can use max() on two or more arrays to carry out an elementwise examination, returning another array containing the maximum values:

julia> r = rand(0:10, 10); s = rand(0:10, 10); t = rand(0:10,10);
julia> max(r, s, t)
10-element Array{Int64,1}:
 8
 9
 7
 5
 8
 9
 6
10
 9
 9

min() and minimum() behave in a similar way.

A way to make max work on an array is to use the ellipsis (splat) operator:

julia> max(r...)
9

You can test each value of an array and change it in a single operation, using element-wise operators. Here's an array of random integers from 0 to 10:

julia> a = rand(0:10,10, 10)
10x10 Array{Int64,2}:
10   5   3   4  7   9  5   8  10   2
 6  10   3   4  6   1  2   2   5  10
 7   0   3   4  1  10  7   7   0   2
 4   9   5   2  4   2  1   6   1   9
 0   0   6   4  1   4  8  10   1   4
10   4   0   5  1   0  4   4   9   2
 9   4  10   9  6   9  4   5   1   1
 1   9  10  10  1   9  3   2   3  10
 4   6   3   2  7   7  5   4   6   8
 3   8   0   7  1   0  1   9   7   5

Now you can test each value for being equal to 0, then set only those elements to 11, like this:

julia> a[a .== 0] .= 11;
julia> a
10x10 Array{Int64,2}:
10   5   3   4  7   9  5   8  10   2
 6  10   3   4  6   1  2   2   5  10
 7  11   3   4  1  10  7   7  11   2
 4   9   5   2  4   2  1   6   1   9
11  11   6   4  1   4  8  10   1   4
10   4  11   5  1  11  4   4   9   2
 9   4  10   9  6   9  4   5   1   1
 1   9  10  10  1   9  3   2   3  10
 4   6   3   2  7   7  5   4   6   8
 3   8  11   7  1  11  1   9   7   5

This works because a .== 0 returns an array of true and false values, and these are then used to select the elements of a which are to be set to 11.

If you're doing arithmetic on 2D matrices, you might want to read more about matrix arithmetic: Matrix arithmetic

Rows and Columns

[edit | edit source]

With a 2D array, you use brackets, colons, and commas to extract individual rows and columns or ranges of rows and columns.

With this table:

julia> table = [r * c for r in 1:5, c in 1:5]
5x5 Array{Int64,2}:
1   2   3   4   5
2   4   6   8  10
3   6   9  12  15
4   8  12  16  20
5  10  15  20  25

you can find a single row using the following (notice the comma):

julia> table[1, :]
1x5 Array{Int64,2}:
5-element Array{Int64,1}:
 1
 2
 3
 4
 5

and you can get a range of rows with a range followed by a comma and a colon:

julia> table[2:3,:]
2x5 Array{Int64,2}:
2  4  6   8  10
3  6  9  12  15

To select columns, start with a colon followed by a comma:

julia> table[:, 2]
5-element Array{Int64,1}:
 2
 4
 6
 8
10

On its own, the colon accesses the entire array:

julia> table[:]
25-element Array{Int64,1}:
 1
 2
 3
 4
 5
 2
 4
 6
 8
10
 3
 6
 9
12
15
 4
 8
12
16
20
 5
10
15
20
25 

To extract a range of columns:

julia> table[:, 2:3]
5x2 Array{Int64,2}:
 2   3
 4   6
 6   9
 8  12
10  15

Finding items in arrays

[edit | edit source]

If you want to know whether an array contains an item, use the in() function, which can be called in two ways:

julia> a = 1:10
julia> 3 in a
true

Or phrased as a function call:

julia> in(3, a) # needle ... haystack
true

There's a set of functions starting with find — such as findall(), findfirst(), findnext(), findprev() and findlast() — that you can use to get the index or indices of array cells that match a specific value, or pass a test. Each of these has two or more more forms.

Here's an array of small primes:

julia> smallprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];

To find the first occurrence of a number, and obtain its index, you can use the following method of the findfirst() function:

julia> findfirst(isequal(13), smallprimes)
6

so the first occurrence of 13 in the array is in the sixth cell:

julia> smallprimes[6]
13

This function is similar to many in Julia which accepts a function as the first argument. The function is applied to each element of an array, and if the function returns true, that element or its index is returned. This function returns the index of the first element.

Here's another example using an anonymous function:

julia> findfirst(x -> x == 13, smallprimes)
6

The findall() function returns an array of indices, pointing to every element where the function returns true when applied:

julia> findall(isinteger, smallprimes)
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
julia> findall(iseven, smallprimes)
1-element Array{Int64,1}:
1

Remember that these are arrays of index numbers, not the actual cell values. The indices can be used to extract the corresponding values using the standard square bracket syntax:

julia> smallprimes[findall(isodd, smallprimes)]
9-element Array{Int64,1}:
 3
 5
 7
11
13
17
19
23
29

whereas findfirst() returns a single number — the index of the first matching cell:

julia> findfirst(iseven, smallprimes)
1
julia> smallprimes[findfirst(iseven, smallprimes)]
2

The findnext() function is very similar to the findall() and findfirst() functions, but accepts an additional number that tells the functions to start the search from somewhere in the middle of the array, rather than from the beginning. For example, if findfirst(smallprimes,13) finds the index of the first occurrence of the number 13 in the array, we can continue the search from there by using this value in findnext():

julia> findnext(isodd, smallprimes, 1 + findfirst(isequal(13), smallprimes))
7
julia> smallprimes[ans]
17

To return the indices of the elements in array B where the elements of array A can be found, use findall(in(A), B):

julia> findall(in([11, 5]), smallprimes)
2-element Array{Int64,1}:
3
5

julia> smallprimes[3]
5

julia> smallprimes[5]
11

The order in which the indices are returned should be noted.

Finding out about an array

[edit | edit source]

With our 2D array:

julia> a2 = [1 2 3; 4 5 6; 7 8 9]
3x3 Array{Int64,2}:
1  2  3
4  5  6
7  8  9

we can find out more about it using the following functions:

  • ndims()
  • size()
  • length()
  • count()

ndims() returns the number of dimensions, i.e. 1 for a vector, 2 for a table, and so on:

julia> ndims(a2)
2

size() returns the row and column count of the array, in the form of a tuple:

julia> size(a2)
(3,3)

length() tells you how many elements the array contains:

julia> length(a2)
9

You can use count() to find out how many times a particular value occurs. For example, how many non-zero items are there?

julia> count(!iszero, a2)
9

For finding the inverse, determinant and other aspects of an array/matrix, see Manipulating matrices.

To convert between index numbers (1 to n) and row/column numbers (1:r, 1:c), you can use:

julia> CartesianIndices(a2)[6]
CartesianIndex(3, 2)

to find the row and column for the sixth element, for example.

And to go in the other direction, what index number corresponds to row3, column 2? Use the opposite of Cartesian indices, Linear indices:

julia> LinearIndices(a2)[3, 2]
6

diff() is useful to find the differences between each element of an array:

julia> [2x for x in 1:10] 
10-element Array{Int64,1}:
  2
  4
  6
  8
 10
 12
 14
 16
 18
 20
  
julia> [2x for x in 1:10] |> diff
9-element Array{Int64,1}:
 2
 2
 2
 2
 2
 2
 2
 2
 2

Comparing arrays

[edit | edit source]

union() builds a new array that's the union or combination of two or more arrays. The operation removes duplicates, and the result contains a single version of each element:

julia> odds = collect(1:2:10)
5-element Array{Int64,1}:
1
3
5
7
9
julia> evens = collect(2:2:10)
5-element Array{Int64,1}:
 2
 4
 6
 8
10
julia> union(odds, evens)
10-element Array{Int64,1}:
 1
 3
 5
 7
 9
 2
 4
 6
 8
10

Notice that the ordering of the new union reflects the original order. This example doesn't sort the numbers at all:

julia> union(1:5, 1:10, 5:-1:-5)
16-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 0
-1
-2
-3
-4
-5

intersect() returns a new array that's the intersection of two or more arrays. The result contains one occurrence of each element, but only if it occurs in every array:

julia> intersect(1:10, 5:15)
5:10
julia> intersect(5:20, 1:15, 3:12)
5:12

setdiff() finds the difference between two arrays, i.e. the elements that are in the first array but not the second:

julia> setdiff(1:15, 5:20)
4-element Array{Int64,1}:
1
2
3
4
julia> setdiff(5:20, 1:15)
5-element Array{Int64,1}:
16
17
18
19
20

Filtering

[edit | edit source]

There's a set of related functions that let you work on an array's elements.

filter() finds and keeps elements if they pass a test. Here, we're using the isodd() function (passing it as a named function without parentheses, rather than a function call with parentheses) to filter (keep) everything in the array that's odd.

julia> filter(isodd, 1:10)
5-element Array{Int64,1}:
 1
 3
 5
 7
 9

Like many Julia functions, there's a version which changes the array. So filter() returns a copy of the original, but filter!() changes the array.

The count() function we met earlier is like filter(), but just counts the number of elements that satisfy the condition:

julia> count(isodd, 1:100)
50

Also, the any() function just tells you whether any of the elements satisfy the condition:

julia> any(isodd, 1:100)
true

and the all() function tells you if all of the elements satisfy the condition. Here, all() checks to see whether filter() did the job properly.

julia> all(isodd, filter(isodd, 1:100))
true

Random element of an array

[edit | edit source]

To choose a random element from an array:

julia> a = collect(1:100);
julia> a[rand(1:end)]
14

Other functions

[edit | edit source]

Because arrays are fundamental to Julia, there are dozens of array-handling functions that can't be described here. But here are a few selections:

Find the extreme values of an array:

julia> a = rand(100:110, 10)

10-element Array{Int64,1}:
 109
 102
 104
 108
 103
 110
 100
 108
 101
 101
julia> extrema(a)
(100,110)

findmax() finds the maximum element and returns it and its index in a tuple:

julia> findmax(a)
(110,6)

Use argmax() to return just the index.

The maximum() and minimum() functions let you supply functions to determine how the "maximum" is determined. This is useful if your arrays are not simple vectors. This example find the maximum array element, where maximum here means, "has the largest last value":

julia> maximum(x -> last(x), [(1, 2), (2, 23), (8, 12), (7, 2)])
23

Functions such as sum(), prod(), mean(), middle(), do what you would expect:

(mean() and middle() have been moved into the Statistics module in the standard library; you may need to first enter "using Statistics" to use them)

julia> sum(a)
1046
julia> prod(1:10)
3628800
julia> mean(a)
104.6
julia> middle(a)
105.0

sum(), mean(), and prod() also let you supply functions: the function is applied to each element and then the results are summed/mean-ed/prod-ded:

julia> sum(sqrt, 1:10)  # the sum of the square roots of the first 10 integers
22.4682781862041

julia> mean(sqrt, 1:10) # the mean of the square roots of the first 10 integers 
2.24682781862041

There are functions in the Combinatorics.jl package that let you find combinations and permutations of arrays. combinations() finds all the possible combinations of elements in an array: you can specify how many elements in each combination:

julia> ]
(v1.0) pkg> add Combinatorics # (do this just once)
julia> using Combinatorics
julia> collect(combinations(a, 3))
120-element Array{Array{Int64,1},1}:
 [109,102,104]
 [109,102,108]
 [109,102,103]
 [109,102,110]
 [109,102,100]
 [109,102,108]
 [109,102,101]
 [109,102,101]
 [109,104,108]
 [109,104,103]
 [109,104,110]
 [109,104,100]
 [109,104,108]
 ⋮            
 [103,108,101]
 [103,101,101]
 [110,100,108]
 [110,100,101]
 [110,100,101]
 [110,108,101]
 [110,108,101]
 [110,101,101]
 [100,108,101]
 [100,108,101]
 [100,101,101]
 [108,101,101]

and permutations() generates all permutations. There are a lot — in practice you probably won't need to use collect() to collect the items into an array:

julia> length(permutations(a))
3628800

Modifying array contents: adding and removing elements

[edit | edit source]

To add an item at the end of an array, use push!():

julia> a = collect(1:10); push!(a, 20)
11-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
20

As usual, the exclamation mark reminds you that this function changes the array. You can push only onto the end of vectors.

To add an item at the front, use pushfirst!():

julia> pushfirst!(a, 0)
12-element Array{Int64,1}:
 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
20

To insert an element into an array at a given index, use the splice!() function. For example, here's a list of numbers with an obvious omission:

julia> a = [1, 2, 3, 5, 6, 7, 8, 9]
8-element Array{Int64,1}:
1
2
3
5
6
7
8
9

Use splice!() to insert a sequence at a specific range of index values. Julia returns the values that were replaced. The array grows larger to accommodate the new elements, and elements after the inserted sequence are pushed down. Let's insert, at position 4:5, the range of numbers 4:6:

julia> splice!(a, 4:5, 4:6)
2-element Array{Int64,1}:
 5
 6

You'll be tempted to check that the new values were inserted correctly:

julia> a
9-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9

Now, if you want to insert some values at a specific inter-index location, you will have to use a feature known as empty ranges. In this case the interspace between indexes n-1 and n is denoted as n:n-1.

For example:

julia> L = ['a','b','f']
3-element Array{Char,1}:
 'a'
 'b'
 'f'
julia> splice!(L, 3:2, ['c','d','e'])
0-element Array{Char,1}
julia> L
6-element Array{Char,1}:
 'a'
 'b'
 'c'
 'd'
 'e'
 'f'

Removing elements

[edit | edit source]

If you don't supply a replacement, you can also use splice!() can remove elements and move the rest of them along.

julia> a = collect(1:10); 
julia> splice!(a,5);
julia> a
9-element Array{Int64,1}:
 1
 2
 3
 4
 6
 7
 8
 9
10

To remove the last item:

julia> pop!(a)
10

and the first:

julia> popfirst!(a)
1

More aggressive modification of arrays (and similar data structures) can be made with functions such as deleteat!() and splice!(). You can find out the indices of elements in various ways. Once you know the indices, you can use deleteat!() to delete an element, given its index number:

julia> a = collect(1:10);
julia> findfirst(isequal(6), a)
6
julia> deleteat!(a, findfirst(isequal(6), a))
9-element Array{Int64,1}:
 1
 2
 3
 4
 5
 7
 8
 9
10

deleteat!() also accepts a range or iterator to specify the indices, so you can do this:

julia> deleteat!(a, 2:6)
4-element Array{Int64,1}:
  1
  8
  9
 10

Remember that you can always remove a group of elements using a filter: see Filtering.

Other functions

[edit | edit source]

If you want to do something to an array, there's probably a function to do it, and sometimes with an exclamation mark to remind you of the potential consequences. Here are a few more of these array-modifying functions:

  • resize!() change the length of a Vector
  • append!() push a second collection at the back of the first one
  • prepend!() insert elements at the beginning of the first Vector
  • empty!(a) remove all elements
  • unique(a) remove duplicate elements from the array "a" without overwriting the array.
  • unique!(a) remove duplicate elements from the array "a" and overwrites the array.
  • rotr90(a) make a copy of an array rotated 90 degrees clockwise:
julia> rotr90([1 2 3 ; 4 5 6])
3x2 Array{Int64,2}:
4  1
5  2
6  3
  • circshift(a) move the elements around 'in a circle' by a number of steps:
julia> circshift(1:6, 1)
6-element Array{Int64,1}:
 6
 1
 2
 3
 4
 5

This function can also do circular shifts on 2D arrays too. For example, here's a table:

julia> table = collect(r*c for r in 1:5, c in 1:5)
5×5 Array{Int64,2}:
 1   2   3   4   5
 2   4   6   8  10
 3   6   9  12  15
 4   8  12  16  20
 5  10  15  20  25

By supplying a tuple you can move rows and columns. For example: moving the columns by 0 and the rows by 1 moves the first dimension by 0 and the second by 1. The first dimension is downwards, the second rightwards:

julia> circshift(table, (0, 1))
5×5 Array{Int64,2}:
  5  1   2   3   4
 10  2   4   6   8
 15  3   6   9  12
 20  4   8  12  16
 25  5  10  15  20

There's a modifying version of circshift(), circshift!

Setting the contents of arrays

[edit | edit source]

To set the contents of an array, specify the indices on the left-hand side of an assignment expression:

julia> a = collect(1:10);

julia> a[9]= -9
-9

To check that the array has really changed:

julia> print(a)
[1,2,3,4,5,6,7,8,-9,10]

You can set a bunch of elements at the same time, using the broadcasting assignment operator:

julia> a[3:6] .= -5
4-element view(::Array{Int64,1}, 3:6) with eltype Int64:
 -5
 -5
 -5
 -5
julia> print(a)
[1,2,-5,-5,-5,-5,7,8,-9,10]

And you can set a sequence of elements to a suitable sequence of values:

julia> a[3:9] = collect(9:-1:3)
7-element Array{Int64,1}:
9
8
7
6
5
4
3

Notice here that, although Julia shows the 7 element slice as the return value, in fact the whole array has been modified:

julia> a
10-element Array{Int64,1}:
 1
 2
 9
 8
 7
 6
 5
 4
 3
10

You can set ranges to a single value in one operation using broadcasting:

julia> a[1:5] .= 0
0
julia> a
10-element Array{Int64,1}:
  0
  0
  0
  0
  0
  6
  7
  8
  9
 10
julia> a[1:10] .= -1;
-1
julia> print(a)
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]

As an alternative to the square bracket notation, there's a function call version that does the same job of setting array contents, setindex!():

julia> setindex!(a, 1:10, 10:-1:1)
10-element Array{Int64,1}:
10
 9
 8
 7
 6
 5
 4
 3
 2
 1

You can refer to the entire contents of an array using the colon separator without start and end index numbers, i.e. [:]. For example, after creating the array a:

julia> a = collect(1:10);

we can refer to the contents of this array a using a[:]:

julia> b = a[:]
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

julia> b[3:6]
4-element Array{Int64,1}:
3
4
5
6

Passing arrays to functions

[edit | edit source]

A function can't modify a variable passed to it as an argument, but it can change the contents of a container passed to it.

Consider the following function, that changes its argument to 5:

 julia> function set_to_5(x)
         x = 5
 	end
 set_to_5 (generic function with 1 method)
julia> x = 3
3
julia> set_to_5(x)
5
julia> x
3

Although the x inside the function is changed, the x outside the function isn't. Variable names in functions are local to the function.

But, you can modify the contents of a container, such as an array. The next function uses the [:] syntax to access the contents of the container x, rather than change the value of the variable x:

 julia> function fill_with_5(x)
          x[:] .= 5
        end
 fill_with_5 (generic function with 1 method)

 julia> '''x = collect(1:10)'''
 10-element Array{Int64,1}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

 julia> '''fill_with_5(x)'''
 5

 julia> '''x'''
 10-element Array{Int64,1}:
 5
 5
 5
 5
 5
 5
 5
 5
 5
 5

If, instead of accessing the container variable's contents, you try to change the variable itself, it won't work. For example, the following function definition creates an array of 5s in temp and then attempts to change the argument x to be temp.

 julia> function fail_to_fill_with_5(x)
          temp = similar(x)
          for i in eachindex(x)
             temp[i] = 5
          end
          x = temp
        end
 fail_to_fill_with_5 (generic function with 1 method)
julia> x = collect(1:10)
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
julia> fail_to_fill_with_5(x)
10-element Array{Int64,1}:
5
5
5
5
5
5
5
5
5
5

It looks like it worked, but:

julia> x
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

You can change elements of the array, but you can't change the variable so that it points to a different array. In other words, your function isn't allowed to change the binding between the argument and the array that was passed to it.

Julia's way of handling function arguments is described as “pass-by-sharing”. An array isn't copied when you pass it to a function (that would be very inefficient for large arrays).

Matrix arithmetic

[edit | edit source]

For matrix-on-matrix arithmetic action, you can:

- add (+) and subtract (-):

julia> A = reshape(1:12, 3, 4)
  3x4 Array{Int64,2}:
  1  4  7  10
  2  5  8  11
  3  6  9  12

julia> B = ones(3,4)
 3x4 Array{Float64,2}:
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0

julia> A + B
 3x4 Array{Float64,2}:
 2.0  5.0   8.0  11.0
 3.0  6.0   9.0  12.0
 4.0  7.0  10.0  13.0


julia> A - B
 3x4 Array{Float64,2}:
  0.0  3.0  6.0   9.0
  1.0  4.0  7.0  10.0
  2.0  5.0  8.0  11.0


- multiply (*), assuming the dimensions are compatible, so m1 * m2 is possible if last(size(m1)) == first(size(m2)). Note the difference between matrix multiplication and elementwise matrix multiplication. Here's a matrix A:

julia> A = [1 2 ; 3 4]
  2x2 Array{Int64,2}:
  1  2
  3  4

and here's matrix B:

julia> B = [10 11 ; 12 13]
  2x2 Array{Int64,2}:
  10  11
  12  13

The .* broadcasting operator multiplies them elementwise:

julia> A .* B
  2x2 Array{Int64,2}:
  10  22
  36  52

Compare this with matrix multiplication, A * B:

julia> A * B
 2x2 Array{Int64,2}:
  34  37
  78  85

which is:

julia> [1 * 10 + 2 * 12        1 * 11 + 2 * 13  ;      3 * 10 + 4 * 12     3 * 11 + 4 * 13]
 2x2 Array{Int64,2}:
  34  37
  78  85

- division of two matrices. You can use the backslash (\) for left division:

julia> A = rand(1:9, 3, 3)
 3x3 Array{Int64,2}:
  5  4  3
  8  7  7
  9  3  7
 julia> B = rand(1:9, 3, 3)
 3x3 Array{Int64,2}:
  6  5  5
  6  7  5
  7  2  7
 julia> A \ B
 3x3 Array{Float64,2}:
2.01961    0.411765   1.84314
0.254902   1.35294   -0.0392157
  -1.70588   -0.823529  -1.35294

and the forward slash (/) right or slash division:

julia> A / B
3x3 Array{Float64,2}:
4.0       -2.0       -1.0     
0.285714   0.714286   0.285714
5.07143   -3.07143   -0.428571

With a matrix and a scalar, you can add, subtract, multiply, and divide:

julia> A + 1
3x3 Array{Int64,2}:
  6  5  4
  9  8  8
 10  4  8
julia> [1 2 3 4 5] * 2
1x5 Array{Int64,2}:
 2  4  6  8  10
julia> A .- 1
3x3 Array{Int64,2}:
 4  3  2
 7  6  6
 8  2  6
julia> A .* 2
3x3 Array{Int64,2}:
 10   8   6
 16  14  14
 18   6  14
julia> A ./ 2
3x3 Array{Float64,2}:
 2.5  2.0  1.5
 4.0  3.5  3.5
 4.5  1.5  3.5

and more besides:

julia> A // 2
3x4 Array{Rational{Int64},2}:
 1//2  2//1  7//2   5//1
 1//1  5//2  4//1  11//2
 3//2  3//1  9//2   6//1
julia> A .< 6
3x3 BitArray{2}:
  true   true   true
 false  false  false
 false   true  false

You can multiply matrix and a vector (the matrix-vector product), if the arrays have compatible shapes. Here's the matrix A:

julia> A = reshape(1:12, 3, 4)
  3x4 Array{Int64,2}:
   1  4  7  10
   2  5  8  11
   3  6  9  12

and here's a vector V:

julia> V = collect(1:4)
  4-element Array{Int64,1}:
   1
   2
   3
   4

The * operator multiplies them:

julia> A * V
  3-element Array{Int64,1}:
   70
   80
   90

The dot or inner product (aTb) can be found using the dot() function, but you'll have to import the LinearAlgebra library first:

julia> using LinearAlgebra


julia> dot([1:3...], [21:23...])
  134

julia> (1 * 21) + (2 * 22) +  (3 * 23)
134

The two arguments must have the same length. You can also use the dot operator, which you can obtain in the REPL by typing "\cdot" followed by a tab:

julia> [1:3] ⋅ [21:23]
134

Joining arrays and matrices

[edit | edit source]

You can use hcat() and vcat() to join matrices together, if their dimensions permit.

hcat() keeps the first dimension and extends (joins) in the second, vcat() keeps the second dimension and extends the first.

Here are two 3 by 4 matrices:

julia> A = reshape(1:12, 3, 4)
3x4 Array{Int64,2}:
 1  4  7  10
 2  5  8  11
 3  6  9  12
julia> B = reshape(100:100:1200, 3, 4)
3x4 Array{Int64,2}:
 100  400  700  1000
 200  500  800  1100
 300  600  900  1200

hcat(A, B) makes a new array that still has 3 rows, but extends/joins the columns to make 8 in total:

julia> hcat(A, B)
3x8 Array{Int64,2}:
 1  4  7  10  100  400  700  1000
 2  5  8  11  200  500  800  1100
 3  6  9  12  300  600  900  1200

vcat(A, B) makes a new array that keeps the 4 columns, but extends to 6 rows:

julia> vcat(A, B)
6x4 Array{Int64,2}:
   1    4    7    10
   2    5    8    11
   3    6    9    12
 100  400  700  1000
 200  500  800  1100
 300  600  900  1200

You'll probably find the shortcuts useful:

  • [A ; B ] is vcat(A, B)
  • [A B ] is hcat(A, B)

vec() flattens a matrix into a vector, turning it into a (what some call a 'column') vector:

 julia> vec(ones(3, 4))
12-element Array{Float64,1}:
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0

There's also an hvcat() function ([A B; C D;]) that does both.

You can use hcat() to convert an array of arrays to a matrix (using the hcat-splat):

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

julia> hcat(a...)
2x3 Array{Int64,2}:
 1  3  5
 2  4  6

Julia arrays are 'column-major'. This means that you read down the columns:

 1  3
 2  4

whereas 'row-major' arrays are to be read across, like this:

 1  2
 3  4

Column-major order is used in Fortran, R, Matlab, GNU Octave, and by the BLAS and LAPACK engines (the "bread and butter of high-performance numerical computation"). Row-major order is used in C/C++, Mathematica, Pascal, Python, C#/CLI/.Net and others.

Growing or extending arrays

[edit | edit source]

Often you want to create an array and then add more to it, or 'grow' it. While can do this with vcat() and hcat(), be aware that both these operations create new temporary arrays and copy elements, so they don't always produce the fastest code. A better way is to use push!. This is an efficient operation that extends the array. You can reshape the array later:

julia> a = []
julia> for i = 1:80
    push!(a, i)
end

julia> a
80-element Array{Any,1}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
  ⋮
 75
 76
 77
 78
 79
 80

reshape() lets you change the dimensions of an array. You can supply the dimensions or use a colon (:) to ask Julia to calculate valid dimensions:

julia> reshape(a, 10, :)
10x8 Array{Any,2}:
  1  11  21  31  41  51  61  71
  2  12  22  32  42  52  62  72
  3  13  23  33  43  53  63  73
  4  14  24  34  44  54  64  74
  5  15  25  35  45  55  65  75
  6  16  26  36  46  56  66  76
  7  17  27  37  47  57  67  77
  8  18  28  38  48  58  68  78
  9  19  29  39  49  59  69  79
 10  20  30  40  50  60  70  80

reshape(a, (10, div(length(a), 10))) would have the same effect.

push!() doesn't let you push new rows to a 2D array or matrix. The best way to do the job is to work on a 1D array, as above, adding more elements at the end, and then use reshape() to convert it to two dimensions. If necessary, use transpose() to flip the matrix.

Manipulating matrices

[edit | edit source]

To transpose an array or matrix, there's an equivalent ' operator for the transpose() function, to swap rows and columns:

julia> M = reshape(1:12, 3, 4)
3×4 Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}}:
 1  4  7  10
 2  5  8  11
 3  6  9  12
julia> transpose(M)
4x3 Array{Int64,2}:
  1   2   3
  4   5   6
  7   8   9
 10  11  12
julia> M' 
4x3 Array{Int64,2}:
  1   2   3
  4   5   6
  7   8   9
 10  11  12

To find the determinant of a square matrix, use det(), after remembering to load the LinearAlgebra library.

julia> using LinearAlgebra
julia> A = rand(2:10, 3, 3)
3x3 Array{Int64,2}:
 8  8   2
 6  9   6
 9  2  10
julia> det(A)
438.00000000000006

inv() (in the Standard Library) finds the inverse of a square matrix, if it has one. (If the determinant of the matrix is zero, it won't have an inverse.)

julia> inv(A)
3x3 Array{Float64,2}:
  0.178082   -0.173516   0.0684932
 -0.0136986   0.141553  -0.0821918
 -0.157534    0.127854   0.0547945

LinearAlgebra.rank() finds the rank of the matrix, and LinearAlgebra.nullspace() finds the basis for the nullspace.

julia> A
3x4 Array{Int64,2}:
 1  4  7  10
 2  5  8  11
 3  6  9  12
julia> rank(A)
2
julia> nullspace(A)
4x2 Array{Float64,2}:
 -0.475185  -0.272395
  0.430549   0.717376
  0.564458  -0.617566
 -0.519821   0.172585

LinearAlgebra.tr() sums the diagonal of a square matrix (trace):

julia> s = reshape(1:9, 3, 3)
3x3 Array{Int64,2}:
 1  4  7
 2  5  8
 3  6  9
julia> tr(s)
15

Applying functions to matrices

[edit | edit source]

There are a number of functions that can be applied to a matrix:

- sum() adds every element:

julia> A = reshape(1:9, 3, 3)
3×3 Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}}:
 1  4  7
 2  5  8
 3  6  9
julia> sum(A)
45

You can specify a dimension if you want to sum just columns or rows. So to sum columns, specify dimension 1:

julia> sum(A, dims=(1))
1x3 Array{Int64,2}:
 6  15  24

To sum rows, specify dimension 2:

julia> sum(A, dims=(2))
3x1 Array{Int64,2}:
 12
 15
 18

- mean() finds the mean of the values in the matrix:

julia> using Statistics; mean(A)
5.0

As with sum(), you can specify a dimension, so that you can find the mean of columns (use dimension 1) or rows (use dimension 2):

julia> mean(A, dims=(1))
1x3 Array{Float64,2}:
 2.0  5.0  8.0
julia> mean(A, dims=(2))
3x1 Array{Float64,2}:
 4.0
 5.0
 6.0

- the min.(A, B) and max.(A, B) functions compare two (or more) arrays element by element, returning a new array with the largest (or smallest) values from each:

julia> A = rand(-1:2:1, 3, 3)
3x3 Array{Int64,2}:
 -1  -1  -1
 -1   1   1
  1  -1   1
 
julia> B = rand(-2:4:2, 3, 3)
3x3 Array{Int64,2}:
 2   2  2
 2  -2  2
 2   2  2
 
julia> min.(A, B)
3×3 Array{Int64,2}:
 1  -2  -2
-1  -2  -1
 1   1  -1
 
julia> max.(A, B)
3×3 Array{Int64,2}:
2  1  1
2  1  2
2  2  2

prod() multiplies a matrix's elements together:

julia> A = reshape(collect(BigInt(1):25), 5, 5)
5×5 Array{BigInt,2}:
 1   6  11  16  21
 2   7  12  17  22
 3   8  13  18  23
 4   9  14  19  24
 5  10  15  20  25

julia> prod(A)
15511210043330985984000000

(Notice the use of BigInt, products are very large.)

You can specify a dimension if you want to multiply just columns or rows. To multiply the elements of columns together, specify dimension 1; for rows, use dimension 2:

julia> prod(A, dims=1)
1x5 Array{Int64,2}:
 120  30240  360360  1860480  6375600
julia> prod(A, dims=2)
5x1 Array{Int64,2}:
  22176
  62832
 129168
 229824
 375000

Matrix norms

[edit | edit source]

Most of these functions live in the LinearAlgebra library:

julia> using LinearAlgebra

Vector norms

[edit | edit source]

The Euclidean norm, , is found by LinearAlgebra.norm(x):

julia> X = [2, 4, -5]
3-element Array{Int64,1}:
  2
  4
 -5
 
julia> LinearAlgebra.norm(X) # Euclidean norm
6.708203932499369

julia> LinearAlgebra.norm(X, 1) # 1-norm of the vector, the sum of element magnitudes
11.0

If X is a 'row' vector:

julia> X = [2 4 -5]
1x3 Array{Int64,2}:
 2  4  -5

julia> LinearAlgebra.norm(X)
6.708203932499369

julia> LinearAlgebra.norm(X, 1)
11.0

The Euclidean distance between vectors and , given by , is found by norm(x - y):

julia> LinearAlgebra.norm([1 2 3] - [2 4 6])
3.741657386773941

julia> LinearAlgebra.norm([1, 2, 3] - [2, 4, 6])
3.741657386773941

The angle between two vectors and is :

acos(dot(a,b)/(norm(a)*norm(b)))

Matrix norms

[edit | edit source]

Here's the 1-norm of a matrix (the maximum absolute column sum):

julia> B = [5 -4 2 ; -1 2 3; -2 1 0]
3x3 Array{Int64,2}:
  5  -4  2
 -1   2  3
 -2   1  0
julia> LinearAlgebra.opnorm(B, 1)
8.0

And here's the infinity norm (the maximum absolute row sum):

julia> LinearAlgebra.opnorm(B, Inf)
11.0

Note they are different from vectorized 1-norm or infinity norm:

julia> LinearAlgebra.norm(B, 1)
20.0
julia> LinearAlgebra.norm(B, Inf)
5.0

The Euclidean norm() is the default:

julia> LinearAlgebra.norm([2 3 ; 4 6]), LinearAlgebra.opnorm([2 3 ; 4 6]), sqrt(2^2 + 3^2 + 4^2 + 6^2)
(8.062257748298547,8.062257748298547,8.06225774829855)

Scaling and rotating matrices

[edit | edit source]

- rmul!(A, n) scales every element of the matrix in place by a scale factor n:

julia> A = [1 2 3  
            4 5 6  
            7 8 9] 
3×3 Array{Int64,2}:
 1  2  3
 4  5  6
 7  8  9

julia> rmul!(A, 2)
3×3 Array{Int64,2}:
  2   4   6
  8  10  12
 14  16  18

There are rotation and circular-shifting functions too:

julia> A = [1 2 3  
            4 5 6  
            7 8 9] 
3×3 Array{Int64,2}:
 1  2  3
 4  5  6
 7  8  9
julia> rot180(A)
3×3 Array{Int64,2}:
 9  8  7
 6  5  4
 3  2  1
julia> circshift(A, (1, 1))
3×3 Array{Int64,2}:
 9  7  8
 3  1  2
 6  4  5
julia> A
3×3 Array{Int64,2}:
 1  2  3
 4  5  6
 7  8  9

reverse() makes a copy of a matrix reversing rows or columns:

julia> reverse(A, dims=(1))
3×3 Array{Int64,2}:
 7  8  9
 4  5  6
 1  2  3
julia> reverse(A, dims=(2))
3×3 Array{Int64,2}:
 3  2  1
 6  5  4
 9  8  7

squeeze() and reshape() can be used to change the dimensions of a matrix. For example, this is how you can use squeeze() to collapse a row vector (1 by 4) into a 4 by 1 array:

julia> a = [1 2 3 4]
1x4 Array{Int64,2}:
1  2  3  4
julia> ndims(a)
2
julia> b = squeeze(a, dims=(1))
4-element Array{Int64,1}:
 1
 2
 3
 4
julia> ndims(b)
1

Sorting arrays

[edit | edit source]

Julia has a flexible sort() function that returns a sorted copy of an array, and a companion sort!() version that changes the array so that it's sorted.

You can usually use sort() without options and obtain the results you'd hoped for:

julia> using Random
julia> rp = randperm(10)
10-element Array{Int64,1}:
 6
 4
 7
 3
10
 5
 8
 1
 9
 2
julia> sort(rp)
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

You can sort 2D arrays:

julia> a = reshape(rand(1:20, 20), 4, 5)
4x5 Array{Int64,2}:
19  13   4  10  10
 6  20  19  18  12
17   7  15  14   9
 1  16   8   7  13
julia> sort(a, dims=(1)) # sort each column, dimension 1
4x5 Array{Int64,2}:
 1   7   4   7   9
 6  13   8  10  10
17  16  15  14  12
19  20  19  18  13
julia> sort(a, dims=(2)) # sort each row, dimension 2
4x5 Array{Int64,2}:
4  10  10  13  19
6  12  18  19  20
7   9  14  15  17
1   7   8  13  16

although there are more powerful alternatives in sortrows() and sortcolumns() — see below for details.

The sortperm() function is similar to sort(), but it doesn't return a sorted copy of the collection. Instead it returns a list of indices that could be applied to the collection to produce a sorted version:

julia> r = rand(100:110, 10)
10-element Array{Int64,1}:
 103
 102
 110
 108
 108
 108
 104
 109
 106
 106
julia> sortperm(r)
10-element Array{Int64,1}:
  2
  1
  7
  9
 10
  4
  5
  6
  8
  3
julia> r[sortperm(r)] 
10-element Array{Int64,1}:
 102
 103
 104
 106
 106
 108
 108
 108
 109
 110

Sort by and comparisons

[edit | edit source]

If you need more than the default sort() offers, use the by and lt keywords and provide your own functions for processing and comparing elements during the sort.

sort by
[edit | edit source]

The by function processes each element before comparison and provides the 'key' for the sort. A typical example is the task of sorting a list of numbers in string form into numerical order. Here's the list:

julia> r = ["1E10", "150", "25", "3", "1.5", "1E-10", "0.5", ".999"];

If you use the default sort, the numbers appear in the order in which the characters appear in Unicode/ASCII:

julia> sort(r)
8-element Array{ASCIIString,1}:
 ".999"
 "0.5"
 "1.5"
 "150"
 "1E-10"
 "1E10"
 "25"
 "3"

with "1E-10" appearing after "0.999".

To sort the numbers by their value, pass the parse() function (from the Meta package) to by:

julia> sort(r, by = x -> Meta.parse(x))
8-element Array{String,1}:
 "1E-10"
 "0.5"  
 ".999" 
 "1.5"  
 "3"    
 "25"   
 "150"  
 "1E10" 

The strings are sorted 'by' their value. Notice that the by function you supply produces the numerical sort key, but the original string elements appear in the final result.

Anonymous functions can be useful when sorting arrays. Here's a 10 rows by 2 columns array of tuples:

julia> table = collect(enumerate(rand(1:100, 10)))
10-element Array{(Int64,Int64),1}:
(1,86) 
(2,25) 
(3,3)  
(4,97) 
(5,89) 
(6,58) 
(7,27) 
(8,93) 
(9,98) 
(10,12)

You can sort this array by the second element of each tuple, not the first, by supplying an anonymous function to by that points to the second element of each. The anonymous function says, given an object x to sort, sort by the second element of x:

julia> sort(table, by = x -> x[2])
10-element Array{(Int64,Int64),1}:
(3,3)  
(10,12)
(2,25) 
(7,27) 
(6,58) 
(1,86) 
(5,89) 
(8,93) 
(4,97) 
(9,98)
Sorting by multiple columns
[edit | edit source]

You can supply a tuple of "column" identifiers in the by function, if you want to sort by more than one column.

julia>  a = [[2, 2, 2, 1],
             [1, 1, 1, 8],
             [2, 1, 2, 2],
             [1, 2, 2, 5],
             [2, 1, 1, 4],
             [1, 1, 2, 7],
             [1, 2, 1, 6],
             [2, 2, 1, 3]] ;
julia> sort(a, by = col -> (col[1], col[2], col[3]))
8-element Array{Array{Int64,1},1}:
 [1,1,1,8]
 [1,1,2,7]
 [1,2,1,6]
 [1,2,2,5]
 [2,1,1,4]
 [2,1,2,2]
 [2,2,1,3]
 [2,2,2,1]

This sorts the array first by column 1, then by column 2, then by column 3.

Redefining 'less than'
[edit | edit source]

By default, sorting uses the built-in isless() function when comparing elements. In a sorted array, the first element is less than the second.

You can change this behaviour by passing a different function to the lt keyword. This function should compare two elements and return true if they're sorted, i.e. if the first element is 'less than' the second, using some definition of 'less than'. The sorting process compares pairs of elements repeatedly until every element of the array is in the right place.

For example, suppose you want to sort an array of words according to the number of vowels in each word; i.e. the more vowels a word has, the earlier in the sorted results it occurs. For example, the word "orange" will be considered to be "less than" the word "lemon", because it has more vowels.

First we'll need a function that counts vowels:

vowelcount(string) = count(c -> (c in "aeiou"), lowercase(string))

Now you can pass an anonymous function to sort() that compares the vowel count of two elements using this function and then returns the element with a higher count in each case:

 sentence = split("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
 sort(sentence, lt = (x,y) -> vowelcount(x) > vowelcount(y))

The result is that the word with the most vowels appears first:

 19-element Array{SubString{String},1}:
 "adipisicing"
 "consectetur"
 "eiusmod"    
 "incididunt" 
 "aliqua."    
 "labore"     
 "dolore"     
 "Lorem"      
 "ipsum"      
 "dolor"      
 "amet,"      
 "elit,"      
 "tempor"     
 "magna"      
 "sit"        
 "sed"        
 "do"         
 "ut"         
 "et"

The sort() function also lets you specify a reverse sort - after the by and lt functions (if used) have done their work, a true value passed to rev reverses the result.

Sorting 2-D arrays

[edit | edit source]

In Julia 1.0, you can sort multidimensional arrays with sortslices().

Here's a simple array of nine strings (you can also use numbers, symbols, functions, or anything that can be compared):

julia> table = ["F" "B" "I"; "A" "D" "G"; "H" "C" "E"]
3×3 Array{String,2}:
 "F"  "B"  "I"
 "A"  "D"  "G"
 "H"  "C"  "E"

You supply a number or a tuple to the dims ("dimensions") keyword that indicates what you want to sort. To sort the table so that the first column is sorted, use 1:

julia> sortslices(table, dims=1)
3×3 Array{String,2}:
 "A"  "D"  "G"
 "F"  "B"  "I"
 "H"  "C"  "E"

Note that sortslices returns a new array. The first column is in alphabetical order.

Use dims=2 to sort the table so that the first row is sorted:

julia>> sortslices(table, dims=2)
3×3 Array{String,2}:
 "B"  "F"  "I"
 "D"  "A"  "G"
 "C"  "H"  "E"

Now the first row is in alphabetical order.

If you want to sort by something other than the first item, pass a function to by. So, to sort rows so that the middle column is in alphabetical order, use:

julia> sortslices(table, dims=1, by = x -> x[2])
3×3 Array{String,2}:
 "F"  "B"  "I"
 "H"  "C"  "E"
 "A"  "D"  "G"

sortslices has most of the options that you'll find in sort, and more besides. You can reverse the order with rev, change the comparator with lt, and so on.

Tuples

[edit | edit source]

A tuple is an ordered sequence of elements, like an array. A tuple is represented by parentheses and commas, rather than the square brackets used by arrays. Tuples are mostly good for small fixed-length collections — they're used everywhere in Julia, for example, as argument lists and for returning multiple values from functions.

The important difference between arrays and tuples is that tuples are immutable. Other than that, tuples work in much the same way as arrays, and many array functions can be used on tuples too:

julia> t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
(1,2,3,4,5,6,7,8,9,10)
julia> t
(1,2,3,4,5,6,7,8,9,10)
julia> t[6:end]
(6,7,8,9,10)

You can have two-dimensional tuples:

julia> t = ((1, 2), (3, 4))
((1,2),(3,4))
julia> t[1]
(1,2)
julia> t[1][2]
2

But you can't change a tuple:

julia> t[1] = 0
LoadError: MethodError: no method matching set index!...

And, because you can't modify tuples, you can't use any of the functions like push!() that you use with arrays:

julia> a = [1,2,3];
julia> push!(a,4)
4-element Array{Int64,1}:
1
2
3
4
julia> t = (1,2,3);
julia> push!(t,4)
LoadError: MethodError: no method matching push!

Named tuples

[edit | edit source]

A named tuple is like a combination of a tuple and a dictionary. Like a tuple, a named tuple is ordered and immutable, and enclosed in parentheses; like a dictionary, each element has a unique key that can be used to access it.

You can create a named tuple by providing keys and values directly:

julia> shape1 = (corner1 = (1, 1), corner2 = (-1, -1), center = (0, 0))

(corner1 = (1, 1), corner2 = (-1, -1), center = (0, 0))

To access the values, use the familiar dot syntax:

julia> shape1.corner1
(1, 1)

julia> shape1.center
(0, 0)

julia> (shape1.corner1, shape1.corner2)
((1, 1), (-1, -1))

You can access all the values (destructuring) as with ordinary tuples:

julia> c1, c2, centerp = shape1;

julia> c1
(1, 1)

julia> c2
(-1, -1)

or just some of them:

julia> c1, c2 = shape1;

julia> c1
(1, 1)
julia> c2
(-1, -1)

Elements can be the same type, or different types, but the keys will always be variable names.

You can iterate over a named tuple:

julia> for i in shape1
         @show i
       end

i = (1, 1)
i = (-1, -1)
i = (0, 0)

julia> for i in shape1
           println(first(i))
       end

1
-1
0

Another way to create a named tuple is to provide the keys and values in separate tuples:

julia> ks = (:corner1, :corner2)
(:corner1, :corner2)

julia> vs = ((10, 10), (20, 20))
((10, 10), (20, 20))

julia> shape2 = NamedTuple{ks}(vs)
(corner1 = (10, 10), corner2 = (20, 20))

julia>shape2.corner1
(10, 10)

julia> shape2.corner2
(20, 20)

You can combine two named tuples to make a new one:

julia> colors = (top = "red", bottom = "green")
(top = "red", bottom = "green")

julia> merge(shape2, colors)
(corner1 = (10, 10), corner2 = (20, 20), top = "red", bottom = "green")

You can use existing variables for keys:

julia> d = :density;

julia> (corner1 = (10, 10), corner2 = (20, 20), d => 0.99)
(corner1 = (10, 10), corner2 = (20, 20), density = 0.99)

Making single value Named Tuples requires a strategically-placed comma:

julia> shape3 = (corner1 = (1, 1),)

(corner1 = (1, 1),)
julia> typeof(shape3)
NamedTuple{(:corner1,),Tuple{Tuple{Int64,Int64}}}

If you forget it, you'll see this:

julia> (corner1 = (1, 1))
(1, 1)

julia> typeof(corner1)
Tuple{Int64,Int64}

You can make new named tuples by combining named tuples together.

julia> shape3 = merge(shape2, colors)
(corner1 = (10, 10), corner2 = (20, 20), top = "red", bottom = "green")

Use a comma after a single element named tuple:

julia> merge(shape2, (top = "green",))
(corner1 = (10, 10), corner2 = (20, 20), top = "green")

because without the comma, the tuple will be interpreted as a parenthesized keyword argument to merge().

To iterate over the "keys", use the fieldnames() and typeof() functions:

julia> fieldnames(typeof(shape3))
(:corner1, :corner2, :top, :bottom)

so you can do:

julia> for key in fieldnames(typeof(shape3))
     @show getindex(shape3, key)
 end

getindex(shape3, key) = (10, 10)
getindex(shape3, key) = (20, 20)
getindex(shape3, key) = "red"
getindex(shape3, key) = "green"

Merging two tuples is done intelligently. For example, if you have this named tuple:

julia> shape3
(corner1 = (10, 10), corner2 = (20, 20), top = "red", bottom = "green")

and you want to add a center point and change the top color:

julia> merge(shape3, (center = (0, 0), top="green"))

(corner1 = (10, 10), corner2 = (20, 20), top = "green", bottom = "green", center = (0, 0))

the new value is inserted, and the existing value is changed.

Using named tuples as keyword arguments

[edit | edit source]

A named tuple is a convenient way to pass a group of keyword arguments to a function. Here's a function that accepts three keyword arguments:

function f(x, y, z; a=10, b=20, c=30)
    println("x = $x, y = $y, z = $z; a = $a, b = $b, c = $c")
end

You can define a named tuple that contains the names and values for one or more keywords:

options = (b = 200, c = 300)

To pass the named tuple to the function, use the ; when you call the function:

f(1, 2, 3; options...)
x = 1, y = 2, z = 3; a = 10, b = 200, c = 300

If you specify a keyword and value, it can be overridden by a later definition:

f(1, 2, 3; b = 1000_000, options...)
x = 1, y = 2, z = 3; a = 1000, b = 200, c = 300
f(1, 2, 3; options..., b= 1000_000)
x = 1, y = 2, z = 3; a = 10, b = 1000000, c = 300

Types

[edit | edit source]
Previous page
Arrays and tuples
Introducing Julia Next page
Controlling the flow
Types

Types

[edit | edit source]

This section, on types, and the next section, on functions and methods, should ideally be read at the same time, because the two topics are so closely connected.

Types of type

[edit | edit source]

Data elements come in different shapes and sizes, which are called types.

Consider the following numeric values: a floating point number, a rational number, and an integer:

0.5  1//2  1

It's easy for us humans to add these numbers without much thought, but a computer won't be able to use a simple addition routine to add all three values, because the types are different. Code for adding rational numbers has to consider numerators and denominators, whereas code for adding integers won't. The computer will probably have to convert two of these values to be the same type as the third—typically the integer and the rational will first be converted to floating-point—then the three floating-point numbers will be added together.

This business of converting types obviously takes time. So, to write really fast code, you want to make sure that you don't make the computer waste time by continually converting values from one type to another. When Julia compiles your source code (which happens every time you evaluate a function for the first time), any type indications you've provided allow the compiler to produce more efficient executable code.

Another issue with converting types is that in some cases you'll be losing precision—converting a rational number to a floating-point number is likely to lose some precision.

The official word from the designers of Julia is that types are optional. In other words, if you don't want to worry about types (and if you don't mind your code running slower than it might), then you can ignore them. But you'll encounter them in error messages and the documentation, so you will eventually have to tackle them…

A compromise is to write your top-level code without worrying about types, but, when you want to speed up your code, find out the bottlenecks where your program spends the most time, and clean up the types in that area.

The type system

[edit | edit source]

There's a lot to know about Julia's type system, so the official documentation is really the place to go. But here's a brief overview.

Type hierarchy

[edit | edit source]

In Julia types are organized in a hierarchy, with a tree structure.

At the tree's root, we have a special type called Any, and all other types are connected to it directly or indirectly. Informally, we can say that the type Any has children. Its children are called Any's subtypes. And a child's supertype is Any. (Note, however, hierarchical relationships between types are explicitly declared, rather than implied by compatible structure.)

We can see a good example of Julia's type hierarchy by looking at the Number types.

type hierarchy for julia numbers
type hierarchy for julia numbers

The type Number is a direct child of Any. To see what Number's supertype is, we can use the supertype() function:

julia> supertype(Number)
 Any

But we could also try to find Number's subtypes (Number's children, therefore Any's grandchildren). To do this, we can use the function subtypes():

julia> subtypes(Number)
2-element Array{Union{DataType, UnionAll},1}:
 Complex
 Real   

We can observe that we have two subtypes of Number: Complex and Real. For mathematicians, Real and Complex numbers are both, indeed, numbers. As a general rule, Julia's type hierarchy reflect the real world's hierarchy.

As another example, if both Jaguar and Lion were Julia types, it would natural if their supertype were Feline. We would have:

julia> abstract type Feline end
julia> mutable struct Jaguar <: Feline end
julia> mutable struct Lion <: Feline end
julia> subtypes(Feline)
2-element Array{Any,1}:
 Jaguar
 Lion  

Concrete and abstract types

[edit | edit source]

Each object in Julia (informally, this means everything you can put into a variable in Julia) has a type. But not all types can have a respective object (instances of that type). The only ones that can have instances are called concrete types. These types cannot have any subtypes. The types that can have subtypes (e.g. Any, Number) are called abstract types. Therefore we cannot have a object of type Number, since it's an abstract type. In other words, only the leaves of the type tree are concrete types and can be instantiated.

If we can't create objects of abstract types, why are they useful? With them, we can write code that generalizes for any of its subtypes. For instance, suppose we write a function that expects a variable of the type Number:

 #this function gets a number, and returns the same number plus one
 function plus_one(n::Number)
     return n + 1
 end

In this example, the function expects a variable n. The type of n must be subtype of Number (directly or indirectly) as indicated with the :: syntax (but don't worry about the syntax yet). What does this mean? No matter if n's type is Int (Integer number) or Float64 (floating-point number), the function plus_one() will work correctly. Furthermore, plus_one() will not work with any types that are not subtypes of Number (e.g. text strings, arrays).

We can divide concrete types into two categories: primitive (or basic), and complex (or composite). Primitive types are the building blocks, usually hardcoded into Julia's heart, whereas composite types group many other types to represent higher-level data structures.

You'll probably see the following primitive types:

  • the basic integer and float types (signed and unsigned): Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, and Float64
  • more advanced numeric types: BigFloat, BigInt
  • Boolean and character types: Bool and Char
  • Text string types: String

A simple example of a composite type is Rational, used to represent fractions. It is composed of two pieces, a numerator and a denominator, both integers (of type Int).

Investigating types

[edit | edit source]

Julia provides two functions for navigating the type hierarchy: subtypes() and supertype().

julia> subtypes(Integer)
4-element Array{Union{DataType, UnionAll},1}:
 BigInt  
 Bool    
 Signed  
 Unsigned

julia> supertype(Float64)
AbstractFloat

The sizeof() function tells you how many bytes an item of this type occupies:

julia> sizeof(BigFloat)
 32

julia> sizeof(Char)
 4

If you want to know how big a number you can fit into a particular type, these two functions are useful:

julia> typemax(Int64)
 9223372036854775807

julia> typemin(Int32)
 -2147483648

There are over 340 types in the base Julia system. You can investigate the type hierarchy with the following function:

 function showtypetree(T, level=0)
     println("\t" ^ level, T)
     for t in subtypes(T)
         showtypetree(t, level+1)
     end
 end
 
 showtypetree(Number)

It produces something like this for the different Number types:

julia> showtypetree(Number)
Number
	Complex
	Real
		AbstractFloat
			BigFloat
			Float16
			Float32
			Float64
		Integer
			BigInt
			Bool
			Signed
				Int128
				Int16
				Int32
				Int64
				Int8
			Unsigned
				UInt128
				UInt16
				UInt32
				UInt64
				UInt8
		Irrational
		Rational

This shows, for example, the four main subtypes of Real number: AbstractFloat, Integer, Rational, and Irrational, as seen in the tree diagram.

type hierarchy for julia numbers
type hierarchy for julia numbers

Specifying the type of variables

[edit | edit source]

We've already seen that Julia does its best to work out the types of things you put in your code, if you don't specify them:

julia> collect(1:10)
10-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

julia> collect(1.0:10)
10-element Array{Float64,1}:
 1.0
 2.0
 3.0
 4.0
 5.0
 6.0
 7.0
 8.0
 9.0
10.0

And we've also seen that you can specify the type for a new empty array:

julia> fill!(Array{String}(undef, 3), "Julia")
3-element Array{String,1}:
 "Julia"
 "Julia"
 "Julia"

For variables, you can specify the type that its value must have. For technical reasons, you can't do this at the top level, in the REPL—you can only do it inside a definition. The syntax uses the :: syntax, which means "is of type". So:

function f(x::Int64)

means that the function f has a method that accepts an argument x which is expected to be an Int64. See Functions.

Type stability

[edit | edit source]

Here's an example of how the performance of Julia code is affected by the choice of types for variables. This is some code for exploring the Collatz conjecture.

function chain_length(n, terms)
    length = 0
    while n != 1
        if haskey(terms, n)
            length += terms[n]
            break
        end
        if n % 2 == 0      # is n even?
            n /= 2
        else
            n = 3n + 1
        end
        length += 1
    end
    return length
end

function main()
    ans = 0
    limit = 1_000_000
    score = 0
    terms = Dict()         # define a dictionary
    for i in 1:limit
        terms[i] = chain_length(i, terms)
        if terms[i] > score
            score = terms[i]
            ans = i
        end
    end
    return ans
end

We can time this, using the @time macro (although better benchmarking tools are available with the BenchmarkTools package).

julia> @time main() 
 2.634295 seconds (17.95 M allocations: 339.074 MiB, 13.50% gc time)

There are two lines of code which prevent the functions from being "type stable". These are places where the compiler is unable to use the best and most efficient types for the task in hand. Can you spot them?

The first is the division of n by 2, after testing whether n is even. n starts out as an integer, but the / division operator always returns a floating-point value. The Julia compiler can't produce pure integer code or pure floating-point code, and has to decide which to use at each stage. As a result, the compiled code isn't as fast or as concise as it could be.

The second problem is the definition of the dictionary here. It's defined without type information, so both the keys and the values can be literally any type. While this is often OK, in this sort of task, where frequent accesses occur within loops, the additional tasks of maintaining the possibility of there being different types of keys and values makes the code more complex.

julia> Dict()
Dict{Any, Any}()

If we tell the Julia compiler that this dictionary is only to contain integers (which is a good assumption here), the compiled code will be much more efficient, and type stable.

So, after changing n /= 2 to n ÷= 2, and terms = Dict() to terms = Dict{Int, Int}(), we would expect the compiler to make much more efficient code, and indeed it's much faster:

Julia> @time main()
0.450561 seconds (54 allocations: 65.170 MiB, 19.33% gc time)

You can get some tips from the compiler about possible issues in your code due to type instability. For this function, for example, you could enter @code_warntype main() and look for items or "Any" highlighted in red.

Creating types

[edit | edit source]

In Julia, it's very easy for the programmer to create new types, benefiting from the same performance and language-wise integration that the native types (those made by Julia's creators) have.

Abstract types

[edit | edit source]

Suppose we want to create an abstract type. To do this, we use Julia's keyword abstract followed by the name of the type you want to create:

abstract type MyAbstractType end

By default, the type you create is a direct subtype of Any:

julia> supertype(MyAbstractType)
 Any

You can change this using the <: operator. If you want your new abstract type to be a subtype of Number, for example, you can declare:

abstract type MyAbstractType2 <: Number end

Now, we get:

julia> supertype(MyAbstractType2)
 Number

Notice that in the same Julia session (without exiting the REPL or ending the script) it's impossible to redefine a type. That's why we had to create a type called MyAbstractType2.

Concrete types and composite

[edit | edit source]

You can create new composite types. To do this, use the struct or mutable structkeyword, which have the same syntax as declaring the supertype. The new type can contain multiple fields, where the object stores values. As an example, let's define a concrete type that is a subtype of MyAbstractType:

 mutable struct MyType <: MyAbstractType
    foo
    bar::Int
 end

We just created a composite struct type called MyType, a subtype of MyAbstractType, with two fields: foo that can be of any type, and bar, that is of type Int.

How do we create an object of MyType? By default, Julia automatically creates a constructor, a function that returns an object of that type. The function has the same name of the type, and each argument of the function correspond to each field. In this example, we can create a new object by typing:

julia> x = MyType("Hello World!", 10)
 MyType("Hello World!", 10)

This creates a MyType object, assigning Hello World! to the foo field and 10 to the bar field. We can access x's fields by using the dot notation:

julia> x.foo
 "Hello World!"

julia> x.bar
 10

Also, we can change the field values of mutable structs easily:

julia> x.foo = 3.0
 3.0

julia> x.foo
 3.0

Notice that, since we didn't specify foo's type when we created the type definition, we can change its type at any time. This is different when we try to change the type of the x.bar field (which we specified as being an Int according to MyType's definition):

julia> x.bar = "Hello World!"
LoadError: MethodError: Cannot `convert` an object of type String to an object of type Int64
This may have arisen from a call to the constructor Int64(...),
since type constructors fall back to convert methods.

The error message tells us that Julia couldn't change x.bar's type. This ensures type-stable code, and can provide better performance when programming. As a performance tip, specifying a field's type when defining your types is usually good practice.

The default constructor is used for simple cases, where you type something like typename(field1, field2) to produce a new instance of the type. But sometimes you want to do more when you construct a new instance, such as checking the incoming values. For this you can use an inner constructor, a function inside the type definition. The next section shows a practical example.

Example: British currency

[edit | edit source]

Here's an example of how you can create a simple composite type that can handle the old-fashioned British currency. Before Britain saw the light and introduced a decimal currency, the monetary system used pounds, shillings, and pence, where a pound consisted of 20 shillings, and a shilling consisted of 12 pence. This was called the £sd or LSD system (Latin for Librae, Solidii, Denarii, because the system originated in the Roman empire).

To define a suitable type, start a new composite type declaration:

 struct LSD

To contain a price in pounds, shillings, and pence, this new type should contain three fields: pounds, shillings, and pence:

   pounds::Int 
   shillings::Int
   pence::Int

The important task is to create a constructor function. This has the same name as the type, and accepts three values as arguments. After a few checks for invalid values, the special new() function creates a new object with the passed-in values. Remember we're still inside the type definition—this is an inner constructor.

  function LSD(a,b,c)
    if a < 0 || b < 0 || c < 0
      error("no negative numbers")
    end
    if c > 12 || b > 20
      error("too many pence or shillings")
    end
    new(a, b, c) 
  end

Now we can finish the type definition:

end

Here's the complete type definition again:

struct LSD
   pounds::Int 
   shillings::Int
   pence::Int
   
   function LSD(a, b, c)
    if a < 0 || b < 0 
      error("no negative numbers")
    end
    if c > 12 || b > 20
      error("too many pence or shillings")
    end
    new(a, b, c) 
   end   
end

It's now possible to create new objects that store old-fashioned British prices. You create a new object of this type by using its name (which calls the constructor function):

julia> price1 = LSD(5, 10, 6)
LSD(5, 10, 6)

julia> price2 = LSD(1, 6, 8)
LSD(1, 6, 8)

And you can't create bad prices, because of the simple checks added to the constructor function:

julia> price = LSD(1, 0, 13)
ERROR: too many pence or shillings
Stacktrace:
[1] LSD(::Int64, ::Int64, ::Int64)

If you inspect the fields of one of the price 'objects' we've created:

julia> fieldnames(typeof(price1))
3-element Array{Symbol,1}:
 :pounds   
 :shillings
 :pence    

you can see the three fields, and these are storing the values:

julia> price1.pounds
5
julia> price1.shillings
10
julia> price1.pence
6

The next task is to make this new type behave in the same way as other Julia objects. For example, we can't add two prices:

julia> price1 + price2
ERROR: MethodError: no method matching +(::LSD, ::LSD)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:420

and the output could definitely be improved:

julia> price2
LSD(5, 10, 6)

Julia already has the addition function (+) with methods defined for many types of object. The following code adds yet another method that can handle two LSD objects:

function Base.:+(a::LSD, b::LSD)
    newpence = a.pence + b.pence
    newshillings = a.shillings + b.shillings
    newpounds = a.pounds + b.pounds
    subtotal = newpence + newshillings * 12 + newpounds * 240
    (pounds, balance) = divrem(subtotal, 240)
    (shillings, pence) = divrem(balance, 12)
    LSD(pounds, shillings, pence)
end

This definition teaches Julia how to handle the new LSD objects, and adds a new method to the + function, one that accepts two LSD objects, adds them together, and produces a new LSD object containing the sum.

Now you can add two prices:

julia> price1 + price2
LSD(6,17,2)

which is indeed the result of adding LSD(5,10,6) and LSD(1,6,8).

The next problem to address is the unattractive presentation of LSD objects. This is fixed in exactly the same way, by adding a new method, but this time to the show() function, which belongs to the Base environment:

function Base.show(io::IO, money::LSD)
    print(io, $(money.pounds).$(money.shillings)s.$(money.pence)d")
end

Here, the io is the output channel currently used by all show() methods. We've added a simple expression that displays the field values with appropriate punctuation and separators.

julia> println(price1 + price2)
£6.17s.2d
julia> show(price1 + price2 + LSD(0,19,11) + LSD(19,19,6))
£27.16s.7d

You can add one or more aliases, which are alternative names for a particular type. Since Price is a better way of saying LSD, we'll create an valid alternative:

julia> const Price=LSD 
LSD

julia> show(Price(1, 19, 11))
£1.19s.11d

So far, so good, but these LSD objects are still not yet fully developed. If you want to do subtraction, multiplication, and division, you have to define additional methods for these functions for handling LSDs. Subtraction is easy enough, just requiring some fiddling with shillings and pence, so we'll leave that for now, but what about multiplication? Multiplying a price by a number involves two types of object, one a Price/LSD object, the other - well, any positive real number should be possible:

function Base.:*(a::LSD, b::Real)
    if b < 0
        error("Cannot multiply by a negative number")
    end

    totalpence = b * (a.pence + a.shillings * 12 + a.pounds * 240)
    (pounds, balance) = divrem(totalpence, 240)
    (shillings, pence) = divrem(balance, 12)
    LSD(pounds, shillings, pence)
end

Like the + method we added to Base's + function, this new * method for Base's * function is defined specifically to multiply a price by a number. It works surprisingly well for a first attempt:

julia> price1 * 2
£11.1s.0d
julia> price1 * 3
£16.11s.6d
julia> price1 * 10
£55.5s.0d
julia> price1 * 1.5
£8.5s.9d
julia> price3 = Price(0,6,5)
£0.6s.5d
julia> price3 * 1//7
£0.0s.11d

However, some failures are to be expected. We didn't allow for the really old-fashioned fractions of a penny: the halfpenny and the farthing:

julia> price1 * 0.25
ERROR: InexactError()
Stacktrace:
 [1] convert(::Type{Int64}, ::Float64) at ./float.jl:675
 [2] LSD(::Float64, ::Float64, ::Float64) at ./REPL[36]:40
 [3] *(::LSD, ::Float64) at ./REPL[55]:10

(The answer should be £1.7s.7½d. Unfortunately our LSD type doesn't allow fractions of a penny.)

But there's another, more pressing, problem. At the moment you have to give the price followed by the multiplier; the other way round fails:

julia> 2 * price1
ERROR: MethodError: no method matching *(::Int64, ::LSD)
Closest candidates are:
 *(::Any, ::Any, ::Any, ::Any...) at operators.jl:420
 *(::Number, ::Bool) at bool.jl:106
...

This is because, although Julia can find a method that matches (a::LSD, b::Number), it can't find one the other way round: (a::Number, b::LSD). But adding it is very easy:

function Base.:*(a::Number, b::LSD)
  b * a
end

which adds yet another method to Base's * function.

julia> price1 * 2
£11.1s.0d
julia> 2 * price1 
£11.1s.0d
julia> for i in 1:10
          println(price1 * i)
       end
£5.10s.6d
£11.1s.0d
£16.11s.6d
£22.2s.0d
£27.12s.6d
£33.3s.0d
£38.13s.6d
£44.4s.0d
£49.14s.6d
£55.5s.0d

The prices are now looking like an old British shop from the 19th century, forsooth!

If you want to see how many methods you've added for working with this old British pounds type so far, use the methodswith() function:

julia> methodswith(LSD)
4-element Array{Method,1}:
*(a::LSD, b::Real) at In[20]:4
*(a::Number, b::LSD) at In[34]:2
+(a::LSD, b::LSD) at In[13]:2
show(io::IO, money::LSD) at In[15]:2

Just four so far.... And you can continue to add methods to make the type more generally useful—it would depend on how you envisage yourself or others using it. For example, you probably want to add division and modulo methods, and to act intelligently about negative monetary values.

Mutable structs

[edit | edit source]

This composite type for holding British prices was defined as an immutable type. You can't change the values of these price objects once you've created them:

julia> price1.pence
6

julia> price1.pence=10
ERROR: type LSD is immutable

To create a new price based on an existing one, you'd have to do this:

julia> price2 = Price(price1.pounds, price1.shillings, 10)
£5.10s.10d

For this particular example this isn't a big problem, but there are many applications when you might want to modify or update the value of a field in a type, rather than create a new one with the right values.

For these cases, you'd want to create a mutable struct. Choose between struct and mutable struct depending on the requirements made on the type.

For more about modules, and importing functions from other modules, see Modules and packages.

Controlling the Flow

[edit | edit source]
Previous page
Types
Introducing Julia Next page
Functions
Controlling the flow

Different ways to control the flow

[edit | edit source]

Typically each line of a Julia program is evaluated in turn. There are various ways to control and modify the flow of evaluation. These correspond with the constructs used in other languages:

  • ternary and compound expressions
  • Boolean switching expressions
  • if elseif else end — conditional evaluation
  • for end — iterative evaluation
  • while end — iterative conditional evaluation
  • try catch error throw exception handling
  • do blocks

Ternary expressions

[edit | edit source]

Often you'll want to do job A (or call function A) if some condition is true, or job B (function B) if it isn't. The quickest way to write this is using the ternary operator ("?" and ":"):

julia> x = 1
1
julia> x > 3 ? "yes" : "no"
"no"
julia> x = 5
5
julia> x > 3 ? "yes" : "no"
"yes"

Here's another example:

julia> x = 0.3
0.3
julia> x < 0.5 ? sin(x) : cos(x)
0.29552020666133955

and Julia returned the value of sin(x), because x was less than 0.5. cos(x) wasn't evaluated at all.

Boolean switching expressions

[edit | edit source]

Boolean operators let you evaluate an expression if a condition is true. You can combine the condition and expression using && or ||. && means "and", and || means "or". Since Julia evaluates expressions one by one, you can easily arrange for an expression to be evaluated only if a previous condition is true or false.

The following example uses a Julia function that returns true or false depending on whether the number is odd: isodd(n).

With &&, both parts have to be true, so we can write this:

julia> isodd(1000003) && @warn("That's odd!")
WARNING: That's odd!

julia> isodd(1000004) && @warn("That's odd!")
false

If the first condition (number is odd) is true, the second expression is evaluated. If the first isn't true, the expression isn't evaluated, and just the condition is returned.

With the || operator, on the other hand:

julia> isodd(1000003) || @warn("That's odd!")
true

julia> isodd(1000004) || @warn("That's odd!")
WARNING: That's odd!

If the first condition is true, there's no need to evaluate the second expression, since we already have the one truth value we need for "or", and it returns the value true. If the first condition is false, the second expression is evaluated, because that one might turn out to be true.

This type of evaluation is also called "short-circuit evaluation".

If and Else

[edit | edit source]

For a more general — and traditional — approach to conditional execution, you can use if, elseif, and else. If you're used to other languages, don't worry about white space, braces, indentation, brackets, semicolons, or anything like that, but remember to finish the conditional construction with end.

name = "Julia"
if name == "Julia"
   println("I like Julia")
elseif name == "Python"
   println("I like Python.")
   println("But I prefer Julia.")
else
   println("I don't know what I like")
end

The elseif and else parts are optional too:

name = "Julia"
if name == "Julia"
   println("I like Julia")
end

Just don't forget the end!

How about 'switch' and 'case' statements? Well, you don't have to learn the syntax for those, because they don't exist!

ifelse

[edit | edit source]

There's an ifelse function, too. It looks like this in action:

julia> s = ifelse(false, "hello", "goodbye") * " world"

ifelse is an ordinary function, which evaluates all the arguments, and returns the second or third, depending on the value of the first. With the conditional if or ? ... :, only the expressions in the chosen route are evaluated. Alternatively, it is possible to write things like:

julia> x = 10
10
julia> if x > 0
          "positive"
       else
           "negative or zero"
       end
"positive"
julia> r = if x > 0
          "positive"
       else
          "negative or zero"
       end
"positive"
                                     
julia> r
"positive"

For loops and iteration

[edit | edit source]

Working through a list or a set of values or from a start value to a finish value are all examples of iteration, and the for ... end construction can let you iterate through a number of different types of object, including ranges, arrays, sets, dictionaries, and strings.

Here's the standard syntax for a simple iteration through a range of values:

julia> for i in 0:10:100
            println(i)
       end
0
10
20
30
40
50
60
70
80
90
100

The variable i takes the value of each element in the array (which is built from a range object) in turn — here stepping from 0 to 100 in steps of 10.

julia> for color in ["red", "green", "blue"] # an array
           print(color, " ")
       end
red green blue
julia> for letter in "julia" # a string
           print(letter, " ")
       end
j u l i a
julia> for element in (1, 2, 4, 8, 16, 32) # a tuple
           print(element, " ")
       end
1 2 4 8 16 32
julia> for i in Dict("A"=>1, "B"=>2) # a dictionary
           println(i)
       end
"B"=>2
"A"=>1
julia> for i in Set(["a", "e", "a", "e", "i", "o", "i", "o", "u"])
           println(i)
       end
e
o
u
a
i

We haven't yet met sets and dictionaries, but iterating through them is exactly the same.

You can iterate through a 2D array, stepping "down" through column 1 from top to bottom, then through column 2, and so on:

julia> a = reshape(1:100, (10, 10))
10x10 Array{Int64,2}:
 1  11  21  31  41  51  61  71  81   91
 2  12  22  32  42  52  62  72  82   92
 3  13  23  33  43  53  63  73  83   93
 4  14  24  34  44  54  64  74  84   94
 5  15  25  35  45  55  65  75  85   95
 6  16  26  36  46  56  66  76  86   96
 7  17  27  37  47  57  67  77  87   97
 8  18  28  38  48  58  68  78  88   98
 9  19  29  39  49  59  69  79  89   99
10  20  30  40  50  60  70  80  90  100
julia> for n in a
           print(n, " ")
       end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

You can use = instead of in.

Iterating over an array and updating it
[edit | edit source]

When you're iterating over an array, the array is checked each time through the loop, in case it's changed. A mistake you should avoid making is to use push! to make an array grow in the middle of a loop. Run the following text carefully, and be ready to Ctrl-C when you've seen enough (otherwise your computer will eventually crash):

julia> c = [1]
1-element Array{Int64,1}:
1
 
julia> for i in c
          push!(c, i)
          @show c
          sleep(1)
      end

c = [1,1]
c = [1,1,1]
c = [1,1,1,1]
...

Loop variables and scope

[edit | edit source]

The variable that steps through each item—the 'loop variable'—exists only inside the loop, and disappears as soon as the loop finishes.

julia> for i in 1:10
         @show i
       end
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10

julia> i
ERROR: UndefVarError: i not defined

If you want to remember the value of the loop variable outside the loop (eg if you had to exit the loop and needed to know the value you'd reached), use the global keyword to define a variable that outlasts the loop.

julia> for i in 1:10
         global howfar 
         if i % 4 == 0 
            howfar = i 
         end 
       end 
julia> howfar
8

Here, howfar didn't exist before the loop, but it survived to tell its story when the looping was over. If howfar existed before the loop started, you can change its value only if you use global in the loop.

Working in the REPL is slightly different from how you write code inside functions. In a function, you would write this:

function f()
    howfar = 0
    for i in 1:10
        if i % 4 == 0 
            howfar = i 
        end 
    end 
    return howfar
end

@show f()
8

Variables declared inside a loop

[edit | edit source]

In a similar way, if you declare a new variable inside a loop, it won't exist once the loop finishes. In this example, k is created inside:

julia> for i in 1:5
          k = i^2 
          println("$(i) squared is $(k)")
       end 
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25

so it doesn't exist after the loop has finished:

julia> k
ERROR: UndefVarError: k not defined

Variables created inside one iteration of a loop are forgotten at the end of each iteration. In this loop:

for i in 1:10
    z = i
    println("z is $z")
end
z is 1
z is 2
z is 3
z is 4
z is 5
z is 6
z is 7
z is 8
z is 9
z is 10

z is created afresh each time. If you want a variable to persist from iteration to iteration, it has to be global:

julia> counter = 0
0

julia> for i in 1:10
               global counter
               counter += i
           end 

julia> counter
55

To see this in more detail, consider the following code.

for i in 1:10
    if ! @isdefined z
        println("z isn't defined")
    end
    z = i
    println("z is $z")
end

Perhaps you expected only the first loop to produce the "z isn't defined error"? In fact, even if z is created in the body of the loop, it is undefined at the start of the next iteration.

z isn't defined
z is 1
z isn't defined
z is 2
z isn't defined
z is 3
z isn't defined
z is 4
z isn't defined
z is 5
z isn't defined
z is 6
z isn't defined
z is 7
z isn't defined
z is 8
z isn't defined
z is 9
z isn't defined
z is 10

Again, use the global keyword to force z to be available outside the loop once it's been created:

for i in 1:10
    global z
    if ! @isdefined z
        println("z isn't defined")
    else
        println("z was $z")
    end
    z = i
    println("z is $z")
end
z isn't defined
z is 1
z was 1
z is 2
z was 2
...
z is 9
z was 9
z is 10

although, if you're working in global scope, z is now available everywhere, with the value 10.

This behaviour is because we're working in the REPL. It's generally better practice to put your code inside functions, where you don't need to mark variables inherited from outside the loop as global:

function f()
   counter = 0
   for i in 1:10
      counter += i
   end
   return counter
end
julia> f()
55

Fine tuning the loop: Continue

[edit | edit source]

Sometimes on a particular iteration you might want to skip to the next value. You can use continue to skip the rest of the code inside the loop and start the loop again with the next value.

for i in 1:10
    if i % 3 == 0
       continue
    end
    println(i) # this and subsequent lines are
               # skipped if i is a multiple of 3
end

1
2
4
5
7
8
10

Comprehensions

[edit | edit source]

This oddly-named concept is simply a way of generating and collecting items. In mathematical circles you would say something like this:

"Let S be the set of all elements n where n is greater than or equal to 1 and less than or equal to 10". 

In Julia, you can write this as:

julia> S = Set([n for n in 1:10])
Set([7,4,9,10,2,3,5,8,6,1])

and the [n for n in 1:10] construction is called array comprehension or list comprehension ('comprehension' in the sense of 'getting everything' rather than 'understanding'). The outer brackets collect together the elements generated by evaluating the expression placed before the for iteration. Instead of end, use a square bracket to finish.

julia> [i^2 for i in 1:10]
10-element Array{Int64,1}:
  1
  4
  9
 16
 25
 36
 49
 64
 81
100

The type of elements can be specified:

julia> Complex[i^2 for i in 1:10]
10-element Array{Complex,1}:
  1.0+0.0im
  4.0+0.0im
  9.0+0.0im
 16.0+0.0im
 25.0+0.0im
 36.0+0.0im
 49.0+0.0im
 64.0+0.0im
 81.0+0.0im
100.0+0.0im

But Julia can work out the types of the results you're producing:

julia> [(i, sqrt(i)) for i in 1:10]
10-element Array{Tuple{Int64,Float64},1}:
(1,1.0)
(2,1.41421)
(3,1.73205)
(4,2.0)
(5,2.23607)
(6,2.44949)
(7,2.64575)
(8,2.82843)
(9,3.0)
(10,3.16228)

Here's how to make a dictionary via comprehension:

julia> Dict(string(Char(i + 64)) => i for i in 1:26)
Dict{String,Int64} with 26 entries:
 "Z" => 26
 "Q" => 17
 "W" => 23
 "T" => 20
 "C" => 3
 "P" => 16
 "V" => 22
 "L" => 12
 "O" => 15
 "B" => 2
 "M" => 13
 "N" => 14
 "H" => 8
 "A" => 1
 "X" => 24
 "D" => 4
 "G" => 7
 "E" => 5
 "Y" => 25
 "I" => 9
 "J" => 10
 "S" => 19
 "U" => 21
 "K" => 11
 "R" => 18
 "F" => 6

Next, here are two iterators in a comprehension, separated with a comma, which makes generating tables very easy. Here we're making a tuple-table:

julia> [(r,c) for r in 1:5, c in 1:2]
5×2 Array{Tuple{Int64,Int64},2}:
(1,1)  (1,2)
(2,1)  (2,2)
(3,1)  (3,2)
(4,1)  (4,2)
(5,1)  (5,2)

r goes through five cycles, one cycle for every value of c. Nested loops work in the opposite manner. Here the column-major order is respected, as shown when the array is filled with nanosecond time values:

julia> [Int(time_ns()) for r in 1:5, c in 1:2]
5×2 Array{Int64,2}:
1223184391741562  1223184391742642
1223184391741885  1223184391742817
1223184391742067  1223184391743009
1223184391742256  1223184391743184
1223184391742443  1223184391743372

You can supply a test expression as well to filter the production. For example, produce all the integers between 1 and 100 that are exactly divisible by 7:

julia> [x for x in 1:100 if x % 7 == 0]
14-element Array{Int64,1}:
  7
 14
 21
 28
 35
 42
 49
 56
 63
 70
 77
 84
 91
 98
Generator expressions
[edit | edit source]

Like comprehensions, generator expressions can be used to produce values from iterating a variable, but, unlike comprehensions, the values are produced on demand.

julia> sum(x^2 for x in 1:10)
385
julia> collect(x for x in 1:100 if x % 7 == 0)
14-element Array{Int64,1}:
  7
 14
 21
 28
 35
 42
 49
 56
 63
 70
 77
 84
 91
 98

Enumerating arrays

[edit | edit source]

Often you want to go through an array element by element while also keeping track of the index number of each element. The enumerate() function gives you an iterable version of something, producing both an index number and the value at each index number:

julia> m = rand(0:9, 3, 3)
3×3 Array{Int64,2}:
6  5  3
4  0  7
1  7  4

julia> [i for i in enumerate(m)]
3×3 Array{Tuple{Int64,Int64},2}:
(1, 6)  (4, 5)  (7, 3)
(2, 4)  (5, 0)  (8, 7)
(3, 1)  (6, 7)  (9, 4)

The array is checked for possible changes at each iteration of the loop.

Zipping arrays

[edit | edit source]

Sometimes you want to work through two or more arrays at the same time, taking the first element of each array first, then the second, and so on. This is possible using the well-named zip() function:

julia> for i in zip(0:10, 100:110, 200:210)
           println(i) 
end
(0,100,200)
(1,101,201)
(2,102,202)
(3,103,203)
(4,104,204)
(5,105,205)
(6,106,206)
(7,107,207)
(8,108,208)
(9,109,209)
(10,110,210)

You'd think it would all go wrong if the arrays were different sizes. What if the third array is too big, or too small?

julia> for i in zip(0:10, 100:110, 200:215)
           println(i)
       end
(0,100,200)
(1,101,201)
(2,102,202)
(3,103,203)
(4,104,204)
(5,105,205)
(6,106,206)
(7,107,207)
(8,108,208)
(9,109,209)
(10,110,210)

but Julia isn't fooled — any oversupply or undersupply in any one of the arrays is handled gracefully.

julia> for i in zip(0:15, 100:110, 200:210)
           println(i)
       end
(0,100,200)
(1,101,201)
(2,102,202)
(3,103,203)
(4,104,204)
(5,105,205)
(6,106,206)
(7,107,207)
(8,108,208)
(9,109,209)
(10,110,210)

This however does not work in case of filling of arrays, in this case dimensions must match:

(v1.0) julia> [i for i in zip(0:4, 100:102, 200:202)]
ERROR: DimensionMismatch("dimensions must match")
Stacktrace:
 [1] promote_shape at ./indices.jl:129 [inlined]
 [2] axes(::Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}}) at ./iterators.jl:371
 [3] _array_for at ./array.jl:611 [inlined]
 [4] collect(::Base.Generator{Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}},getfield(Main, Symbol("##5#6"))}) at ./array.jl:624
 [5] top-level scope at none:0
(v1.0) julia> [i for i in zip(0:2, 100:102, 200:202)]
3-element Array{Tuple{Int64,Int64,Int64},1}:
 (0, 100, 200)
 (1, 101, 201)
 (2, 102, 202)

Iterable objects

[edit | edit source]

The "for something in something" construction is the same for everything that you can iterate through: arrays, dictionaries, strings, sets, ranges, and so on. In Julia this is a general principle: there are a number of ways in which you can create an "iterable object", an object that is designed to be used as part of the iteration process that provides the elements one at a time.

The most obvious example we've already met is the range object. It doesn't look much when you type it into the REPL:

julia> ro = 0:2:100
0:2:100

But it gives you the numbers when you start iterating through it:

julia> [i for i in ro]
51-element Array{Int64,1}:
  0
  2
  4
  6
  8
 10
 12
 14
 16
 18
 20
 22
 24
 26
 28
  ⋮
 74
 76
 78
 80
 82
 84
 86
 88
 90
 92
 94
 96
 98
100

Should you want the numbers from a range (or other iterable object) in an array, you can use collect() to collect them up:

julia> collect(0:25:100)
5-element Array{Int64,1}:
  0
 25
 50
 75
100

You don't have to collect every element of an iterable object, you can just iterate through it. This can be particularly helpful when you have iterable objects created by other Julia functions. For example, permutations() creates an iterable object containing all the permutations of an array. You could of course use collect() to grab them and make a new array:

julia> collect(permutations(1:4))
24-element Array{Array{Int64,1},1}:
 [1,2,3,4]
 [1,2,4,3]
 
 [4,3,2,1]

but on anything large there are going to hundreds or thousands of permutations. That's the reason why iterator objects don't produce all the values from the iteration at the same time: memory and performance. A range object doesn't take up much room, even if iterating over it might take ages, depending on how big the range is. If you generate all the numbers at once, rather than only producing them when they're needed, they would all have to be stored somewhere until you need them…

Julia provides iterable objects for working with other types of data. For example, when you're working with files, you can treat an open file as an iterable object:

 filehandle = "/Users/me/.julia/logs/repl_history.jl"
 for line in eachline(filehandle)
     println(length(line), line)
 end
Use eachindex()
[edit | edit source]

A common pattern when iterating through arrays is to perform some task for each value of i, where i is the index number of each element, not the element:

 for i in eachindex(A)
   # do something with i or A[i]
 end

That is idiomatic Julia code and correct in all cases, and faster in some situations (than the alternative following code). A bad code pattern to do the same, in cases where it works (which isn't always), is:

 for i = 1:length(A)
   # do something with i or A[i]
 end
Note for advanced users
[edit | edit source]

For the purposes of this introduction, it's probably OK to assume that arrays and matrices are indexed starting at 1 (it's not for fully generic code, i.e. for introducing in registered packages). However, it's certainly possible to use other indexing bases in Julia — for example, the OffsetArrays.jl package lets you choose any starting index. It's a good idea to read the official documentation at [2] once you start working with more advanced types of array indexing.

Even more iterators

[edit | edit source]

There's a Julia package called IterTools.jl that provides some advanced iterator functions.

julia> ]
(v1.0) pkg> add IterTools
julia> using IterTools

For example, partition() groups the objects in the iterator into easily-handled chunks:

julia> collect(partition(1:10, 3, 1))
8-element Array{Tuple{Int64,Int64,Int64},1}:
(1, 2, 3) 
(2, 3, 4) 
(3, 4, 5) 
(4, 5, 6) 
(5, 6, 7) 
(6, 7, 8) 
(7, 8, 9) 
(8, 9, 10)

chain() works through all the iterators one after the other:

 for i in chain(1:3, ['a', 'b', 'c'])
   @show i
end

 i = 1
 i = 2
 i = 3
 i = 'a'
 i = 'b'
 i = 'c'

subsets() works through all subsets of an object. You can specify a size:

 for i in subsets(collect(1:6), 3)
   @show i
end

 i = [1,2,3]
 i = [1,2,4]
 i = [1,2,5]
 i = [1,2,6]
 i = [1,3,4]
 i = [1,3,5]
 i = [1,3,6]
 i = [1,4,5]
 i = [1,4,6]
 i = [1,5,6]
 i = [2,3,4]
 i = [2,3,5]
 i = [2,3,6]
 i = [2,4,5]
 i = [2,4,6]
 i = [2,5,6]
 i = [3,4,5]
 i = [3,4,6]
 i = [3,5,6]
 i = [4,5,6]

Nested loops

[edit | edit source]

If you want to nest one loop inside another, you don't have to duplicate the for and end keywords. Just use a comma:

julia> for x in 1:10, y in 1:10
          @show (x, y)
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (1,4)
(x,y) = (1,5)
(x,y) = (1,6)
(x,y) = (1,7)
(x,y) = (1,8)
(x,y) = (1,9)
(x,y) = (1,10)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (2,4)
(x,y) = (2,5)
(x,y) = (2,6)
(x,y) = (2,7)
(x,y) = (2,8)
(x,y) = (2,9)
(x,y) = (2,10)
(x,y) = (3,1)
(x,y) = (3,2)
...
(x,y) = (9,9)
(x,y) = (9,10)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)
(x,y) = (10,4)
(x,y) = (10,5)
(x,y) = (10,6)
(x,y) = (10,7)
(x,y) = (10,8)
(x,y) = (10,9)
(x,y) = (10,10)

(The useful @show macro prints out the names of things and their values.)

One difference between the shorter and longer forms of nesting loops is the behaviour of break:

julia> for x in 1:10
          for y in 1:10
              @show (x, y)
              if y % 3 == 0
                 break
              end
          end
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (3,1)
(x,y) = (3,2)
(x,y) = (3,3)
(x,y) = (4,1)
(x,y) = (4,2)
(x,y) = (4,3)
(x,y) = (5,1)
(x,y) = (5,2)
(x,y) = (5,3)
(x,y) = (6,1)
(x,y) = (6,2)
(x,y) = (6,3)
(x,y) = (7,1)
(x,y) = (7,2)
(x,y) = (7,3)
(x,y) = (8,1)
(x,y) = (8,2)
(x,y) = (8,3)
(x,y) = (9,1)
(x,y) = (9,2)
(x,y) = (9,3)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)

julia> for x in 1:10, y in 1:10
          @show (x, y)
         if y % 3 == 0
           break
         end
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)

Notice that break breaks out of both inner and outer loops in the shorter form, but only out of the inner loop in the longer form.

Optimizing nested loops

[edit | edit source]

With Julia, inner loops should concern rows rather than columns. This is due to how arrays are stored in memory. In this Julia array, for example, cells 1, 2, 3, and 4 are stored next to each other in memory (the 'column-major' format). So moving down the columns from 1 to 2 to 3 is faster than moving along rows, because jumping across from column to column, from 1 to 5 to 9, requires an extra calculation:

+-----+-----+-----+--+
|  1  |  5  |  9  |
|     |     |     |
+--------------------+
|  2  |  6  |  10 |
|     |     |     |
+--------------------+
|  3  |  7  |  11 |
|     |     |     |
+--------------------+
|  4  |  8  |  12 |
|     |     |     |
+-----+-----+-----+--+

The following examples consist of simple loops, but the way the rows and columns are iterated differ. The "bad" version looks along the first row column by column, then moves down to the next row, and so on.

function laplacian_bad(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr, nc = size(x)
    for ir = 2:nr-1, ic = 2:nc-1 # bad loop nesting order
        lap_x[ir, ic] =
            (x[ir+1, ic] + x[ir-1, ic] +
            x[ir, ic+1] + x[ir, ic-1]) - 4*x[ir, ic]
    end
end

In the "good" version, the two loops are nested properly, so that the inner loop moves down through the rows, following the memory layout of the array:

function laplacian_good(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr,nc = size(x)
    for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
        lap_x[ir,ic] =
            (x[ir+1,ic] + x[ir-1,ic] +
            x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
    end
end

Another way to increase the speed is to remove the array bounds checking, using the macro @inbounds:

function laplacian_good_nocheck(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr,nc = size(x)
    for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
        @inbounds begin lap_x[ir,ic] = # no array bounds checking
            (x[ir+1,ic] +  x[ir-1,ic] +
            x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
        end
    end
end

Here's the test function:

function main_test(nr, nc)
    field = zeros(nr, nc)
    for ic = 1:nc, ir = 1:nr
        if ir == 1 || ic == 1 || ir == nr || ic == nc
            field[ir,ic] = 1.0
        end
    end
    lap_field = zeros(size(field))

    t = @elapsed laplacian_bad(lap_field, field)
    println(rpad("laplacian_bad", 30), t)
    
    t = @elapsed laplacian_good(lap_field, field)
    println(rpad("laplacian_good", 30), t)
    
    t = @elapsed laplacian_good_nocheck(lap_field, field)
    println(rpad("laplacian_good no check", 30), t)
end

and the results show the difference in performance just based on the row/column scanning order. The "no check" version is even faster....

julia> main_test(10000,10000)
laplacian_bad                 1.947936034
laplacian_good                0.190697149
laplacian_good no check       0.092164871

Making your own iterable objects

[edit | edit source]

It's possible to design your own iterable objects. When you're defining your type, you add a couple of methods to Julia's iterate() function. Then you can use something like for .. end loop to work through the components of your object, and these iterate() methods are called automatically as necessary.

The following example shows how you can create an iterable object that generates the sequence of strings combining an uppercase letter with a number from 1 to 9. So the first item in our sequence is "A1", followed by "A2", "A3", up to "A9", then "B1", "B2", and so on, finishing at "Z9".

First, we'll define a new type called SN (StringNumber):

mutable struct SN
    str::String
    num::Int64
end

Later we'll create an iterable object of this type using something like this:

sn = SN("A", 1)

and the iterator will yield all the strings up to "Z9".

We must now add two methods to the iterate() function. This function already exists in Julia (that's why you can iterate over all the basic data objects), so the Base prefix is required: we're adding a new method to the existing iterate() function, one which is designed to handle these special objects.

The first method takes no arguments, except for the type, and is for starting the iteration process off.

function Base.iterate(sn::SN)
    str = sn.str 
    num = sn.num

    if num == 9
        nextnum = 1
        nextstr = string(Char(Int(str[1])) + 1)    
    else
        nextnum = num + 1
        nextstr = str
    end

    return (sn, SN(nextstr, nextnum))
end

This returns a tuple: the first value, and the future value of the iterator, which we've calculated (just in case we ever want to start the iterator at a point other than "A1").

The second method of iterate() takes two arguments: an iterable object and the current state. It again returns a tuple of two values, the next item and the next state. But first, if there are no more values available, the iterate() function should return nothing.

function Base.iterate(sn::SN, state)

    # check if we've finished?
    if state.str == "[" # when Z changes to [ we're done
        return 
    end 

    # we haven't finished, so we'll use the incoming one immediately
    str = state.str
    num = state.num

    # and prepare the one after that, to be saved for later
    if num == 9
        nextnum = 1
        nextstr = string(Char(Int(str[1])) + 1)    
    else
        nextnum = num + 1
        nextstr = state.str
    end

    # return: the one to use next, the one after that
    return (SN(str, num), SN(nextstr, nextnum))
end

Telling the iterator when it's finished is easy, because as soon as the incoming state contains a "[" we've finished, because the code for "[" (91) is immediately after the code for "Z" (90).

With these two methods added to handle the SN type, it's now possible to iterate through them. It's also useful to add methods for a few other Base functions, such as show() and length(). The length() method works out how many more SN strings are available starting at sn.

Base.show(io::IO, sn::SN) = print(io, string(sn.str, sn.num))

function Base.length(sn::SN) 
    cn1 = Char(Int(Char(sn.str[1]) + 1)) 
    cnz = Char(Int(Char('Z')))
    (length(cn1:cnz) * 9) + (10 - sn.num)
end

The iterator is now ready for use:

julia> sn = SN("A", 1)
A1

julia> for i in sn
          @show i 
       end 
i = A1
i = A2
i = A3
i = A4
i = A5
i = A6
i = A7
i = A8
...
i = Z6
i = Z7
i = Z8
i = Z9
julia> for sn in SN("K", 9)
           print(sn, " ") 
       end
K9 L1 L2 L3 L4 L5 L6 L7 L8 L9 M1 M2 M3 M4 M5 M6 M7 M8 M9 N1 N2 N3 N4 N5 N6 N7 N8
N9 O1 O2 O3 O4 O5 O6 O7 O8 O9 P1 P2 P3 P4 P5 P6 P7 P8 P9 Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8
Q9 R1 R2 R3 R4 R5 R6 R7 R8 R9 S1 S2 S3 S4 S5 S6 S7 S8 S9 T1 T2 T3 T4 T5 T6 T7 T8
T9 U1 U2 U3 U4 U5 U6 U7 U8 U9 V1 V2 V3 V4 V5 V6 V7 V8 V9 W1 W2 W3 W4 W5 W6 W7 W8
W9 X1 X2 X3 X4 X5 X6 X7 X8 X9 Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Y9 Z1 Z2 Z3 Z4 Z5 Z6 Z7 Z8
Z9
julia> collect(SN("Q", 7)),
(Any[Q7, Q8, Q9, R1, R2, R3, R4, R5, R6, R7  …  Y9, Z1, Z2, Z3, Z4, Z5, Z6, Z7, Z8, Z9],)

While loops

[edit | edit source]

To repeat some expressions while a condition is true, use the while ... end construction.

julia> x = 0
0
julia> while x < 4
           println(x)
           global x += 1
       end

0
1
2
3

If you're working outside a function, you'll need the global declaration of x before you can change its value. Inside a function, you don't need global.

If you want the condition to be tested after the statements, rather than before, producing a "do .. until" form, use the following construction:

while true
   println(x)
   x += 1
   x >= 4 && break
end

0
1
2
3

Here we're using a Boolean switch rather than an if ... end statement.

Template for while loops

[edit | edit source]

Here is a basic template for a while loop that will run the function find_value repeatedly until it returns a value that's no greater than 0.

function find_value(n) # find next value if current value is n
    return n - 0.5
end 

function main(start=10)
    attempts = 0
    value = start # starting value
    while value > 0.0 
        value = find_value(value) # next value given this value
        attempts += 1
        println("value: $value after $attempts attempts" )
    end
    return value, attempts
end

final_value, number_of_attempts = main(0)

println("The final value was $final_value, and it took $number_of_attempts attempts.")

For example, with small changes this code explores the famous Collatz_conjecture

function find_value(n)
    ifelse(iseven(n), n ÷ 2, 3n + 1) # Collatz calculation
end 

function main(start=10)
    attempts = 0
    value = start # starting value
    while value > 1 # while greater than 1
        value = find_value(value)
        attempts += 1
        println("value: $value after $attempts attempts" )
    end
    return value, attempts
end

final_value, number_of_attempts = main(27)

println("The final value was $final_value, and it took $number_of_attempts attempts.")

main(12) takes 9 attempts, whereas main(27) takes 111 attempts.

Using Julia's macros, you can create your own control structures. See Metaprogramming.

Exceptions

[edit | edit source]

If you want to write code that checks for errors and handles them gracefully, use the try ... catch construction.

With a catch phrase, you can handle problems that occur in your code, possibly allowing the program to continue rather than grind to a halt.

In the next example, our code attempts to change the first character of a string directly (which isn't allowed, because strings in Julia can't be modified in place):

julia> s = "string";
julia> try
          s[1] = "p"
       catch e
          println("caught an error: $e")
          println("but we can continue with execution...")
       end

 caught an error: MethodError(setindex!,("string","p",1)) but we can continue with execution...

The error() function raises an error exception with a given message.

Do block

[edit | edit source]

Finally, let's look at a do block, which is another syntax form that, like the list comprehension, looks at first sight to be a bit backwards (i.e. it can perhaps be better understood by starting at the end and working towards the beginning).

Remember the find() example from earlier?

julia> smallprimes = [2,3,5,7,11,13,17,19,23];
julia> findall(x -> isequal(13, x), smallprimes)
1-element Array{Int64,1}:
6

The anonymous function (x -> isequal(13, x)) is the first argument of find(), and it operates on the second. But with a do block, you can lift the function out and put it in between a do ... end block construction:

julia> findall(smallprimes) do x
         isequal(x, 13) 
      end
1-element Array{Int64,1}:
6

You just lose the arrow and change the order, putting the find() function and its target argument first, then adding the anonymous function's arguments and body after the do.

The idea is that it's easier to write a longer anonymous function on multiple lines at the end of the form, rather than wedged in as the first argument.

Functions

[edit | edit source]
Previous page
Controlling the flow
Introducing Julia Next page
Dictionaries and sets
Functions

Functions

[edit | edit source]

Functions are the building blocks of Julia code, acting as the subroutines, procedures, blocks, and similar structural concepts found in other programming languages.

A function is a collected group of instructions that can return one or more values, possibly based on the input arguments. If the arguments contain mutable values like arrays, the array can be modified inside the function. By convention, an exclamation mark (!) at the end of a function's name indicates that the function may modify its arguments.

There are various syntaxes for defining functions:

  • when the function contains a single expression
  • when the function contains multiple expressions
  • when the function doesn't need a name

Single expression functions

[edit | edit source]

To define a simple function, all you need to do is provide the function name and any arguments in parentheses on the left and an expression on the right of an equals sign. These are just like mathematical functions:

julia> f(x) = x * x
f (generic function with 1 method)

julia> f(2)
4
julia> g(x, y) = sqrt(x^2 + y^2)
g (generic function with 1 method)

julia> g(3, 4)
5.0

Functions with multiple expressions

[edit | edit source]

The syntax for defining a function with more than one expression is like this:

function functionname(args) 
   expression
   expression
   expression
   ...
   expression
end

Here's a typical function that calls two other functions and then ends.

function breakfast()
   maketoast()
   brewcoffee()
end

breakfast (generic function with 1 method)

Whatever the value returned by the final expression — here, the brewcoffee() function — that value is also returned by the breakfast() function.

You can use the return keyword to indicate a specific value to be returned:

julia> function canpaybills(bankbalance)
    if bankbalance < 0
       return false
    else
       return true
    end
end
canpaybills (generic function with 1 method)
julia> canpaybills(20)
true
 
julia> canpaybills(-10)
false

Some consider it good style to always use a return statement, even if it's not strictly necessary. Later we'll see how to make sure that the function doesn't go adrift if you call it with the wrong type of argument.

Returning more than one value from a function

[edit | edit source]

To return more than one value from a function, use a tuple (explored in more detail in a later chapter).

function doublesix()
    return (6, 6)
end
doublesix (generic function with 1 method)
julia> doublesix()
(6, 6)

Here you could write 6, 6 without parentheses.

Optional arguments and variable number of arguments

[edit | edit source]

You can define functions with optional arguments, so that the function can use sensible defaults if specific values aren't supplied. You provide a default symbol and value in the argument list:

function xyzpos(x, y, z=0)
    println("$x, $y, $z")
end
xyzpos (generic function with 2 methods)

And when you call this function, if you don't provide a third value, the variable z defaults to 0 and uses that value inside the function.

julia> xyzpos(1,2)
1, 2, 0
julia> xyzpos(1,2,3)
1, 2, 3

Keyword and positional arguments

[edit | edit source]

When you write a function with a long list of arguments like this:

function f(p, q, r, s, t, u)
...
end

sooner or later, you will forget the order in which you have to supply the arguments. For instance, it can be:

f("42", -2.123, atan2, "obliquity", 42, 'x')

or

f(-2.123, 42, 'x', "42", "obliquity", atan2)

You can avoid this problem by using keywords to label arguments. Use a semicolon after the function's unlabelled arguments, and follow it with one or more keyword=value pairs:

function f(p, q ; r = 4, s = "hello")
  println("p is $p")
  println("q is $q")
  return "r => $r, s => $s"
end
f (generic function with 1 method)

When called, this function expects two arguments, and also accepts a number and a string, labelled r and s. If you don't supply the keyword arguments, their default values are used:

julia> f(1,2)
p is 1
q is 2
"r => 4, s => hello"

julia> f("a", "b", r=pi, s=22//7)
p is a
q is b
"r => π = 3.1415926535897..., s => 22//7"

If you supply a keyword argument, it can be anywhere in the argument list, not just at the end or in the matching place.

julia> f(r=999, 1, 2)
p is 1
q is 2
"r => 999, s => hello"

julia> f(s="hello world", r=999, 1, 2)
p is 1
q is 2
"r => 999, s => hello world"
julia>

When defining a function with keyword arguments, remember to insert a semicolon before the keyword/value pairs.

Here's another example from the Julia manual. The rtol keyword can appear anywhere in the list of arguments or it can be omitted:

julia> isapprox(3.0, 3.01, rtol=0.1)
true

julia> isapprox(rtol=0.1, 3.0, 3.01)
true

julia> isapprox(3.0, 3.00001)
true

A function definition can combine all the different kinds of arguments. Here's one with normal, optional, and keyword arguments:

function f(a1, opta2=2; key="foo")
   println("normal argument: $a1")
   println("optional argument: $opta2")
   println("keyword argument: $key")
end
f (generic function with 2 methods)
julia> f(1)
normal argument: 1
optional argument: 2
keyword argument: foo

julia> f(key=3, 1)
normal argument: 1
optional argument: 2
keyword argument: 3

julia> f(key=3, 2, 1)
normal argument: 2
optional argument: 1
keyword argument: 3

Functions with variable number of arguments

[edit | edit source]

Functions can be defined so that they can accept any number of arguments:

function fvar(args...)
    println("you supplied $(length(args)) arguments")
    for arg in args
       println(" argument ", arg)
    end
end

The three dots indicate the famous splat. Here it means 'any', including 'none'. You can call this function with any number of arguments:

julia> fvar()
you supplied 0 arguments

julia> fvar(64)
you supplied 1 arguments
argument 64

julia> fvar(64,65)
you supplied 2 arguments
argument 64
argument 65

julia> fvar(64,65,66)
you supplied 3 arguments
argument 64
argument 65
argument 66

and so on.

Here's another example. Suppose you define a function that accepts two arguments:

function test(x, y)
   println("x $x y $y")
end

You can call this in the usual way:

julia> test(12, 34)
x 12 y 34

If you have the two numbers, but in a tuple, then how can you supply a single tuple of numbers to this two argument function? Again, the answer is to use the ellipsis (splat).

julia> test((12, 34) ...)
x 12 y 34

The use of the ellipsis or 'splat' is also referred to as 'splicing' the arguments:

julia> test([3,4]...)
x 3 y 4

You can also do this:

julia> map(test, [3, 4]...)
x 3 y 4

Local variables and changing the values of arguments

[edit | edit source]

Any variable you define inside a function will be forgotten when the function finishes.

function test(a,b,c)
    subtotal = a + b + c
end
julia> test(1,2,3)
6
julia> subtotal
LoadError: UndefVarError: subtotal not defined

If you want to keep values around across function calls, then you can think about using global variables.

A function can't modify an existing variable passed to it as an argument, but it can change the contents of a container passed to it. For example, here is a function that changes its argument to 5:

function set_to_5(x)
    x = 5
end
julia> x = 3
3

julia> set_to_5(x)
5

julia> x
3

Although the x inside the function is changed, the x outside the function isn't. Variable names in functions are local to the function.

But a function can modify the contents of a container, such as an array. This function uses the [:] syntax to access the contents of the container x, rather than change the value of the variable x:

function fill_with_5(x)
    x[:] .= 5
end
julia> x = collect(1:10);

julia> fill_with_5(x)
5

julia> x
10-element Array{Int64,1}:
5
5
5
5
5
5
5
5
5
5

You can change elements of the array, but you can't change the variable so that it points to a different array. In other words, your function isn't allowed to change the binding of the argument.

Anonymous functions

[edit | edit source]

Sometimes you don't want to worry about thinking up a cool name for a function. Anonymous functions — functions with no name — can be used in a number of places in Julia, such as with map(), and in list comprehensions.

The syntax uses ->, like this:

x -> x^2 + 2x - 1

which defines a nameless function that takes an argument, calls it x, and returns x^2 + 2x - 1.

For example, the first argument of the map() function is a function, and you can define an one-off function that exists just for one particular map() operation:

julia> map(x -> x^2 + 2x - 1, [1,3,-1])
3-element Array{Int64,1}:
 2
14
-2

After the map() finishes, both the function and the argument x have disappeared:

julia> x
ERROR: x not defined

If you want an anonymous function that accepts more than one argument, provide the arguments as a tuple:

julia> map((x,y,z) -> x + y + z, [1,2,3], [4, 5, 6], [7, 8, 9])
3-element Array{Int64,1}:
 12
 15
 18

Notice that the results are 12, 15, 18, rather than 6, 15, and 24. The anonymous function takes the first value of each of the three arrays and adds them, followed by the second, then the third.

In addition, anonymous functions can have zero arguments, if you use an 'empty' tuple():

julia> random = () -> rand(0:10)
#3 (generic function with 1 method)

julia> random()
3
julia> random()
1

If you already have a function and an array, you can call the function for each element of the array by using map(). This calls the function on each element in turn, collects the results, and returns them in an array. This process is called mapping:

julia> a=1:10;

julia> map(sin, a)
10-element Array{Float64,1}:
 0.841471
 0.909297
 0.14112
-0.756802
-0.958924
-0.279415
 0.656987
 0.989358
 0.412118
-0.544021

map() returns a new array but if you call map!() , you modify the contents of the original array.

Often, you don't have to use map() to apply a function like sin() to every member of an array, because many functions automatically operate "element-wise". The timings of the two different versions are similar (sin.() has the edge perhaps, depending on the number of elements):

julia> @time map(sin, 1:10000);
 0.149156 seconds (568.96 k allocations: 29.084 MiB, 2.01% gc time)
   
julia> @time sin.(1:10000);
 0.074661 seconds (258.76 k allocations: 13.086 MiB, 5.86% gc time)

map() collects the result of each application in an array and returns the array. Sometimes you might want the 'mapping' action but you don't want the results returned as an array. For this job, use foreach():

julia> foreach(println, 1:20)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

and ans is nothing (ans == nothing is true).

Map with multiple arrays

[edit | edit source]

You can use map() with more than one array. The function is applied to the first element of each of the arrays, then to the second, and so on. The arrays must be of the same length (unlike the zip() function, which is more tolerant).

Here's an example which generates an array of imperial (non-metric) spanner/socket sizes. The second array is just a bunch of repeated 32s to match the integers from 5 to 24 in the first array. Julia simplifies the rationals for us:

julia> map(//, 5:24, fill(32,20))
20-element Array{Rational{Int64},1}:
 5//32
 3//16
 7//32
 1//4 
 9//32
 5//16
11//32
 3//8 
13//32
 7//16
15//32
 1//2 
17//32
 9//16
19//32
 5//8 
21//32
11//16
23//32
 3//4 

(In reality, an imperial spanner set won't contain some of these strange sizes - I've never seen an old 17/32" spanner, but you can buy them online.)

Applying functions using the dot syntax

[edit | edit source]

As well as map(), it's possible to apply functions directly to arguments that are arrays. See the section on Dot syntax for vectorizing functions.

Reduce and folding

[edit | edit source]

The map() function collects the results of some function working on each and every element of an iterable object, such as an array of numbers. The reduce() function does a similar job, but after every element has been seen and processed by the function, only one is left. The function should take two arguments and return one. The array is reduced by continual application, so that just one is left.

A simple example is the use of reduce() to sum the numbers in an iterable object (which works like the built-in function sum()):

julia> reduce(+, 1:10)
55

Internally, this does something similar to this:

((((((((1 + 2) + 3) + 4) + 6) + 7) + 8) + 9) + 10)

After each operation adding two numbers, a single number is carried over to the next iteration. This process reduces all the numbers to a single final result.

A more useful example is when you want to apply a function to work on each consecutive pair in an iterable object. For example, here's a function that compares the length of two strings and returns the longer one:

julia> l(a, b) = length(a) > length(b) ? a : b
l (generic function with 1 method)

This can be used to find the longest word in a sentence by working through the string, pair by pair:

julia> reduce(l, split("This is a sentence containing some very long strings"))
"containing" 

"This" lasts a few rounds, and is then beaten by "sentence", but finally "containing" takes the lead, and there are no other challengers after that. If you want to see the magic happen, redefine l like this:

julia> l(a, b) = (println("comparing \"$a\" and \"$b\""); length(a) > length(b) ? a : b)
l (generic function with 1 method)
 
julia> reduce(l, split("This is a sentence containing some very long strings"))
comparing "This" and "is"
comparing "This" and "a"
comparing "This" and "sentence"
comparing "sentence" and "containing"
comparing "containing" and "some"
comparing "containing" and "very"
comparing "containing" and "long"
comparing "containing" and "strings"
"containing"

You can use an anonymous function to process an array pairwise. The trick is to make the function leave behind a value that will be used for the next iteration. This code takes an array such as [1, 2, 3, 4, 5, 6...] and returns [1 * 2, 2 * 3, 3 * 4, 4 * 5...], multiplying adjacent elements.

store = Int[];
reduce((x,y) -> (push!(store, x * y); y), 1:10)
julia> store
9-element Array{Int64,1}:
 2
 6
12
20
30
42
56
72
90

Folding

[edit | edit source]

Julia also offers two related functions, foldl() and foldr(). These offer the same basic functionality as reduce(). The differences are concerned with the direction in which the traversal occurs. In the simple summation example above, our best guess at what happened inside the reduce() operation assumed that the first pair of elements were added first, followed by the second pair, and so on. However, it's also possible that reduce() started at the end and worked towards the front. If it's important, use foldl() for left to right, and foldr() for right to left. In many cases, the results are the same, but here's an example where you'll get different results depending on which version you'll use:

julia> reduce(-, 1:10)
-53
 
julia> foldl(-, 1:10)
-53

julia> foldr(-, 1:10)
-5

Julia offers other functions in this group: check out mapreduce(), mapfoldl(), and mapfoldr().

If you want to use reduce() and the fold-() functions for functions that take only one argument, use a dummy second argument:

julia> reduce((x, y) -> sqrt(x), 1:4, init=256)
1.4142135623730951

which is equivalent to calling the sqrt() function four times:

julia> sqrt(sqrt(sqrt(sqrt(256))))
1.4142135623730951

Functions that return functions

[edit | edit source]

You can treat Julia functions in the same way as any other Julia object, particularly when it comes to returning them as the result of other functions.

For example, let's create a function-making function. Inside this function, a function called newfunction is created, and this will raise its argument (y) to the number that was originally passed in as the argument x. This new function is returned as the value of the create_exponent_function() function.

function create_exponent_function(x)
    newfunction = function (y) return y^x end
    return newfunction
end

Now we can construct lots of exponent-making functions. First, let's build a squarer() function:

julia> squarer = create_exponent_function(2)
#8 (generic function with 1 method)

and a cuber() function:

julia> cuber = create_exponent_function(3)
#9 (generic function with 1 method)

While we're at it, let's do a "raise to the power of 4" function (called quader, although I'm starting to struggle with the Latin and Greek naming):

julia> quader = create_exponent_function(4)
#10 (generic function with 1 method)

These are ordinary Julia functions:

julia> squarer(4)
16
 
julia> cuber(5)
125
 
julia> quader(6)
1296

The definition of the create_exponent_function() above is perfectly valid Julia code, but it's not idiomatic. For one thing, the return value doesn't always need to be provided explicitly — the final evaluation is returned if return isn't used. Also, in this case, the full form of the function definition can be replaced with the shorter one-line version. This gives the concise version:

function create_exponent_function(x)
   y -> y^x
end

which acts in the same way.

make_counter = function()
     so_far = 0
     function()
       so_far += 1
     end
end
julia> a = make_counter();

julia> b = make_counter();

julia> a()
1

julia> a()
2

julia> a()
3

julia> a()
4

julia> b()
1

julia> b()
2

Here's another example of making functions. To make it easier to see what the code is doing, here is the make_counter() function written in a slightly different manner:

function make_counter()
     so_far = 0
     counter = function()
                 so_far += 1
                 return so_far
               end
     return counter
end
julia> a = make_counter()
#15 (generic function with 1 method)

julia> a()
1

julia> a()
2

julia> a()
3

julia> for i in 1:10
           a()
       end

julia> a()
14

Function chaining and composition

[edit | edit source]

Functions in Julia can be used in combination with each other.

Function composition is when you apply two or more functions to arguments. You use the function composition operator () to compose the functions. (You can type the composition operator at the REPL using \circ). For example, the sqrt() and + functions can be composed like this:

julia> (sqrt ∘ +)(3, 5)
2.8284271247461903

which adds the numbers first, then finds the square root.

This example composes three functions.

julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Array{Char,1}:
'U'
'N'
'E'
'S'
'E'
'S'

Function chaining (sometimes called "piping" or "using a pipe to send data to a subsequent function") is when you apply a function to the previous function's output:

julia> 1:10 |> sum |> sqrt
7.416198487095663

where the total produced by sum() is passed to the sqrt() function. The equivalent composition is:

julia> (sqrt ∘ sum)(1:10)
7.416198487095663

Piping can send data to a function that accepts a single argument. If the function requires more than one argument, you may be able to use an anonymous function:

julia> collect(1:9) |> n -> filter(isodd, n)
5-element Array{Int64,1}:
 1
 3
 5
 7
 9

Methods

[edit | edit source]

A function can have one or more different methods of doing a similar job. Each method usually concentrates on doing the job for a particular type.

Here is a function to check a longitude when you type in a location:

function check_longitude_1(loc)
    if -180 < loc < 180
        println("longitude $loc is a valid longitude")
    else
        println("longitude $loc should be between -180 and 180 degrees")
    end
end
 check_longitude_1 (generic function with 1 method)

The message ("generic function with 1 method") you see if you define this in the REPL tells you that there is currently one way you can call the check_longitude_1() function. If you call this function and supply a number, it works fine.

julia> check_longitude_1(-182)
longitude -182 should be between -180 and 180 degrees

julia> check_longitude_1(22)
longitude 22 is a valid longitude

But what happens when you type in a longitude in, say, the format seen on Google Maps:

julia> check_longitude_1("1°24'54.6\"W")
ERROR: MethodError: `isless` has no method matching isless(::Int64, ::UTF8String)

The error tells us that the function has stopped because the concept of less than (<), which we are using inside our function, makes no sense if one argument is a string and the other a number. Strings are not less than or greater than integers because they are two different things, so the function fails at that point.

Notice that the check_longitude_1() function did start executing, though. The argument loc could have been anything - a string, a floating point number, an integer, a symbol, or even an array. There are many ways for this function to fail. This is not the best way to write code!

To fix this problem, we might be tempted to add code that tests the incoming value, so that strings are handled differently. But Julia proposes a better alternative: methods and multiple dispatch.

In the case where the longitude is supplied as a numeric value, the loc argument is defined as 'being of type Real'. Let's start again, define a new function, and do it properly:

function check_longitude(loc::Real)
    if -180 < loc < 180
        println("longitude $loc is a valid longitude")
    else
        println("longitude $loc should be between -180 and 180 degrees")
    end
end

Now this check_longitude function doesn't even run if the value in loc isn't a real number. The problems of what to do when the value is a string is avoided. With a type Real, this particular method can be called with any argument provided that it is some kind of number.

We can use the applicable() function to test this. applicable() lets you know whether you can apply a function to an argument — i.e. whether there is an available method for the function for arguments with that type:

julia> applicable(check_longitude, -30)
true 

julia> applicable(check_longitude, pi)
true

julia> applicable(check_longitude, 22/7)
true

julia> applicable(check_longitude, 22//7)
true

julia> applicable(check_longitude, "1°24'54.6\"W")
false

The false indicates that you can't pass a string value to the check_longitude() function because there is no method for this function that accepts a string:

julia> check_longitude("1°24'54.6\"W")
ERROR: MethodError: `check_longitude` has no method matching check_longitude(::UTF8String)

Now the body of the function isn't even looked at — Julia doesn't know a method for calling check_longitude() function with a string argument.

The obvious next step is to add another method for the check_longitude() function, only this time one that accepts a string argument. In this way, a function can be given a number of alternative methods: one for numeric arguments, one for string arguments, and so on. Julia selects and runs one of the available methods depending on the types of arguments you provide to a function.

This is multiple dispatch.

function check_longitude(loc::String)
  # not real code, obviously!
    if endswith(loc, "W")
       println("longitude $loc is West of Greenwich")
    else
       println("longitude $loc is East of Greenwich")
    end
end
check_longitude (generic function with 2 methods)

Now the check_longitude() function has two methods. The code to run depends on the types of the arguments you provide to the function. And you can avoid testing the types of arguments at the start of this function, because Julia only dispatches the flow to the string-handling method if loc is a string.

You can use the built-in methods() function to find out how many methods you have defined for a particular function.

julia> methods(check_longitude)
# 2 methods for generic function "check_longitude":
check_longitude(loc::Real) at none:2
check_longitude(loc::String) at none:3

An instructive example is to see how many different methods the + function has:

julia> methods(+)
# 176 methods for generic function "+":
[1] +(x::Bool, z::Complex{Bool}) in Base at complex.jl:276
[2] +(x::Bool, y::Bool) in Base at bool.jl:104
...
[174] +(J::LinearAlgebra.UniformScaling, B::BitArray{2}) in LinearAlgebra at  /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v0.7/LinearAlgebra/src/uniformscaling.jl:90
[175] +(J::LinearAlgebra.UniformScaling, A::AbstractArray{T,2} where T) in LinearAlgebra at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v0.7/LinearAlgebra/src/uniformscaling.jl:91
[176] +(a, b, c, xs...) in Base at operators.jl:466

This is a long list of every method currently defined for the + function; there are many different types of thing that you can add together, including arrays, matrices, and dates. If you design your own types, you might well want to write a function that adds two of them together.

Julia chooses "the most specific method" to handle the types of arguments. In the case of check_longitude(), we have two specific methods, but we could define a more general method:

function check_longitude(loc::Any)
    println("longitude $loc should be a string or a number")
end
check_longitude (generic function with 3 methods)

This method of check_longitude() is called when the loc argument is neither a Real number or a String. It is the most general method, and won't be called at all if a more specific method is available.

Type parameters in method definitions

[edit | edit source]

It's possible to work with type information in method definitions. Here's a simple example:

julia>function test(a::T) where T <: Real
    println("$a is a $T")
end
test (generic function with 1 methods)
julia> test(2.3)
2.3 is a Float64

julia> test(2)
2 is a Int64

julia> test(.02)
0.02 is a Float64

julia> test(pi)
π = 3.1415926535897... is a Irrational{:π}
julia> test(22//7)
22//7 is a Rational{Int64}
julia> test(0xff)
255 is a UInt8

The test() method automatically extracts the type of the single argument a passed to it and stores it in the 'variable' T. For this function, the definition of T was where T is a subtype of Real, so the type of T must be a subtype of the Real type (it can be any real number, but not a complex number). 'T' can be used like any other variable — in this method it's just printed out using string interpolation. (It doesn't have to be T, but it nearly always is!)

This mechanism is useful when you want to constrain the arguments of a particular method definition to be of a particular type. For example, the type of argument a must belong to the Real number supertype, so this test() method doesn't apply when a isn't a number, because then the type of the argument isn't a subtype of Real:

julia> test("str")
ERROR: MethodError: no method matching test(::ASCIIString)

julia> test(1:3)
ERROR: MethodError: no method matching test(::UnitRange{Int64})

Here's an example where you might want to write a method definition that applies to all one-dimensional integer arrays. It finds all the odd numbers in an array:

function findodds(a::Array{T,1}) where T <: Integer
              filter(isodd, a)
           end
findodds (generic function with 1 method)
julia> findodds(collect(1:20))
10-element Array{Int64,1}:
 1
 3
 5
 7
 9
11
13
15
17
19

but can't be used for arrays of real numbers:

julia> findodds([1, 2, 3, 4, 5, 6, 7, 8, 9, 10.0])
ERROR: MethodError: no method matching findodds(::Array{Float64,1})
Closest candidates are:
  findodds(::Array{T<:Integer,1}) where T<:Integer at REPL[13]:2

Note that, in this simple example, because you're not using the type information inside the method definition, you might be better off sticking to the simpler way of defining methods, by adding type information to the arguments:

function findodds(a::Array{Int64,1})
   findall(isodd, a)
end

But if you wanted to do things inside the method that depended on the types of the arguments, then the type parameters approach will be useful.

Dictionaries and Sets

[edit | edit source]
Previous page
Functions
Introducing Julia Next page
Strings and characters
Dictionaries and sets

Dictionaries

[edit | edit source]

Many of the functions introduced so far have been shown working on arrays (and tuples). But arrays are just one type of collection. Julia has others.

A simple look-up table is a useful way of organizing many types of data: given a single piece of information, such as a number, string, or symbol, called the key, what is the corresponding data value? For this purpose, Julia provides the Dictionary object, called Dict for short. It's an "associative collection" because it associates keys with values.

Creating dictionaries

[edit | edit source]

You can create a simple dictionary using the following syntax:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3)
Dict{String,Int64} with 3 entries:
 "c" => 3
 "b" => 2
 "a" => 1

dict is now a dictionary. The keys are "a", "b", and "c", the corresponding values are 1, 2, and 3. The => operator is called the Pair() function. In a dictionary, keys are always unique – you can't have two keys with the same name.

If you know the types of the keys and values in advance, you can (and probably should) specify them after the Dict keyword, in curly braces:

julia> dict = Dict{String,Integer}("a"=>1, "b" => 2)
Dict{String,Integer} with 2 entries:
 "b" => 2
 "a" => 1

You can also create dictionaries using the generator/comprehensions syntax:

julia> dict = Dict(string(i) => sind(i) for i = 0:5:360)
Dict{String,Float64} with 73 entries:
 "320" => -0.642788
 "65"  => 0.906308
 "155" => 0.422618
 ⋮     => ⋮

Use the following syntax to create a typed empty dictionary:

julia> dict = Dict{String,Int64}()
Dict{String,Int64} with 0 entries

or you can omit the types, and get an untyped dictionary:

julia> dict = Dict()
Dict{Any,Any} with 0 entries

It's sometimes useful to create dictionary entries using a for loop:

files = ["a.txt", "b.txt", "c.txt"]
fvars = Dict()
for (n, f) in enumerate(files)
   fvars["x_$(n)"] = f
end

This is one way you could create a set of 'variables' stored in a dictionary:

julia> fvars
Dict{Any,Any} with 3 entries:
 "x_1" => "a.txt"
 "x_2" => "b.txt"
 "x_3" => "c.txt"

Looking things up

[edit | edit source]

To get a value, if you have the key:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5)

julia> dict["a"]
1

if the keys are strings. Or, if the keys are symbols:

julia> symdict = Dict(:x => 1, :y => 3, :z => 6)
Dict{Symbol,Int64} with 3 entries:
 :z => 6
 :x => 1
 :y => 3
julia> symdict[:x]
1

Or if the keys are integers:

julia> intdict = Dict(1 => "one", 2 => "two", 3  => "three")
Dict{Int64,String} with 3 entries:
 2 => "two"
 3 => "three"
 1 => "one"
julia> intdict[2]
"two"

You can instead use the get() function, and provide a fail-safe default value if there's no value for that particular key:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5)
julia> get(dict, "a", 0)
1

julia> get(dict, "Z", 0)
0

If you don't want get() to provide a default value, use a try...catch block:

try
    dict["Z"]
catch error
    if isa(error, KeyError)
        println("sorry, I couldn't find anything")
    end
end

sorry, I couldn't find anything

To change a value assigned to an existing key (or assign a value to a hitherto unseen key):

julia> dict["a"] = 10
10

Keys must be unique for a dictionary. There's always only one key called a in this dictionary, so when you assign a value to a key that already exists, you're not creating a new one, just modifying an existing one.

To see if the dictionary contains a key, use haskey():

julia> haskey(dict, "Z")
false

To check for the existence of a key/value pair:

julia> in(("b" => 2), dict)
true

To add a new key and value to a dictionary, use this:

julia> dict["d"] = 4
4

You can delete a key from the dictionary, using delete!():

julia> delete!(dict, "d")
Dict{String,Int64} with 4 entries:
 "c" => 3
 "e" => 5
 "b" => 2
 "a" => 1

You'll notice that the dictionary doesn't seem to be sorted in any way — at least, the keys are in no particular order. This is due to the way they're stored, and you can't sort them in place. (But see Sorting, below.)

To get all keys, use the keys() function:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5);
julia> keys(dict)
Base.KeySet for a Dict{String,Int64} with 5 entries. Keys:
 "c"
 "e"
 "b"
 "a"
 "d"

The result is an iterator that has just one job: to iterate through a dictionary key by key:

julia> collect(keys(dict))
5-element Array{String,1}:
"c"
"e"
"b"
"a"
"d"

julia> [uppercase(key) for key in keys(dict)]
5-element Array{Any,1}:
"C"
"E"
"B"
"A"
"D"

This uses the list comprehension form ([ new-element for loop-variable in iterator ]) and each new element is collected into an array. An alternative would be:

julia> map(uppercase, collect(keys(dict)))
5-element Array{String,1}:
"C"
"E"
"B"
"A"
"D"

Values

[edit | edit source]

To retrieve all the values, use the values() function:

julia> values(dict)
Base.ValueIterator for a Dict{String,Int64} with 5 entries. Values:
 3
 5
 2
 1
 4

If you want to go through a dictionary and process each key/value, you can make use the fact that dictionaries themselves are iterable objects:

julia> for kv in dict
   println(kv)
end

"c"=>3
"e"=>5
"b"=>2
"a"=>1
"d"=>4

where kv is a tuple containing each key/value pair in turn.

Or you could do:

julia> for k in keys(dict)
          println(k, " ==> ", dict[k])
       end

c ==> 3
e ==> 5
b ==> 2
a ==> 1
d ==> 4

Even better, you can use a key/value tuple to simplify the iteration even more:

julia> for (key, value) in dict
           println(key, " ==> ", value)
       end

c ==> 3
e ==> 5
b ==> 2
a ==> 1
d ==> 4

Here's another example:

for tuple in Dict("1"=>"Hydrogen", "2"=>"Helium", "3"=>"Lithium")
    println("Element $(tuple[1]) is $(tuple[2])")
end

Element 1 is Hydrogen
Element 2 is Helium
Element 3 is Lithium

(Notice the string interpolation operator, $. This allows you to use a variable's name in a string and get the variable's value when the string is printed. You can include any Julia expression in a string using $().)

Sorting a dictionary

[edit | edit source]

Because dictionaries don't store the keys in any particular order, you might want to output the dictionary to a sorted array to obtain the items in order:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6)
Dict{String,Int64} with 6 entries:
 "f" => 6
 "c" => 3
 "e" => 5
 "b" => 2
 "a" => 1
 "d" => 4
julia> for key in sort(collect(keys(dict)))
   println("$key => $(dict[key])")
end
a => 1
b => 2
c => 3
d => 4
e => 5
f => 6

If you really need to have a dictionary that remains sorted all the time, you can use the SortedDict data type from the DataStructures.jl package (after having installed it).

julia> import DataStructures
julia> dict = DataStructures.SortedDict("b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6)
DataStructures.SortedDict{String,Int64,Base.Order.ForwardOrdering} with 5 entries:
 "b" => 2
 "c" => 3
 "d" => 4
 "e" => 5
 "f" => 6
julia> dict["a"] = 1
1
julia> dict
DataStructures.SortedDict{String,Int64,Base.Order.ForwardOrdering} with 6 entries:
 "a" => 1
 "b" => 2
 "c" => 3
 "d" => 4
 "e" => 5
 "f" => 6

Recent versions of Julia sort dictionaries for you:

julia> dict = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6)
Dict{String,Int64} with 6 entries:
  "f" => 6
  "c" => 3
  "e" => 5
  "b" => 2
  "a" => 1
  "d" => 4
   
julia> sort(dict)
OrderedCollections.OrderedDict{String,Int64} with 6 entries:
  "a" => 1
  "b" => 2
  "c" => 3
  "d" => 4
  "e" => 5
  "f" => 6

Simple example: counting words

[edit | edit source]

A simple application of a dictionary is to count how many times each word appears in a piece of text. Each word is a key, and the value of the key is the number of times that word appears in the text.

Let's count the words in the Sherlock Holmes stories. I've downloaded the text from the excellent Project Gutenberg and stored them in a file "sherlock-holmes-canon.txt". To create a list of words from the loaded text in canon, we'll split the text using a regular expression, and convert every word to lowercase. (There are probably faster methods.)

julia> f = open("sherlock-holmes-canon.txt")
julia> wordlist = String[]
julia> for line in eachline(f)
   words = split(line, r"\W")
   map(w -> push!(wordlist, lowercase(w)), words)
end
julia> filter!(!isempty, wordlist)
julia> close(f)

wordlist is now an array of nearly 700,000 words:

julia> wordlist[1:20]
20-element Array{String,1}:
"THE"     
"COMPLETE"
"SHERLOCK"
"HOLMES"  
"Arthur"  
"Conan"   
"Doyle"   
"Table"   
"of"      
"contents"
"A"       
"Study"   
"In"      
"Scarlet" 
"The"     
"Sign"    
"of"      
"the"     
"Four"    
"The"    

To store the words and the word counts, we'll create a dictionary:

julia> wordcounts = Dict{String,Int64}()
Dict{String,Int64} with 0 entries

To build the dictionary, loop through the list of words, and use get() to look up the current tally, if any. If the word has already been seen, the count can be increased. If the word hasn't been seen before, the fall-back third argument of get() ensures that the absence doesn't cause an error, and 1 is stored instead.

for word in wordlist
    wordcounts[word]=get(wordcounts, word, 0) + 1
end

Now you can look up words in the wordcounts dictionary and find out how many times they appear:

julia> wordcounts["watson"]
1040

julia> wordcounts["holmes"]
3057

julia> wordcounts["sherlock"]
415

julia> wordcounts["lestrade"]
244

Dictionaries aren't sorted, but you can use the collect() and keys() functions on the dictionary to collect the keys and then sort them. In a loop you can work through the dictionary in alphabetical order:

for i in sort(collect(keys(wordcounts)))
  println("$i, $(wordcounts[i])")
end
 000, 5
 1, 8
 10, 7
 100, 4
 1000, 9
 104, 1
 109, 1
 10s, 2
 10th, 1
 11, 9
 1100, 1
 117, 2
 117th, 2
 11th, 1
 12, 2
 120, 2
 126b, 3
            
 zamba, 2
 zeal, 5
 zealand, 3
 zealous, 3
 zenith, 1
 zeppelin, 1
 zero, 2
 zest, 3
 zig, 1
 zigzag, 3
 zigzagged, 1
 zinc, 3
 zion, 2
 zoo, 1
 zoology, 2
 zu, 1
 zum, 2
 â, 41
 ã, 4

But how do you find out the most common words? One way is to use collect() to convert the dictionary to an array of tuples, and then to sort the array by looking at the last value of each tuple:

julia> sort(collect(wordcounts), by = tuple -> last(tuple), rev=true)
19171-element Array{Pair{String,Int64},1}:
("the",36244)     
("and",17593)     
("i",17357)       
("of",16779)      
("to",16041)      
("a",15848)       
("that",11506)   
⋮                 
("enrage",1)      
("smuggled",1)    
("lounges",1)     
("devotes",1)     
("reverberated",1)
("munitions",1)   
("graybeard",1) 

To see only the top 20 words:

julia> sort(collect(wordcounts), by = tuple -> last(tuple), rev=true)[1:20]
20-element Array{Pair{String,Int64},1}:
("the",36244) 
("and",17593) 
("i",17357)   
("of",16779)  
("to",16041)  
("a",15848)   
("that",11506)
("it",11101)  
("in",10766)  
("he",10366)  
("was",9844)  
("you",9688)  
("his",7836)  
("is",6650)   
("had",6057)  
("have",5532) 
("my",5293)   
("with",5256) 
("as",4755)   
("for",4713) 

In a similar way, you can use the filter() function to find, for example, all words that start with "k" and occur less than four times:

julia> filter(tuple -> startswith(first(tuple), "k") && last(tuple) < 4, collect(wordcounts))
73-element Array{Pair{String,Int64},1}:
("keg",1)
("klux",2)
("knifing",1)
("keening",1)
("kansas",3)
⋮
("kaiser",1)
("kidnap",2)
("keswick",1)
("kings",2)
("kratides",3)
("ken",2)
("kindliness",2)
("klan",2)
("keepsake",1)
("kindled",2)
("kit",2)
("kicking",1)
("kramm",2)
("knob",1)

More complex structures

[edit | edit source]

A dictionary can hold many different types of values. Here for example is a dictionary where the keys are strings and the values are arrays of arrays of points (assuming that the Point type has been defined already). For example, this could be used to store graphical shapes describing the letters of the alphabet (some of which have two or more loops):

julia> p = Dict{String, Array{Array}}()
Dict{String,Array{Array{T,N},N}}
    
julia> p["a"] = Array[[Point(0,0), Point(1,1)], [Point(34, 23), Point(5,6)]]
2-element Array{Array{T,N},1}:
 [Point(0.0,0.0), Point(1.0,1.0)]
 [Point(34.0,23.0), Point(5.0,6.0)]
   
julia> push!(p["a"], [Point(34.0,23.0), Point(5.0,6.0)])
3-element Array{Array{T,N},1}:
 [Point(0.0,0.0), Point(1.0,1.0)]
 [Point(34.0,23.0), Point(5.0,6.0)]
 [Point(34.0,23.0), Point(5.0,6.0)]

Or create a dictionary with some already-known values:

julia> d = Dict("shape1" => Array [ [ Point(0,0), Point(-20,57)], [Point(34, -23), Point(-10,12) ] ])
Dict{String,Array{Array{T,N},1}} with 1 entry:
 "shape1" => Array [ [ Point(0.0,0.0), Point(-20.0,57.0)], [Point(34.0,-23.0), Point(-10.0,12.0) ] ]

Add another array to the first one:

julia> push!(d["shape1"], [Point(-124.0, 37.0), Point(25.0,32.0)])
3-element Array{Array{T,N},1}:
 [Point(0.0,0.0), Point(-20.0,57.0)]
 [Point(34.0,-23.0), Point(-10.0,12.0)]
 [Point(-124.0,37.0), Point(25.0,32.0)]

A set is a collection of elements, just like an array or dictionary, with no duplicated elements.

The two important differences between a set and other types of collection is that in a set you can have only one of each element, and, in a set, the order of elements isn't important (whereas an array can have multiple copies of an element and their order is remembered).

You can create an empty set using the Set constructor function:

julia> colors = Set()
Set{Any}({})

As elsewhere in Julia, you can specify the type:

julia> primes = Set{Int64}()
Set(Int64)[]

You can create and fill sets in one go:

julia> colors = Set{String}(["red","green","blue","yellow"])
Set(String["yellow","blue","green","red"])

or you can let Julia "guess the type":

julia> colors = Set(["red","green","blue","yellow"])
Set{String}({"yellow","blue","green","red"})

Quite a few of the functions that work with arrays also work with sets. Adding elements to sets, for example, is a bit like adding elements to arrays. You can use push!():

julia> push!(colors, "black") 
Set{String}({"yellow","blue","green","black","red"})

But you can't use pushfirst!(), because that works only for things that have a concept of "first", like arrays.

What happens if you try to add something to the set that's already there? Absolutely nothing. You don't get a copy added, because it's a set, not an array, and sets don't store repeated elements.

To see if something is in the set, you can use in():

julia> in("green", colors)
true

There are some standard operations you can do with sets, namely find their union, intersection, and difference, with the functions, union(), intersect(), and setdiff():

julia> rainbow = Set(["red","orange","yellow","green","blue","indigo","violet"])
Set(String["indigo","yellow","orange","blue","violet","green","red"])

The union of two sets is the set of everything that is in one or the other sets. The result is another set – so you can't have two "yellow"s here, even though we've got a "yellow" in each set:

julia> union(colors, rainbow)
Set(String["indigo","yellow","orange","blue","violet","green","black","red"])

The intersection of two sets is the set that contains every element that belongs to both sets:

julia> intersect(colors, rainbow)
Set(String["yellow","blue","green","red"])

The difference between two sets is the set of elements that are in the first set, but not in the second. This time, the order in which you supply the sets matters. The setdiff() function finds the elements that are in the first set, colors, but not in the second set, rainbow:

julia> setdiff(colors, rainbow)
Set(String["black"])

Other functions

[edit | edit source]

Functions that work on arrays and sets sometimes work on dictionaries and other collections too. For example, some of the set operations can be applied to dictionaries, not just sets and arrays:

julia> d1 = Dict(1=>"a", 2 => "b")
Dict{Int64,String} with 2 entries:
  2 => "b"
  1 => "a"
 
julia> d2 = Dict(2 => "b", 3 =>"c", 4 => "d")
Dict{Int64,String} with 3 entries:
  4 => "d"
  2 => "b"
  3 => "c"

julia> union(d1, d2)
4-element Array{Pair{Int64,String},1}:
 2=>"b"
 1=>"a"
 4=>"d"
 3=>"c"

julia> intersect(d1, d2)
1-element Array{Pair{Int64,String},1}:
 2=>"b"
 
julia> setdiff(d1, d2)
1-element Array{Pair{Int64,String},1}:
 1=>"a"

Notice that the results are returned as arrays of Pairs, rather than as Dictionaries.

Functions such as filter(), map(), and collect() which we've already seen being used with arrays also work with dictionaries:

julia> filter((k, v) -> k == 1, d1)
Dict{Int64,String} with 1 entry:
  1 => "a"

There's a merge() function which can merge two dictionaries:

julia> merge(d1, d2)
Dict{Int64,String} with 4 entries:
  4 => "d"
  2 => "b"
  3 => "c"
  1 => "a"

The findmin() function can find the minimum value in a dictionary, and return the value, and its key.

julia> d1 = Dict(:a => 1, :b => 2, :c => 0)
Dict{Symbol,Int64} with 3 entries:
 :a => 1
 :b => 2
 :c => 0

julia> findmin(d1)
(0, :c)

Strings and Characters

[edit | edit source]
Previous page
Dictionaries and sets
Introducing Julia Next page
Working with text files
Strings and characters

Strings and characters

[edit | edit source]

Strings

[edit | edit source]

A string is a sequence of one or more characters, usually found enclosed in double quotes:

"this is a string"

There are two important things you need to know about strings.

One is, that they're immutable. You can't change them once they're created. But it's easy to make new strings from parts of existing ones.

The second is that you have to be careful when using two specific characters: double quotes ("), and dollar signs ($). If you want to include a double quote character in the string, it has to be preceded with a backslash, otherwise the rest of the string would be interpreted as Julia code, with potentially interesting results. And if you want to include a dollar sign ($) in a string, that should also be prefaced by a backslash, because it's used for string interpolation.

julia> demand = "You owe me \$50!"
"You owe me \$50!"

julia> println(demand)
You owe me $50!
julia> demandquote = "He said, \"You owe me \$50!\""
"He said, \"You owe me \$50!\""

Strings can also be enclosed in triple double quotes. This is useful because you can use ordinary double quotes inside the string without having to put backslashes before them:

julia> """this is "a" string"""
"this is \"a\" string"

You'll encounter a few specialized types of string too, which consist of one or more characters immediately followed by the opening double quote:

  • r" " indicates a regular expression
  • v" " indicates a version string
  • b" " indicates a byte literal
  • raw" " indicates a raw string that doesn't do interpolation

String interpolation

[edit | edit source]

You often want to use the results of Julia expressions inside strings. For example, suppose you want to say:

"The value of x is n."

where n is the current value of x. Any Julia expression can be inserted into a string with the $() construction:

julia> x = 42
42

julia> "The value of x is $(x)."
"The value of x is 42."

You don't have to use the parentheses if you're just using the name of a variable:

julia> "The value of x is $x."
"The value of x is 42."

To include the result of a Julia expression in a string, enclose the expression in parentheses first, then precede it with a dollar sign:

julia> "The value of 2 + 2 is $(2 + 2)."
"The value of 2 + 2 is 4."

Substrings

[edit | edit source]

To extract a smaller string from a string, use getindex(s, range) or s[range] syntax. For basic ASCII strings, you can use the same techniques that you use to extract elements from arrays:

julia> s ="a load of characters"
"a load of characters"

julia> s[1:end]
"a load of characters"

julia> s[3:6]
"load"
julia> s[3:end-6]
"load of char"

which is equivalent to:

julia> s[begin+2:end-6]
"load of char"

You can easily iterate through a string:

for char in s
    print(char, "_")
end
a_ _l_o_a_d_ _o_f_ _c_h_a_r_a_c_t_e_r_s_

Watch out if you take a single element from the string, rather than a string of length 1 (i.e. with the same start and end positions):

julia> s[1:1]
"a" 

julia> s[1]
'a'

The second result isn't a string, but a character (inside single quotes).

Unicode strings

[edit | edit source]

Not all strings are ASCII. To access individual characters in Unicode strings, you can't always use simple indexing, because some characters occupy more than one index position. Don't be fooled just because some of the index numbers appear to work:

julia> su = "AéB𐅍CD"
"AéB𐅍CD"

julia> su[1]
'A'

julia> su[2]
'é'

julia> su[3]
ERROR: UnicodeError: invalid character index
in slow_utf8_next(::Array{UInt8,1}, ::UInt8, ::Int64) at ./strings/string.jl:67
in next at ./strings/string.jl:92 [inlined]
in getindex(::String, ::Int64) at ./strings/basic.jl:70

Instead of length(str) to find the length of a string, use lastindex(str):

julia> length(su)
6
julia> lastindex(su)
10

The isascii() functions tests whether a string is ASCII or contains Unicode characters:

julia> isascii(su)
false

In this string, the 'second' character, é, has 2 bytes, the 'fourth' character, 𐅍, has 4 bytes.

for i in eachindex(su)
    println(i, " -> ", su[i])
end
1 -> A
2 -> é
4 -> B
5 -> 𐅍
9 -> C
10 -> D

The 'third' character, B, starts with the 4th element in the string.

You can also do this even more easily using the pairs() function:

for pair in pairs(su)
    println(pair)
end
1 => A
2 => é
4 => B
5 => 𐅍
9 => C
10 => D

As an alternative, use the eachindex iterator:

for charindex in eachindex(su)
    @show su[charindex]
end
su[charindex] = 'A'
su[charindex] = 'é'
su[charindex] = 'B'
su[charindex] = '𐅍'
su[charindex] = 'C'
su[charindex] = 'D'



There are other useful functions for working with strings like this, including collect(), thisind(), nextind(), and prevind():

julia> collect(su)
 6-element Array{Char,1}:
 'A'
 'é'
 'B'
 '𐅍'
 'C'
 'D'
for i in 1:10
    print(thisind(su, i), " ")
end
1 2 2 4 5 5 5 5 9 10 

Splitting and joining strings

[edit | edit source]

You can stick strings together (a process often called concatenation) using the multiply (*) operator:

julia> "s" * "t"
"st"

If you've used other programming languages, you might expect to use the addition (+) operator:

julia> "s" + "t"
LoadError: MethodError: `+` has no method matching +(::String, ::String)

- so use *.

If you can 'multiply' strings, you can also raise them to a power:

julia> "s" ^ 18
"ssssssssssssssssss"

You can also use string():

julia> string("s", "t")
"st"

but if you want to do a lot of concatenation, inside a loop, perhaps, it might be better to use the string buffer approach (see below).

To split a string, use split() function. Given this simple string:

julia> s = "You know my methods, Watson."
"You know my methods, Watson."

a simple call to the split() function divides the string at the spaces, returning a five-piece array:

julia> split(s)
5-element Array{SubString{String},1}:
"You"
"know"
"my"
"methods,"
"Watson."

Or you can specify the string of 1 or more characters to split at:

julia> split(s, "e")
2-element Array{SubString{String},1}:
"You know my m"
"thods, Watson."

julia> split(s, " m")
3-element Array{SubString{String},1}:
"You know"    
"y"       
"ethods, Watson."

The characters you use to do the splitting don't appear in the final result:

julia> split(s, "hod")
2-element Array{SubString{String},1}:
"You know my met"
"s, Watson."

If you want to split a string into separate single-character strings, use the empty string ("") which splits the string between the characters:

julia> split(s,"")
28-element Array{SubString{String},1}:
"Y"
"o"
"u"
" "
"k"
"n"
"o"
"w"
" "
"m"
"y"
" "
"m"
"e"
"t"
"h"
"o"
"d"
"s"
","
" "
"W"
"a"
"t"
"s"
"o"
"n"
"."

You can also split strings using a regular expression to define the splitting points. Use the special regex string construction r" ". Inside this, you can use regular expression characters with special meanings:

julia> split(s, r"a|e|i|o|u")
8-element Array{SubString{String},1}:
"Y"
""
" kn"
"w my m"
"th"
"ds, W"
"ts"
"n."

Here, the r"a|e|i|o|u" is a regular expression string, and — as you'll know if you love regular expressions — that this matches any of the vowels. So the resulting array consists of the string split at every vowel. Notice the empty strings in the results -— if you don't want those, add a false flag at the end:

julia> split(s, r"a|e|i|o|u", false)
7-element Array{SubString{String},1}:
"Y"   
" kn"  
"w my m"
"th"  
"ds, W" 
"ts"  
"n."  

If you wanted to keep the vowels, rather than use them for splitting work, you have to delve deeper into the world of regex literal strings. Read on.

You can join the elements of a split string in array form using join():

julia> join(split(s, r"a|e|i|o|u", false), "aiou")
"Yaiou knaiouw my maiouthaiouds, Waioutsaioun."

Splitting using a function

[edit | edit source]

Many functions in Julia let you use functions as part of a function call. Anonymous functions are useful, because you can make function calls which have smart choices built-in. For example, split() lets you provide a function in place of the delimiter character. In the next example, the delimiter is (bizarrely) specified to be any upper-case character whose ASCII code is a multiple of 8:

julia> split(join(Char.(65:90)),  c -> Int(c) % 8 == 0)
4-element Array{SubString{String},1}:
 "ABCDEFG"
 "IJKLMNO"
 "QRSTUVW"
 "YZ"

Character objects

[edit | edit source]

Above we extracted smaller strings from larger strings:

julia> s[1:1]
"a"

But when we extracted a single element from a string:

julia> s[1]
'a'

note the single quotes. In Julia, these are used to mark character objects, so 'a' is a character object, but "a" is a string with length 1. These are not equivalent.

You can convert character objects to strings easily enough:

julia> string('s') * string('d')
"sd"

or

julia> string('s', 'd')
"sd"

It's easy to input 32 bits Unicode characters using \U escape sequence (the uppercase means 32 bits). The lowercase escape sequence \u can be used for 16 and 8 bit characters:

julia> ('\U1014d', '\u2640', '\u26')
('𐅍','♀','&')

For strings, the \Uxxxxxxxx and \uxxxx syntax is more strict.

julia> "\U0001014d2\U000026402\u26402\U000000a52\u00a52\U000000352\u00352\x352"
"𐅍2♀2♀2¥2¥2525252"

Converting between numbers and strings

[edit | edit source]

Turning integers into strings is the job of the string() function. The keyword base lets you specify the number base for the conversion, which you can use to convert decimal digits to a binary, octal, or hexadecimal string:

julia> string(11, base=2)
"1011"
julia> string(11, base=8)
"13"

julia> string(11, base=16)
"b"

julia> string(11)
"11"
julia> a = BigInt(2)^200
1606938044258990275541962092341162602522202993782792835301376
julia> string(a)
"1606938044258990275541962092341162602522202993782792835301376"
julia> string(a, base=16)
"1000000000000000000000000000000000000000000000000"

To convert strings to numbers, use parse(), and you can also specify the number base (such as binary or hex) if you want the string to be interpreted as using a number base:

julia> parse(Int, "100")
100

julia> parse(Int, "100", base=2)
4

julia> parse(Int, "100", base=16)
256

julia> parse(Float64, "100.32")
100.32

julia> parse(Complex{Float64}, "0 + 1im")
0.0 + 1.0im

Converting characters to integers and back again

[edit | edit source]

Int() converts a character into an integer, and Char() turns an integer into a character.

julia> Char(8253)
'‽': Unicode U+203d (category Po: Punctuation, other)

julia> Char(0x203d) # the Interrobang is Unicode U+203d in hexadecimal
'‽': Unicode U+203d (category Po: Punctuation, other)

julia> Int('‽')
8253

julia> string(Int('‽'), base=16)
"203d"

To go from a single character string to the code number (such as its ASCII or UTF code number), try this:

julia> Int("S"[1])
83

For a quick alphabet:

julia> string.(Char.("A"[1]:"Z"[1])) |> collect 
26-element Array{String,1}:
 "A"
 "B"
 ...
 "Y"
 "Z"

printf formatting

[edit | edit source]

If you're deeply attached to C-style printf() functionality, you'll be able to use a Julia macro (you call macros by prefacing them with the @ sign). The macro is provided in the Printf package, which you'll need to load first:

julia> using Printf
julia> @printf("pi = %0.20f", float(pi))
pi = 3.14159265358979311600

or you can create another string using the sprintf() macro, also to be found in the Printf package:

julia> @sprintf("pi = %0.20f", float(pi))
"pi = 3.14159265358979311600"

Convert a string to an array

[edit | edit source]

To read from a string into an array, you can use the IOBuffer() function. This is available with a number of Julia functions (including printf()). Here's a string of data (it could have been read from a file):

data="1 2 3 4
5 6 7 8
9 0 1 2"

"1 2 3 4\n5 6 7 8\n9 0 1 2"

Now you can "read" this string using functions such as readdlm(), the "read with delimiters" function. This can be found in the package DelimitedFiles.

julia> using DelimitedFiles
julia> readdlm(IOBuffer(data))
3x4 Array{Float64,2}:
1.0 2.0 3.0 4.0
5.0 6.0 7.0 8.0
9.0 0.0 1.0 2.0

You can add an optional type specification:

julia> readdlm(IOBuffer(data), Int)
3x4 Array{Int64,2}:
1 2 3 4
5 6 7 8
9 0 1 2

Sometimes you want to do things to strings that you can do better with arrays. Here's an example.

julia> s = "/Users/me/Music/iTunes/iTunes Media/Mobile Applications";

You can explode the pathname string into an array of character objects, using collect(), which gathers the items in a collection or string into an array:

julia> collect(s)
55-element Array{Char,1}:
'/'
'U'
's'
'e'
'r'
's'
'/'
...

Similarly, you can use split() to split the string and count the results:

julia> split(s, "")
55-element Array{Char,1}:
'/'
'U'
's'
'e'
'r'
's'
'/'
...

To count the occurrences of a particular character object, you can use an anonymous function:

julia> count(c -> c == '/', collect(s))
6

although here converting to an array is unnecessary and inefficient. Here's a better way:

julia> count(c -> c == '/', s)
6

Finding and replacing things inside strings

[edit | edit source]

If you want to know whether a string contains a specific character, use the general-purpose in() function.

julia> s = "Elementary, my dear Watson";
julia> in('m', s)
true

But the occursin() function, which accepts two strings, is more generally useful, because you can use substrings with one or more characters. Notice that you place the search term first, then the string you're looking in — occursin(needle, haystack):

julia> occursin("Wat", s)
true
julia> occursin("m", s)
true
julia> occursin("mi", s)
false
julia> occursin("me", s)
true

You can get the location of the first occurrence of a substring using findfirst(needle, haystack). The first argument can be a single character, a string, or a regular expression:

julia> s ="You know my methods, Watson.";

julia> findfirst("meth", s)
13:16
julia> findfirst(r"[aeiou]", s)  # first vowel
2
julia> findfirst(isequal('a'), s) # first occurrence of character 'a'
23

In each case, the result contains the indices of the characters, if present.

Replacing

[edit | edit source]

The replace() function returns a new string with a substring of characters replaced with something else:

julia> replace("Sherlock Holmes", "e" => "ee")
"Sheerlock Holmees"

You use the => operator to specify the pattern you're looking for, and its replacement. Usually the third argument is another string, as here. But you can also supply a function that processes the result:

julia> replace("Sherlock Holmes", "e" => uppercase)
"ShErlock HolmEs"

where the function (here, the built-in uppercase() function) is applied to the matching substring.

There's no replace! function, where the "!" indicates a function that changes its argument. That's because you can't change a string — they're immutable.

Replacing using functions
[edit | edit source]

Many functions in Julia allow you to supply functions as part of the function call, and you can make good use of anonymous functions for this. Here, for example, is how to use a function to provide random replacements in a replace() function.

julia>  t = "You can never foretell what any one man will do, but you can say with precision what an average number will be up to. Individuals vary, but percentages remain constant.";
julia> replace(t, r"a|e|i|o|u" => (c) -> rand(Bool) ? "0" : "1") 
"Y00 c1n n0v0r f1r0t1ll wh1t 0ny 0n0 m0n w1ll d0, b0t y01 c1n s1y w0th pr1c1s10n wh0t 1n 1v0r0g0 n1mb0r w0ll b0 0p t1. Ind1v0d11ls v0ry, b0t p1rc0nt0g0s r0m01n c1nst0nt."
julia> replace(t, r"a|e|i|o|u" => (c) -> rand(Bool) ? "0" : "1")
"Y11 c0n...n1v0r f0r1t0ll wh1t 1ny 0n1 m0n w1ll d1, b1t y10 c1n s1y w1th pr0c1s01n wh0t 0n 0v1r0g0 n1mb1r w0ll b0 1p t1. Ind1v0d01ls v0ry, b1t p0rc1nt1g0s r0m01n c1nst0nt."

Regular expressions

[edit | edit source]

You can use regular expressions to find matches for substrings. Some functions that accept a regular expression are:

  • replace() changes occurrences of regular expressions
  • match() returns the first match or nothing
  • eachmatch() returns an iterator that lets you search through all matches
  • split() splits a string at every match

Use replace() to replace each consonant with an underscore:

julia> replace("Elementary, my dear Watson!", r"[^aeiou]" => "_")
"__e_e__a________ea___a__o__"

and the following code replaces each vowel with the results of running a function on each match:

julia> replace("Elementary, my dear Watson!", r"[aeiou]" => uppercase)
"ElEmEntAry, my dEAr WAtsOn!"

With replace() you can access the matches if you provide a special substitution string s"", where \1 refers to the first match, \2 to the second, and so on. With this regex operation, each lowercase letter preceded by a space is repeated three times:

julia> replace("Elementary, my dear Watson!", r"(\s)([a-z])" => s"\1\2\2\2")
"Elementary, mmmy dddear Watson!"

For more regular expression fun, there are the -match- functions.

Here I've loaded the complete text of "The Adventures of Sherlock Holmes" from a file into the string called text:

julia> f = "/tmp/adventures-of-sherlock-holmes.txt"
julia> text = read(f, String);

To use the possibility of a match as a Boolean condition, suitable for use in an if statement for example, use occursin().

julia> occursin(r"Opium", text)
false

That's odd. We were expecting to find evidence of the great detective's peculiar pharmacological recreations. In fact, the word "opium" does appear in the text, but only in lower-case, hence this false result—regular expressions are case-sensitive.

julia> occursin(r"(?i)Opium", text)
true

This is a case-insensitive search, set by the flag (?i)), and it returns true.

You could check every line for the word using a simple loop:

for l in split(text, "\n")
    occursin(r"opium", l) && println(l)
end
opium. The habit grew upon him, as I understand, from some
he had, when the fit was on him, made use of an opium den in the
brown opium smoke, and terraced with wooden berths, like the
wrinkled, bent with age, an opium pipe dangling down from between
very short time a decrepit figure had emerged from the opium den,
opium-smoking to cocaine injections, and all the other little
steps - for the house was none other than the opium den in which
lives upon the second floor of the opium den, and who was
learn to have been the lodger at the opium den, and to have been
doing in the opium den, what happened to him when there, where is
"Had he ever showed any signs of having taken opium?"
room above the opium den when I looked out of my window and saw,

For more useable output (in the REPL), add enumerate() and some highlighting:

red = Base.text_colors[:red]; default = Base.text_colors[:default];
for (n, l) in enumerate(split(text, "\n"))
    occursin(r"opium", l) && println("$n $(replace(l, "opium" => "$(red)opium$(default)"))")
end
5087 opium. The habit grew upon him, as I understand, from some
5140 he had, when the fit was on him, made use of an opium den in the
5173 brown opium smoke, and terraced with wooden berths, like the
5237 wrinkled, bent with age, an opium pipe dangling down from between
5273 very short time a decrepit figure had emerged from the opium den,
5280 opium-smoking to cocaine injections, and all the other little
5429 steps - for the house was none other than the opium den in which
5486 lives upon the second floor of the opium den, and who was
5510 learn to have been the lodger at the opium den, and to have been
5593 doing in the opium den, what happened to him when there, where is
5846 "Had he ever showed any signs of having taken opium?"
6129 room above the opium den when I looked out of my window and saw,

There's an alternative syntax for adding regex modifiers, such as case-insensitive matches. Notice the "i" immediately following the regex string in the second example:

julia> occursin(r"Opium", text)
false

julia> occursin(r"Opium"i, text)
true

With the eachmatch() function, you apply the regex to the string to produce an iterator. For example, to look for substrings in our text matching the letters "L", followed by some other characters, ending with "ed":

julia> lmatch = eachmatch(r"L.*?ed", text)

The result in lmatch is an iterable object containing all the matches, as RegexMatch objects:

julia> collect(lmatch)[1:10]
10-element Array{RegexMatch,1}:
RegexMatch("London, and proceed")         
RegexMatch("London is a pleasant thing indeed")  
RegexMatch("Looking for lodgings,\" I answered") 
RegexMatch("London he had received")       
RegexMatch("Lied")                
RegexMatch("Life,\" and it attempted")      
RegexMatch("Lauriston Gardens wore an ill-omened")
RegexMatch("Let\" card had developed")      
RegexMatch("Lestrade, is here. I had relied")   
RegexMatch("Lestrade grabbed")         

We can step through the iterator and look at each match in turn. You can access a number of fields of a RegexMatch, to extract information about the match. These include captures, match, offset, offsets, and regex. For example, the match field contains the matched substring:

for i in lmatch
    println(i.match)
end
London - quite so! Your Majesty, as I understand, became entangled
Lodge. As it pulled
Lord, Mr. Wilson, that I was a red
League of the Red
League was founded
London when he was young, and he wanted
LSON" in white letters, upon a corner house, announced
League, and the copying of the 'Encyclopaed
Leadenhall Street Post Office, to be left till called
Let the whole incident be a sealed
Lestrade, being rather puzzled
Lestrade would have noted
...
Lestrade," drawled
Lestrade looked
Lord St. Simon has not already arrived
Lord St. Simon sank into a chair and passed
Lord St. Simon had by no means relaxed
Lordship. "I may be forced
London. What could have happened
London, and I had placed

Other fields include captures, the captured substrings as an array of strings, offset, the offset into the string at which the whole match begins, and offsets, the offsets of the captured substrings.

To get an array of matching strings, use something like this:

julia> collect(m.match for m in eachmatch(r"L.*?ed", text))
58-element Array{SubString{String},1}:
"London - quite so! Your Majesty, as I understand, became entangled"
"Lodge. As it pulled"                        
"Lord, Mr. Wilson, that I was a red"                
"League of the Red"                         
"League was founded"                        
"London when he was young, and he wanted"              
"Leadenhall Street Post Office, to be left till called"       
"Let the whole incident be a sealed"                
"Lestrade, being rather puzzled"                  
"Lestrade would have noted"                     
"Lestrade looked"                          
"Lestrade laughed"                         
"Lestrade shrugged"                         
"Lestrade called"                          
... 
"Lord St. Simon shrugged"                      
"Lady St. Simon was decoyed"                    
"Lestrade,\" drawled"                        
"Lestrade looked"                          
"Lord St. Simon has not already arrived"              
"Lord St. Simon sank into a chair and passed"            
"Lord St. Simon had by no means relaxed"              
"Lordship. \"I may be forced"                    
"London. What could have happened"                 
"London, and I had placed" 

The basic match() function looks for the first match for your regex. Use the match field to extract the information from the RegexMatch object:

julia> match(r"She.*",text).match
"Sherlock Holmes she is always THE woman. I have seldom heard\r"

A more streamlined way of obtaining matching lines from a file is this:

julia> f = "adventures of sherlock holmes.txt"

julia> filter(s -> occursin(r"(?i)Opium", s), map(chomp, readlines(open(f))))
12-element Array{SubString{String},1}:
"opium. The habit grew upon him, as I understand, from some"    
"he had, when the fit was on him, made use of an opium den in the" 
"brown opium smoke, and terraced with wooden berths, like the"   
"wrinkled, bent with age, an opium pipe dangling down from between"
"very short time a decrepit figure had emerged from the opium den,"
"opium-smoking to cocaine injections, and all the other little"  
"steps - for the house was none other than the opium den in which" 
"lives upon the second floor of the opium den, and who was"    
"learn to have been the lodger at the opium den, and to have been" 
"doing in the opium den, what happened to him when there, where is"
"\"Had he ever showed any signs of having taken opium?\""     
"room above the opium den when I looked out of my window and saw,"

Making a Regex

[edit | edit source]

Sometimes you want to make a regular expression from within your code. You can do this by making a Regex object. Here is one way you could count the number of vowels in the text:

f = open("sherlock-holmes.txt")

text = read(f, String)

for vowel in "aeiou"
    r = Regex(string(vowel))
    l = [m.match for m = eachmatch(r, text)]
    println("there are $(length(l)) letter \"$vowel\"s in the text.")
end
there are 219626 letter "a"s in the text.
there are 337212 letter "e"s in the text.
there are 167552 letter "i"s in the text.
there are 212834 letter "o"s in the text.
there are 82924 letter "u"s in the text.
Making a substitution string
[edit | edit source]

Sometimes you'll want to assemble a substitution string. To do this, you can use SubstitutionString() instead of s"...".

For example, say you want to do some string interpolation in the replacement string. Perhaps you have a list of files, and you want to renumber them, so that "file2.png" becomes "file1.png":

files = ["file2.png", "file3.png", "file4.png", "file5.png", "file6.png", "file7.png"] 

for (n, f) in enumerate(files)
    newfilename = replace(f, r"(.*)\d\.png" => SubstitutionString("\\g<1>$(n).png"))
    # now to do the renaming...

Notice that you can't simply use \1 in the SubstitutionString to refer to the first captured expression, you have to escape it as \\1, and use \g (escaped as \\g) to refer to the named capture group.

Testing and changing strings

[edit | edit source]

There are lots of functions for testing and changing strings:

  • length(str) length of string
  • sizeof(str) length/size
  • startswith(strA, strB) does strA start with strB?
  • endswith(strA, strB) does strA end with strB?
  • occursin(strA, strB) does strA occur in strB?
  • all(isletter, str) is str entirely letters?
  • all(isnumeric, str) is str entirely number characters?
  • isascii(str) is str ASCII?
  • all(iscntrl, str) is str entirely control characters?
  • all(isdigit, str) is str 0-9?
  • all(ispunct, str) does str consist of punctuation?
  • all(isspace, str) is str whitespace characters?
  • all(isuppercase, str) is str uppercase?
  • all(islowercase, str) is str entirely lowercase?
  • all(isxdigit, str) is str entirely hexadecimal digits?
  • uppercase(str) return a copy of str converted to uppercase
  • lowercase(str) return a copy of str converted to lowercase
  • titlecase(str) return copy of str with the first character of each word converted to uppercase
  • uppercasefirst(str) return copy of str with first character converted to uppercase
  • lowercasefirst(str) return copy of str with first character converted to lowercase
  • chop(str) return a copy with the last character removed
  • chomp(str) return a copy with the last character removed only if it's a newline

Streams

[edit | edit source]

To write to a string, you can use a Julia stream. The sprint() (String Print) function lets you use a function as the first argument, and uses the function and the rest of the arguments to send information to a stream, returning the result as a string.

For example, consider the following function, f. The body of the function maps an anonymous 'print' function over the arguments, enclosing them with angle brackets. When used by sprint, the function f processes the remaining arguments and sends them to the stream.

function f(io::IO, args...)
    map((a) -> print(io,"<",a, ">"), args)
end
f (generic function with 1 method)
julia> sprint(f, "fred", "jim", "bill", "fred blogs")
"<fred><jim><bill><fred blogs>"

Functions like println() can take an IOBuffer or stream as their first argument. This lets you print to streams instead of printing to the standard output device:

julia> iobuffer = IOBuffer()
IOBuffer(data=Uint8[...], readable=true, writable=true, seekable=true, append=false, size=0, maxsize=Inf, ptr=1, mark=-1)
julia> for i in 1:100
           println(iobuffer, string(i))
       end

After this, the in-memory stream called iobuffer is full of numbers and newlines, even though nothing was printed on the terminal. To copy the contents of iobuffer from the stream to a string or array, you can use take!():

julia> String(take!(iobuffer))
"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14 ... \n98\n99\n100\n"

Colored / Styled Output

[edit | edit source]

The following prints out messages in their respective colors using printstyled:

julia> for color in [:red, :green, :blue, :magenta]
           printstyled("Hello in $(color)\n"; color = color)
       end
Hello in red
Hello in green
Hello in blue
Hello in magenta

Printing a formatted Backtrace

[edit | edit source]

In the middle of a try catch statement, the following will print the original backtrace that caused the exception:

try
    # some code that can fail, but you want to continue even after a failure
catch e
    # show the error, but with its backtrace
    showerror(stderr, e, catch_backtrace())
end

If you are outside a try-catch and want to print a stacktrace without throwing stopping execution use this:

showerror(stderr, ErrorException("show stacktrace"), stacktrace())

Working with Text Files

[edit | edit source]
Previous page
Strings and characters
Introducing Julia Next page
Working with dates and times
Working with text files

Reading from files

[edit | edit source]

The standard approach for getting information from a text file is using the open(), read(), and close() functions.

To read text from a file, first obtain a file handle:

f = open("sherlock-holmes.txt")

f is now Julia's connection to the file on disk. When you've finished with the file, you should close the connection, using:

close(f)

In general, the recommended way to work with a file in Julia is to wrap any file-processing functions inside a do block:

open("sherlock-holmes.txt") do file
    # do stuff with the open file
end

The open file is automatically closed when this block finishes. See Controlling the flow for more about do blocks.

Because of the scope of local variables in blocks, you might want to keep some of information that was processed:

totaltime, totallines = open("sherlock-holmes.txt") do f
    linecounter = 0
    timetaken = @elapsed for l in eachline(f)
        linecounter += 1
    end
    (timetaken, linecounter)
end
julia> totaltime, totallines
(0.004484679, 76803)

Slurp – reading a file all at once

[edit | edit source]

You can read the entire contents of an open file at once with read():

julia> s = read(f, String)

This stores the contents of the file in s:

s = open("sherlock-holmes.txt") do file
    read(file, String)
end

You can use readlines() to read in the whole file as an array, with each line an element:

julia> f = open("sherlock-holmes.txt");

julia> lines = readlines(f)
76803-element Array{String,1}:
"THE ADVENTURES OF SHERLOCK HOLMES by SIR ARTHUR CONAN DOYLE\r\n"
"\r\n"
"   I. A Scandal in Bohemia\r\n"
"  II. The Red-headed League\r\n"
...
"Holmes, rather to my disappointment, manifested no further\r\n"
"interest in her when once she had ceased to be the centre of one\r\n"
"of his problems, and she is now the head of a private school at\r\n"
"Walsall, where I believe that she has met with considerable success.\r\n"
julia> close(f)

Now you can step through the lines:

counter = 1
for l in lines
   println("$counter $l")
   counter += 1
end
1 THE ADVENTURES OF SHERLOCK HOLMES by SIR ARTHUR CONAN DOYLE
2
3    I. A Scandal in Bohemia
4   II. The Red-headed League
5  III. A Case of Identity
6   IV. The Boscombe Valley Mystery
...
12638 interest in her when once she had ceased to be the centre of one
12639 of his problems, and she is now the head of a private school at
12640 Walsall, where I believe that she has met with considerable success.

There's a better way to do this – see enumerate(), below.

You might find the chomp() function useful – it removes the trailing newline from a string.

Line by line

[edit | edit source]

The eachline() function turns a source into an iterator. This allows you to process a file a line at a time:

open("sherlock-holmes.txt") do file
    for ln in eachline(file)
        println("$(length(ln)), $(ln)")
    end
end
1, THE ADVENTURES OF SHERLOCK HOLMES by SIR ARTHUR CONAN DOYLE
2,
28,    I. A Scandal in Bohemia
29,   II. The Red-headed League
26,  III. A Case of Identity
35,   IV. The Boscombe Valley Mystery
…
62, the island of Mauritius. As to Miss Violet Hunter, my friend
60, Holmes, rather to my disappointment, manifested no further
66, interest in her when once she had ceased to be the centre of one
65, of his problems, and she is now the head of a private school at
70, Walsall, where I believe that she has met with considerable success.

Another approach is to read until you reach the end of the file. You might want to keep track of which line you're on:

 open("sherlock-holmes.txt") do f
   line = 1
   while !eof(f)
     x = readline(f)
     println("$line $x")
     line += 1
   end
 end

A better approach is to use enumerate() on an iterable object – you'll get the line numbering 'for free':

open("sherlock-holmes.txt") do f
    for i in enumerate(eachline(f))
      println(i[1], ": ", i[2])
    end
end

If you have a specific function that you want to call on a file, you can use this alternative syntax:

function shout(f::IOStream)
    return uppercase(read(f, String))
end
julia> shoutversion = open(shout, "sherlock-holmes.txt");
julia> shoutversion[30237:30400]
"ELEMENTARY PROBLEMS. LET HIM, ON MEETING A\nFELLOW-MORTAL, LEARN AT A GLANCE TO DISTINGUISH THE HISTORY OF THE\nMAN, AND THE TRADE OR  PROFESSION TO WHICH HE BELONGS. "

This opens the file, runs the shout() function on it, then closes it again, assigning the processed contents to the variable.

You can use the CSV.jl to read and write comma-separated-values (.csv) files, and it's recommended over (handles more corner cases and can be faster, especially for larger files) using DelimitedFiles.readdlm() function to read lines delimited with certain characters, such as data files, arrays stored as text files, and tables. If you use the DataFrames package, there's also a readtable() specifically designed to read data into a table.

Working with paths and filenames

[edit | edit source]

These functions will be useful for working with filenames:

  • cd(path) changes the current directory.
  • pwd() gets the current working directory.
  • readdir(path) returns a lists of the contents of a named directory, or the current directory.
  • abspath(path) adds the current directory's path to a filename to make an absolute pathname.
  • joinpath(str, str, ...) assembles a pathname from pieces.
  • isdir(path) tells you whether the path is a directory.
  • splitdir(path) – split a path into a tuple of the directory name and file name.
  • splitdrive(path) – on Windows, split a path into the drive letter part and the path part. On Unix systems, the first component is always the empty string.
  • splitext(path) – if the last component of a path contains a dot, split the path into everything before the dot and everything including and after the dot. Otherwise, return a tuple of the argument unmodified and the empty string.
  • expanduser(path) – replaces a tilde character at the start of a path with the current user's home directory.
  • normpath(path) – normalizes a path, removing "." and ".." entries.
  • realpath(path) – canonicalizes a path by expanding symbolic links and removing "." and ".." entries.
  • homedir() – gets current user's home directory.
  • dirname(path) – gets the directory part of a path.
  • basename(path) – gets the file name part of a path.

To work on a restricted selection of files in a directory, use filter() and an anonymous function to filter the file names and just keep the ones you want. (filter() is more of a fishing net or sieve, rather than a coffee filter, in that it catches what you want to keep.)

for f in filter(x -> endswith(x, "jl"), readdir())
    println(f)
end

Astro.jl
calendar.jl
constants.jl
coordinates.jl
...
pseudoscience.jl
riseset.jl
sidereal.jl
sun.jl
utils.jl
vsop87d.jl

If you want to match a group of files using a regular expression, then use occursin(). Let's look for files with ".jpg" or ".png" suffixes (remembering to escape the "."):

for f in filter(x -> occursin(r"(?i)\.jpg|\.png", x), readdir())
    println(f)
end
034571172750.jpg
034571172750.png
51ZN2sCNfVL._SS400_.jpg
51bU7lucOJL._SL500_AA300_.jpg
Voronoy.jpg
kblue.png
korange.png
penrose.jpg
r-home-id-r4.png
wave.jpg

To examine a file hierarchy, use walkdir(), which lets you work through a directory, and examine the files in each directory in turn.

File information

[edit | edit source]

If you want information about a specific file, use stat("pathname"), and then use one of the fields to find out the information. Here's how to get all the information and the field names listed for a file "i":

 for n in fieldnames(typeof(stat("i")))
    println(n, ": ", getfield(stat("i"),n))
end
device: 16777219
inode: 2955324
mode: 16877
nlink: 943
uid: 502
gid: 20
rdev: 0
size: 32062
blksize: 4096
blocks: 0
mtime:1.409769933e9
ctime:1.409769933e9

You can access these fields via a 'stat' structure:

julia> s = stat("Untitled1.ipynb")
StatStruct(mode=100644, size=64424)
julia> s.ctime
1.446649269e9

and you can also use some of them directly:

julia> ctime("Untitled2.ipynb")
1.446649269e9

although not size:

julia> s.size
64424

To work on specific files that meet conditions – all Jupyter files (i.e. files with the extension "ipynb") modified after a certain date, for example – you could use something like this:

using Dates
function output_file(path)
    println(stat(path).size, ": ", path)
end 

for afile in filter!(f -> endswith(f, "ipynb") && (mtime(f) > Dates.datetime2unix(DateTime("2015-11-03T09:00"))),
    readdir())
    output_file(realpath(afile))
end

Interacting with the file system

[edit | edit source]

The cp(), mv(), rm(), and touch() functions have the same names and functions as their Unix shell counterparts.

To convert filenames to pathnames, use abspath(). You can map this over a list of files in a directory:

julia> map(abspath, readdir())
67-element Array{String,1}:
"/Users/me/.CFUserTextEncoding"
"/Users/me/.DS_Store"
"/Users/me/.Trash"
"/Users/me/.Xauthority"
"/Users/me/.ahbbighrc"
"/Users/me/.apdisk"
"/Users/me/.atom"
...

To restrict the list to filenames that contain a particular substring, use an anonymous function inside filter() – something like this:

julia> filter(x -> occursin("re", x), map(abspath, readdir()))
4-element Array{String,1}:
"/Users/me/.DS_Store"
"/Users/me/.gitignore"
"/Users/me/.hgignore_global"
"/Users/me/Pictures"
...

To restrict the list to regular expression matches, try this:

julia> filter(x -> occursin(r"recur.*\.jl", x), map(abspath, readdir()))
2-element Array{String,1}:
 "/Users/me/julia/recursive-directory-scan.jl"
 "/Users/me/julia/recursive-text.jl"

Writing to files

[edit | edit source]

To write to a text file, open it using the "w" flag and make sure that you have permission to create the file in the specified directory:

open("/tmp/t.txt", "w") do f
    write(f, "A, B, C, D\n")
end

Here's how to write 20 lines of 4 random numbers between 1 and 10, separated by commas:

function fourrandom()
    return rand(1:10,4)
end

open("/tmp/t.txt", "w") do f
           for i in 1:20
              n1, n2, n3, n4 = fourrandom()
              write(f, "$n1, $n2, $n3, $n4 \n")
           end
       end

A quicker alternative to this is to use the DelimitedFiles.writedlm() function, described next:

using DelimitedFiles
writedlm("/tmp/test.txt", rand(1:10, 20, 4), ", ")

Writing and reading array to and from a file

[edit | edit source]

In the DelimitedFiles package are two convenient functions, writedlm() and readdlm(). These let you read/write an array or collection from/to a file.

writedlm() writes the contents of an object to a text file, and readdlm() reads the data from a file into an array:

julia> numbers = rand(5,5)
5x5 Array{Float64,2}:
0.913583  0.312291  0.0855798  0.0592331  0.371789
0.13747   0.422435  0.295057   0.736044   0.763928
0.360894  0.434373  0.870768   0.469624   0.268495
0.620462  0.456771  0.258094   0.646355   0.275826
0.497492  0.854383  0.171938   0.870345   0.783558

julia> writedlm("/tmp/test.txt", numbers)

You can see the file using the shell (type a semicolon ";" to switch):

<shell> cat "/tmp/test.txt"
.9135833328830523	.3122905420350348	.08557977218948465	.0592330821115965	.3717889559226475
.13747015238054083	.42243494637594203	.29505701073304524	.7360443978397753	.7639280496847236
.36089432672073607	.43437288984307787	.870767989032692	.4696243851552686	.26849468736154325
.6204624598015906	.4567706404666232	.25809436255988105	.6463554854347682	.27582613759302377
.4974916625466639	.8543829989347014	.17193814498701587	.8703447748713236	.783557793485824

The elements are separated by tabs unless you specify another delimiter. Here, a colon is used to delimit the numbers:

julia> writedlm("/tmp/test.txt", rand(1:6, 10, 10), ":")
shell> cat "/tmp/test.txt"
3:3:3:2:3:2:6:2:3:5
3:1:2:1:5:6:6:1:3:6
5:2:3:1:4:4:4:3:4:1
3:2:1:3:3:1:1:1:5:6
4:2:4:4:4:2:3:5:1:6
6:6:4:1:6:6:3:4:5:4
2:1:3:1:4:1:5:4:6:6
4:4:6:4:6:6:1:4:2:3
1:4:4:1:1:1:5:6:5:6
2:4:4:3:6:6:1:1:5:5

To read in data from a text file, you can use readdlm().

julia> numbers = rand(5,5)
5x5 Array{Float64,2}:
0.862955  0.00827944  0.811526  0.854526  0.747977
0.661742  0.535057    0.186404  0.592903  0.758013
0.800939  0.949748    0.86552   0.113001  0.0849006
0.691113  0.0184901   0.170052  0.421047  0.374274
0.536154  0.48647     0.926233  0.683502  0.116988
julia> writedlm("/tmp/test.txt", numbers)

julia> numbers = readdlm("/tmp/test.txt")
5x5 Array{Float64,2}:
0.862955  0.00827944  0.811526  0.854526  0.747977
0.661742  0.535057    0.186404  0.592903  0.758013
0.800939  0.949748    0.86552   0.113001  0.0849006
0.691113  0.0184901   0.170052  0.421047  0.374274
0.536154  0.48647     0.926233  0.683502  0.116988

There are also a number of Julia packages specifically designed for reading and writing data to files, including DataFrames.jl and CSV.jl. Search in JuliaHub or JuliaPackages for these and more. Many of these packages live at the home of the JuliaData organization.

Working with Dates and Times

[edit | edit source]
Previous page
Working with text files
Introducing Julia Next page
Plotting
Working with dates and times

Working with dates and times

[edit | edit source]

Functions for working with dates and times are provided in the standard package Dates. To use any of the time and date functions, you must do one of the following:

  • using Dates
  • import Dates

If you use import Dates functions, you’ll need to prefix every function with an explicit Dates., e.g. Dates.dayofweek(dt), as shown in this chapter. However, if you add the line using Dates to your code, this brings all exported Dates functions into Main, and they can be used without the Dates. prefix.

Types

[edit | edit source]

This diagram shows the relationship between the various types used to store Times, Dates, and DateTimes.

Shows the hierarchy of date and date-time types in Julia
Shows the hierarchy of date and date-time types in Julia

Date, Time, and DateTimes

[edit | edit source]

There are three main datatypes available:

  • A Dates.Time object represents a precise moment of time in a day. It doesn't say anything about the day of the week, or the year, though. It's accurate to a nanosecond.
  • A Dates.Date object represents just a date: no time zones, no daylight saving issues, etc... It's accurate to, well, a day.
  • A Dates.DateTime object is a combination of a date and a time of day, and so it specifies an exact moment in time. It's accurate to a millisecond or so.

Use one of these constructors to make the type of object you want:

julia> rightnow = Dates.Time(Dates.now()) # a Dates.Time object
16:51:56.374
julia> birthday = Dates.Date(1997,3,15)   # a Dates.Date object
1997-03-15

julia> armistice = Dates.DateTime(1918,11,11,11,11,11) # a Dates.DateTime object
1918-11-11T11:11:11

The Dates.today() function returns a Date object for the current date:

julia> datetoday = Dates.today()
2014-09-02

The Dates.now() function returns a DateTime object for the current instant in time:

julia> datetimenow = Dates.now()
2014-09-02T08:20:07.437

(We used Dates.now() earlier to define rightnow, then converted it to a Dates.Time using Dates.Time().)

Sometimes you want UTC (the reference time for the world, without local adjustments for daylight savings):

julia> Dates.now(Dates.UTC)
2014-09-02T08:27:54.14

To create an object from a formatted string, use the DateTime() function in Dates, and supply a suitable format string that matches the formatting:

julia> Dates.DateTime("20140529 120000", "yyyymmdd HHMMSS")
2014-05-29T12:00:00

julia> Dates.DateTime("18/05/2009 16:12", "dd/mm/yyyy HH:MM")
2009-05-18T16:12:00

julia> vacation = Dates.DateTime("2014-09-02T08:20:07") # defaults to expecting ISO8601 format
2014-09-02T08:20:07

See Date Formatting below for more examples.

Date and time queries

[edit | edit source]

Once you have a date/time or date object, you can extract information from it with the following functions. For both date and datetime objects, you can obtain the year, month, day, and so on:

julia> Dates.year(birthday)
1997

julia> Dates.year(datetoday)
2014

julia> Dates.month(birthday)
3

julia> Dates.month(datetoday)
9

julia> Dates.day(birthday)
15

julia> Dates.day(datetoday)
2

and, for date/time objects:

julia> Dates.minute(now())
37

julia> Dates.hour(now())
16

julia> Dates.second(now())
8
julia> Dates.minute(rightnow)
37

julia> Dates.hour(rightnow)
16

julia> Dates.second(rightnow)
8

There's also a bunch of other useful ones:

julia> Dates.dayofweek(birthday)
6

julia> Dates.dayname(birthday)
"Saturday"

julia> Dates.yearmonth(now())
(2014,9)

julia> Dates.yearmonthday(birthday)
(1997,3,15)

julia> Dates.isleapyear(birthday)
false

julia> Dates.daysofweekinmonth(datetoday)
5

julia> Dates.monthname(birthday)
"March"

julia> Dates.monthday(now())
(9,2)

julia> Dates.dayofweekofmonth(birthday)
3 

Two of those functions are very similarly named: the Dates.daysofweekinmonth() (days of week in month) function tells you how many days there are in the month with the same day name as the specified day — there are five Tuesdays in the current month (at the time of writing). The last function, dayofweekofmonth(birthday) (day of week of month), tells us that the 15th of March, 1997, was the third Saturday of the month.

You can also find days relative to a date, such as the first day of the week containing that day, using the adjusting functions, described below.

Date arithmetic

[edit | edit source]

You can do arithmetic on dates and date/time objects. Subtracting two dates or datetimes to find the difference is the most obvious one:

julia> datetoday - birthday
6380 days

julia> datetimenow - armistice
3023472252000 milliseconds

which you can convert to Dates.Days or Dates.Milliseconds or some other unit:

julia> Dates.Period(datetoday - birthday)
7357 days

julia> Dates.canonicalize(Dates.CompoundPeriod(datetimenow - armistice))
5138 weeks, 5 days, 5 hours, 46 minutes, 1 second, 541 milliseconds

julia> convert(Dates.Day, Dates.Period(Dates.today() - Dates.Date(2016, 1, 1)))
491 days

julia> convert(Dates.Millisecond, Dates.Period(Dates.today() - Dates.Date(2016, 1, 1)))
42422400000 milliseconds

To add and subtract periods of time to date and date/time objects, use the Dates. constructor functions to specify the period. For example, Dates.Year(20) defines a period of 20 years, and Dates.Month(6) defines a period of 6 months. So, to add 20 years and 6 months to the birthday date:

julia> birthday + Dates.Year(20) + Dates.Month(6)
2017-09-15

Here's 6 months ago from now:

julia> Dates.now() - Dates.Month(6)
2014-03-02T16:43:08

and similarly for months, weeks:

julia> Dates.now() - Dates.Year(2) - Dates.Month(6)
2012-03-02T16:44:03

and similarly for weeks and hours. Here's the date and time for two weeks and 12 hours from now:

julia> Dates.now() + Dates.Week(2) + Dates.Hour(12)
2015-09-18T20:49:16

and there are

julia> daystoxmas = Dates.Date(Dates.year(Dates.now()), 12, 25) - Dates.today()
148 days

or 148 (shopping) days till Christmas (at the time this was written).

To retrieve the value as a number, use the function Dates.value():

julia> Dates.value(daystoxmas)
148

This works with different types of date/time objects too:

julia> lastchristmas = Dates.now() - Dates.DateTime(2017, 12, 25, 0, 0, 0)
25464746504 milliseconds

julia> Dates.value(lastchristmas)
25464746504

Range of dates

[edit | edit source]

You can make iterable range objects that define a range of dates:

julia>  d = Dates.Date(1980,1,1):Dates.Month(3):Dates.Date(2019,1,1)
1980-01-01:3 months:2019-01-01

This iterator yields the first day of every third month. To find out which of these fall on weekdays, you can provide an anonymous function to filter() that tests the day name against the given day names:

julia> weekdays = filter(dy -> Dates.dayname(dy) != "Saturday" && Dates.dayname(dy) != "Sunday" , d)

104-element Array{Date,1}:

1980-01-01
1980-04-01
1980-07-01
⋮         
2014-07-01
2014-10-01
2016-04-01
2016-07-01
2018-01-01
2018-10-01
2019-01-01

Similarly, here's a range of times 3 hours apart from now, for a year hence:

julia> d = collect(Dates.DateTime(Dates.now()):Dates.Hour(3):Dates.DateTime(Dates.now() + Dates.Year(1)))
2929-element Array{DateTime,1}:
2015-09-04T08:30:59
2015-09-04T11:30:59
2015-09-04T14:30:59
 ⋮                  
2016-09-03T20:30:59
2016-09-03T23:30:59
2016-09-04T02:30:59
2016-09-04T05:30:59
2016-09-04T08:30:59

If you have to pay a bill every 30 days, starting on the 1st of January 2018, the following code shows how the due date creeps forward every month:

julia> foreach(d -> println(Dates.format(d, "d u yyyy")), Dates.Date("2018-01-01"):Dates.Day(30):Dates.Date("2019-01-01"))
1 Jan 2018
31 Jan 2018
2 Mar 2018
1 Apr 2018
1 May 2018
31 May 2018
30 Jun 2018
30 Jul 2018
29 Aug 2018
28 Sep 2018
28 Oct 2018
27 Nov 2018
27 Dec 2018

Date formatting

[edit | edit source]

To specify date formats, you use date formatting codes in a formatting string. Each character refers to a date/time element:

y  Year digit eg yyyy => 2015, yy => 15
m  Month digit eg m => 3 or 03
u  Month name eg Jan
U  Month name eg January
e  Day of week eg Tue
E  Day of week eg Tuesday
d  Day eg 3 or 03
H  Hour digit eg HH => 00
M  Minute digit eg MM => 00
S  Second digit eg S => 00
s  Millisecond digit eg .000

You can use these formatting strings with functions such as DateTime() and Dates.format(). For example, you create a DateTime object from a string by identifying the different elements in the incoming string:

julia> Dates.Date("Fri, 15 Jun 2018", "e, d u y")
2018-06-15
julia> Dates.DateTime("Fri, 15 Jun 2018 11:43:14", "e, d u y H:M:S")
2018-06-15T11:43:14

Other characters are used literally. In the second example, the formatting characters matched up as follows:

Fri, 15 Jun 2018 11:43:14
e  ,  d   u    y  H: M: S

You can supply a format string to Dates.format to format a date object. In the formatting string, you repeat the characters to control how years and days, for example, are output:

julia> timenow = Dates.now()
2015-07-28T11:43:14
julia> Dates.format(timenow, "e, dd u yyyy HH:MM:SS")
"Tue, 28 Jul 2015 11:43:14"

When you're creating a formatted date, you can double some of the components of the format string to produce a leading zero for single digit date elements:

julia> anothertime = Dates.DateTime("Tue, 8 Jul 2015 2:3:7", "e, d u y H:M:S")
2015-07-08T02:03:07

julia> Dates.format(anothertime, "e: dd u yy, HH.MM.SS") # with leading zeros
"Wed: 08 Jul 15, 02.03.07"

julia> Dates.format(anothertime, "e: d u yy, H.M.S")
"Wed: 8 Jul 15, 2.3.7"

To convert a date string from one format to another, you can use DateTime() and a format string to convert the string to a DateTime object, then DateFormat() to output the object in a different format:

julia> formatted_date = "Tue, 28 Jul 2015 11:43:14"
"Tue, 28 Jul 2015 11:43:14"

julia> temp = Dates.DateTime(formatted_date, "e, dd u yyyy HH:MM:SS")
2015-07-28T11:43:14

julia> Dates.format(temp, "dd, U, yyyy HH:MM, e")
"28, July, 2015 11:43, Tue"

If you're doing a lot of date formatting (you can apply date functions to an array of strings), it's a good idea to pre-define a DateFormat object and then use that for bulk conversions (this is quicker):

julia> dformat = Dates.DateFormat("y-m-d");

julia> Dates.Date.([   # broadcast
      "2010-01-01", 
      "2011-03-23", 
      "2012-11-3", 
      "2013-4-13", 
      "2014-9-20", 
      "2015-3-1"
      ], dformat)
6-element Array{Date,1}:
 2010-01-01
 2011-03-23
 2012-11-03
 2013-04-13
 2014-09-20
 2015-03-01

There are some built-in formats that you can use. For example, there's Dates.ISODateTimeFormat to give you the ISO8601 format:

julia> Dates.DateTime.([  
          "2010-01-01", 
          "2011-03-23", 
          "2012-11-3", 
          "2013-4-13", 
          "2014-9-20", 
          "2015-3-1" 
          ], Dates.ISODateTimeFormat) 
6-element Array{DateTime,1}:
2010-01-01T00:00:00
2011-03-23T00:00:00
2012-11-03T00:00:00
2013-04-13T00:00:00
2014-09-20T00:00:00
2015-03-01T00:00:00

and here's good old RFC1123:

julia> Dates.format(Dates.now(), Dates.RFC1123Format)
"Sat, 30 Jul 2016 16:36:09"

Date adjustments

[edit | edit source]

Sometimes you want to find a date nearest to another - for example, the first day of that week, or the last day of the month that contains that date. You can do this with the functions like Dates.firstdayofweek() and Dates.lastdayofmonth(). So, if we're currently in the middle of the week:

julia> Dates.dayname(now())
"Wednesday"

the first day of the week is returned by this:

julia> Dates.firstdayofweek(now())
2014-09-01T00:00:00

which you could also write using the function chain operator:

julia> Dates.now() |> Dates.firstdayofweek |> Dates.dayname 
"Monday"

A more general solution is provided by the tofirst(), tolast(), tonext(), and toprev() methods.

With tonext() and toprev(), you can provide a (possibly anonymous) function that returns true when a date has been correctly adjusted. For example, the function:

d->Dates.dayofweek(d) == Dates.Tuesday

returns true if the day d is a Tuesday. Use this with the tonext() method:

julia> Dates.tonext(d->Dates.dayofweek(d) == Dates.Tuesday, birthday)
1997-03-18 # the first Tuesday after the birthday

Or you can find the next Sunday following the birthday date:

julia> Dates.tonext(d->Dates.dayname(d) == "Sunday", birthday)
1997-03-16 # the first Sunday after the birthday

With tofirst() and tolast(), you can find the first Sunday, or Thursday, or whatever, of a month. Monday is 1, Tuesday 2, etc.

julia> Dates.tofirst(birthday, 1) # the first Monday (1) of that month
1997-03-03

Supply the keyword argument of=Year to get the first matching weekday of the year.

julia> Dates.tofirst(birthday, 1, of=Year) # the first Monday (1) of 1997
1997-01-06

Rounding dates and times

[edit | edit source]

You can use round(), floor(), and ceil(), usually used to round numbers up or down to the nearest preferred values, to adjust dates forward or backwards in time so that they have 'rounder' values.

julia> Dates.now()
2016-09-12T17:55:11.378

julia> Dates.format(round(Dates.DateTime(Dates.now()), Dates.Minute(15)), Dates.RFC1123Format)
"Mon, 12 Sep 2016 18:00:00"

The ceil() adjusts dates or times forward in time:

julia> ceil(birthday, Dates.Month)
1997-04-01

julia> ceil(birthday, Dates.Year)
1998-01-01

julia> ceil(birthday, Dates.Week)
1997-03-17

Recurring dates

[edit | edit source]

It's useful to be able to find all dates in a range of dates that satisfy some particular criteria. For example, you can work out the second Sunday in a month by using the Dates.dayofweekofmonth() and Dates.dayname() functions.

For example, let's create a range of dates from the first of September 2014 until Christmas Day, 2014:

julia> dr = Dates.Date(2014,9,1):Dates.Day(1):Dates.Date(2014,12,25)
2014-09-01:1 day:2014-12-25

Now an anonymous function similar to the ones we used in tonext() earlier finds a selection of those dates in that range that satisfy that function:

julia> filter(d -> Dates.dayname(d) == "Sunday", dr)
16-element Array{Date,1}:
 2014-09-07
 2014-09-14
 2014-09-21
 2014-09-28
 2014-10-05
 2014-10-12
 2014-10-19
 2014-10-26
 2014-11-02
 2014-11-09
 2014-11-16
 2014-11-23
 2014-11-30
 2014-12-07
 2014-12-14
 2014-12-21

These are the dates of every Sunday between September 1st 2014 until Christmas Day, 2014.

By combining criteria in the anonymous function, you can build up more complicated recurring events. Here's a list of all the Tuesdays in that period which are on days that are odd numbered and greater than 20:

julia> filter(d->Dates.dayname(d) == "Tuesday" && isodd(Dates.day(d)) && Dates.day(d) > 20, dr)
4-element Array{Date,1}:
2014-09-23
2014-10-21
2014-11-25
2014-12-23

and here's every second Tuesday in 2016 between April and November:

dr = Dates.Date(2015):Dates.Day(1):Dates.Date(2016);
filter(dr) do x
    Dates.dayofweek(x) == Dates.Tue &&
    Dates.April <= Dates.month(x) <= Dates.Nov &&
    Dates.dayofweekofmonth(x) == 2
end
8-element Array{Base.Dates.Date,1}:
 2015-04-14
 2015-05-12
 2015-06-09
 2015-07-14
 2015-08-11
 2015-09-08
 2015-10-13
 2015-11-10

Unix time

[edit | edit source]

You sometimes have to deal with another type of timekeeping: Unix time. Unix time is a count of the number of seconds that have elapsed since the beginning of the year 1970 (the birth of Unix). In Julia the count is stored in a 64 bit integer, and we'll never see the end of Unix time. (The universe will have ended long before 64 bit Unix time reaches the maximum possible value, which will be in approximately 292 billion years from now, at 15:30:08 on Sunday, 4 December 292,277,026,596.)

In Julia, the time() function, used without arguments, returns the Unix time value of the current second:

julia> time()
1.414141581230945e9

The strftime() ("string format time") function, which lives in the Libc module, converts a number of seconds in Unix time to a more readable form:

julia> Libc.strftime(86400 * 365.25 * 4) # 4 years worth of Unix seconds
"Tue  1 Jan 00:00:00 1974"

You can choose a different format by supplying a format string, with the different components of the date and time defined by '%' letter codes:

julia> Libc.strftime("%A, %B %e at %T, %Y", 86400 * 365.25 * 4)
"Tuesday, January  1 at 00:00:00, 1974"

The strptime() function takes a format string and a date string, and returns a TmStruct expression. This can then be converted to a Unix time value by passing it to time():

julia> Libc.strptime("%A, %B %e at %T, %Y", "Tuesday, January  1 at 00:00:00, 1974")
Base.Libc.TmStruct(0,0,0,1,0,74,2,0,0,0,0,0,0,0)

julia> time(ans)
1.262304e8

julia> time(Libc.strptime("%Y-%m-%d","2014-10-1"))
1.4121216e9

The Dates module also offers a unix2datetime() function, which converts a Unix time value to a date/time object:

julia> Dates.unix2datetime(time())
2014-10-24T09:26:29.305

Moments in time

[edit | edit source]

DateTimes are stored as milliseconds, in the field instant. Use Dates.value to obtain the value.

julia> moment=Dates.now()
2017-02-01T12:45:46.326
  
julia> Dates.value(moment)
63621636346326
julia> moment.instant
Base.Dates.UTInstant{Base.Dates.Millisecond}(63621636346326 milliseconds)

If you use the more precise Dates.Time type, you can access nanoseconds.

julia> moment = Dates.Time(Dates.now())
17:38:44.33
julia> Dates.value(moment)
63524330000000
 
julia> moment.instant
63524330000000 nanoseconds

Timing and monitoring

[edit | edit source]

The @elapsed macro returns the number of seconds an expression took to evaluate:

function test(n)
     for i in 1:n
        x = sin(rand())
     end
end
julia> @elapsed test(100000000)
1.309819509

The @time macro tells you how long an expression took to evaluate, and how memory was allocated.

julia> @time test(100000000)
2.532941 seconds (4 allocations: 160 bytes)

Plotting

[edit | edit source]
Previous page
Working with dates and times
Introducing Julia Next page
Metaprogramming
Plotting

Plotting

[edit | edit source]

There are a number of different packages for plotting in Julia, and there's probably one to suit your needs and tastes. This section is a quick introduction to one of them, Plots.jl, which is interesting because it talks to many of the other plotting packages. Before making plots with Julia, download and install the first plotting package or any or all to choose from (to get the prompt press ]):

(v1.0) pkg> add Plots PyPlot GR UnicodePlots  # See also Gnuplot.jl (and Gaston.jl alternative for)

The first package, Plots, is a high-level plotting package that interfaces with other plotting packages, which here are referred to as 'back-ends'. They act as the graphics "engines" that produce the graphics. Each of these is also a stand-alone plotting package, and can be used separately, but the advantage of using Plots as the interface is, as you'll see, a simpler and consistent interface.

See also the powerful Makie.jl, which is unrelated to Plots.jl, and has its own backends, such as GLMakie.jl, and many extensions such as AlgebraOfGraphics.jl (these are not explained more in the article, except for this installation example):

(v1.6) pkg> add GLMakie AlgebraOfGraphics

You can start using the Plots.jl package in a Julia session in the usual way:

julia> using Plots

You usually want to plot one or more series, arrays of numerical values. Alternatively, you can provide one or more functions to generate numerical values.

In this example, we'll plot the phases (illuminated fractions) of the moon for the month of May, 2022.

julia> using AstroLib  # add if necessary with ] add AstroLib
julia> using Dates 
julia> points = DateTime(2022,05,01):Dates.Hour(1):DateTime(2022,05,31,23,59,59)
julia> moonphases = mphase.(jdcnv.(points)) 

We now have an array of Float64 values, one for each hour of the month, representing how much of the moon's disk is illuminated:

julia> moonphases
744-element Vector{Float64}:
 0.0002806471321559201
 0.00041259024384365794
 0.0005791256946680035
 0.0007801698949687075
 0.0010156372084771381
 0.0012854399754271273
 ⋮
 0.015263669925646928
 0.016249662554591593
 0.017266056993952783
 0.018312770267986556
 0.019389718259650524
 0.020496815690093984

To plot these series, just pass them to Plots' plot() function.

julia> plot(points, moonphases)

This has used the first available plotting engine (GR.jl). Plots has added other plotting "furniture" and then plotted everything for you.

If you want to switch to a different engine, use one of the provided functions: gr(), unicodeplots(), plotly(), and so on. For example, to switch to using the Unicodeplots plotting package (which uses Unicode characters to make plots, and is ideal for use in the REPL/terminal), do this:

 julia> '''unicodeplots()'''
 
 julia> '''plot(moonphases)'''
    
     ┌────────────────────────────────────────┐ 
   1 │                .:''':.                 │ 
     │               .:     '.                │ 
     │              :'       ':               │ 
     │             :'         '.              │ 
     │            .'           :.             │ 
     │           .:             :             │ 
     │          .:               :            │ 
     │         .:                ':           │ 
     │         :                  :.          │ 
     │        :                    :.         │ 
     │       :'                     :.        │ 
     │      :'                       :.       │ 
     │    .:                          :.      │ 
     │   .:                            :.     │ 
   0 │..:'                              ':....│ 
     └────────────────────────────────────────┘

Customizing plots

[edit | edit source]

There is copious documentation for the Plots.jl package, and after studying it you'll be able to spend hours tweaking and customizing your plots to your heart's content. Here's just one example of a plot of the equation of time for every day in a year.

The ticks along the x-axis are the numbers from 1:365. It would be better to see the dates themselves. First, create the strings:

julia> days = Dates.DateTime(2018, 1, 1, 0, 0, 0):Dates.Day(1):Dates.DateTime(2018, 12, 31, 0, 0, 0)
julia> datestrings = Dates.format.(days, "u dd")

The supplied value for the xticks option is a tuple consisting of two arrays/ranges:

(xticks = (1:14:366, datestrings[1:14:366])

the first provides the numerical values, the second provides matching text labels for the ticks.

Extra labels and legends are easily added. You can access colors from the Colors.jl package:

julia>  plot!(                                    
    eq_values,                                        
                                                      
    label  = "equation of time (calculated)",     
    line=(:black, 0.5, 6, :solid),                
                                                  
    size=(800, 600),                              
                                                  
    xticks = (1:14:366, datestrings[1:14:366]),   
    yticks = -20:2.5:20,                          
                                                  
    ylabel = "Minutes faster or slower than GMT", 
    xlabel = "day in year",                       
                                                  
    title  = "The Equation of Time",              
    xrotation = rad2deg(pi/3),                    
                                                  
    fillrange = 0,                                
    fillalpha = 0.25,                             
    fillcolor = :lightgoldenrod,                  
                                                  
    background_color = :ivory                     
    )                                             

examples of plotting in Julia using Plots.jl

Other packages

[edit | edit source]

UnicodePlots

[edit | edit source]

If you work in the REPL a lot, perhaps you want a quick and easy way to draw plots that use text rather than graphics for output? The UnicodePlots.jl package uses Unicode characters to draw various plots, avoiding the need to load various graphic libraries. It can produce:

  • scatter plots
  • line plots
  • bar plots (horizontal)
  • staircase plots
  • histograms (horizontal)
  • sparsity patterns
  • density plots

Download and add it to your Julia installation, if you haven't already done so:

pkg> add UnicodePlots

You have to do this just once. Now you load the module and import the functions:

julia> using UnicodePlots

Here is a quick example of a line plot:

julia> myPlot = lineplot([1, 2, 3, 7], [1, 2, -5, 7], title="My Plot", border=:dotted)
                       My Plot
      ⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤
   10 ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠔⠒⠊⠉⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠉⠉⠉⠉⠉⠉⠉⠉⠉⠫⡉⠉⠉⠉⠉⠉⢉⠝⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠱⡀⠀⢀⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
  -10 ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸
      ⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚
      0                                       10

And here's a density plot:

julia> myPlot = densityplot(collect(1:100), randn(100), border=:dotted)
      ⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤
   10 ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                            ░           ⢸
      ⡇ ░░░        ░ ▒░  ▒░     ░  ░ ░ ░ ░   ░ ⢸
      ⡇░░  ░▒░░▓▒▒ ▒░░ ▓░░ ░░░▒░ ░ ░   ▒ ░ ░▒░░⢸
      ⡇▓▒█▓▓▒█▓▒▒▒█▒▓▒▓▒▓▒▓▓▒▓▒▓▓▓█▒▒█▓▒▓▓▓▓▒▒▒⢸
      ⡇    ░     ░         ░░░ ░    ▒ ░ ░ ░░ ░ ⢸
      ⡇                          ░             ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
      ⡇                                        ⢸
  -10 ⡇                                        ⢸
      ⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚
      0                                      100

(Note that it needs the terminal environment for the displayed graphs to be 100% successful - when you copy and paste, some of the magic is lost.)

VegaLite

[edit | edit source]

allows you to create visualizations in a web browser window. VegaLite is a visualization grammar, a declarative format for creating and saving visualization designs. With VegaLite you can describe data visualizations in a JSON format, and generate interactive views using either HTML5 Canvas or SVG. You can produce:

  • Area plots
  • Bar plots/Histograms
  • Line plots
  • Scatter plots
  • Pie/Donut charts
  • Waterfall charts
  • Wordclouds

To use VegaLite, first add the package to your Julia installation. You have to do this just once:

pkg> add VegaLite

Here's how to create a stacked area plot.

julia> using VegaLite
julia> x = [0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]
julia> y = [28, 43, 81, 19, 52, 24, 87, 17, 68, 49, 55, 91, 53, 87, 48, 49, 66, 27, 16, 15]
julia> g = [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1]
 
julia> a = areaplot(x = x, y = y, group = g, stacked = true)

graphic created with Julia and Vega.jl

A general feature of VegaLite is that you can modify a visualization after you've created it. So, let's change the color scheme using a function (notice the "!" to indicate that the arguments are modified):

julia> colorscheme!(a, ("Reds", 3))

graphic created with Julia and Vega.jl

You can create pie (and donut) charts easily by supplying two arrays. The x array provides the labels, the y array provides the quantities:

julia> fruit = ["peaches", "plums", "blueberries", "strawberries", "bananas"];
julia> bushels = [100, 32, 180, 46, 21];
julia> piechart(x = fruit, y = bushels, holesize = 125)

a pie/donut chart created in Julia/Vega.jl

Metaprogramming

[edit | edit source]
Previous page
Plotting
Introducing Julia Next page
Modules and packages
Metaprogramming

What is metaprogramming?

[edit | edit source]

Meta-programming is when you write Julia code to process and modify Julia code. With the meta-programming tools, you can write Julia code that modifies other parts of your source files, and even control if and when the modified code runs.

In Julia, the execution of raw source code takes place in two stages. (In reality there are more stages than this, but at this point we'll focus on just these two.)

Stage 1 is when your raw Julia code is parsed — converted into a form that is suitable for evaluation. You'll be familiar with this phase, because this is when all your syntax mistakes are noticed... The result of this is an abstract syntax tree or AST (Abstract Syntax Tree), a structure that contains all your code, but in a format that is easier to manipulate than the human-friendly syntax normally used.

Stage 2 is when that parsed code is executed. Usually, when you type code into the REPL and press Return, or when you run a Julia file from the command line, you don't notice the two stages, because they happen so quickly. However, with Julia's metaprogramming facilities, you can access the code after it's been parsed but before it's evaluated.

This lets you do things that you can't normally do. For example, you can convert simple expressions to more complicated expressions, or examine code before it runs and change it so that it runs faster. Any code that you intercept and modify using these meta-programming tools will eventually be evaluated in the usual way, running as fast as ordinary Julia code.

You may have already used two existing examples of meta-programming in Julia:

- the @time macro:

julia> @time [sin(cos(i)) for i in 1:100000];
0.102967 seconds (208.65 k allocations: 9.838 MiB)

The @time macro inserts a "start the stopwatch" command at the beginning of the code, and adds some code at the end to "stop the stopwatch", and calculate the elapsed time and memory usage. The modified code is then passed on for evaluation.

- the @which macro

julia> @which 2 + 2
+(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:53

This macro doesn't allow the expression 2 + 2 to be evaluated at all. Instead, it reports which method would be used for these particular arguments. And it also tells you the source file that contains the method's definition, and the line number.

Other uses for meta-programming include the automation of tedious coding jobs by writing short pieces of code that produce larger chunks of code, and the ability to improve the performance of 'standard' code by producing the sort of faster code that perhaps you wouldn't want to write by hand.

Quoted expressions

[edit | edit source]

For meta-programming to be possible, there has to be a way for Julia to store an unevaluated but parsed expression, as soon as the parsing phase has finished. This is the ':' (colon) prefix operator:

julia> x = 3
3

julia> :x
:x 

To Julia, the :x is an unevaluated or quoted symbol.

(If you're unfamiliar with the use of quoted symbols in computer programming, think of how quotes are sometimes used in writing to distinguish between ordinary use and special use. For example, in the sentence:

'Copper' contains six letters.

the quotes indicate that the word 'Copper' is not a reference to the metal, but to the word itself. In the same way, in :x, the colon before the symbol is to make you and Julia think of 'x' as an unevaluated symbol rather than as the value 3.)

To quote whole expressions rather than individual symbols, start with a colon and then enclose the Julia expression in parentheses:

julia> :(2 + 2)
:(2 + 2)

There's an alternative form of the :( ) construction that uses the quote ... end keywords to enclose and quote an expression:

quote
   2 + 2
end

which returns:

quote
    #= REPL[123]:2 =#
    2 + 2
end

And this expression:

expression = quote
   for i = 1:10
      println(i)
   end
end

returns:

quote
    #= REPL[124]:2 =#
    for i = 1:10
        #= REPL[124]:3 =#
        println(i)
    end
end

The expression object is of type Expr:

julia> typeof(expression)
Expr

It's parsed, primed, and ready to go.

Evaluating expressions

[edit | edit source]

There's also a function for evaluating an unevaluated expression. It's called eval():

julia> eval(:x)
3
julia> eval(:(2 + 2))
4
julia> eval(expression)
1
2
3
4
5
6
7
8
9
10

With these tools, it's possible to create any expression and store it without having it evaluate:

e = :(
    for i in 1:10
        println(i)
    end
)

returning:

:(for i = 1:10 # line 2:
    println(i)
end)

and then to recall and evaluate it later:

julia> eval(e)
1
2
3
4
5
6
7
8
9
10

More usefully, it's possible to modify the contents of the expression before it's evaluated.

Inside Expressions

[edit | edit source]

Once you have Julia code in an unevaluated expression, rather than as a piece of text in a string, you can do things with it.

Here's another expression:

P = quote
   a = 2
   b = 3
   c = 4
   d = 5
   e = sum([a,b,c,d])
end

which returns:

quote
    #= REPL[125]:2 =#
    a = 2
    #= REPL[125]:3 =#
    b = 3
    #= REPL[125]:4 =#
    c = 4
    #= REPL[125]:5 =#
    d = 5
    #= REPL[125]:6 =#
    e = sum([a, b, c, d])
end

Notice the helpful line numbers that have been added to each line of the quoted expression. (The labels for each line are added on the end of the previous line.)

We can use the fieldnames() function to see what's inside this expression:

julia> fieldnames(typeof(P))
(:head, :args, :typ)

The head field is :block. The args field is an array, containing expressions (including comments). We can examine these with the usual Julia techniques. For example, what's the second subexpression:

julia> P.args[2]
:(a = 2)

Print them out:

for (n, expr) in enumerate(P.args)
    println(n, ": ", expr)
end
1: #= REPL[125]:2 =#
2: a = 2
3: #= REPL[125]:3 =#
4: b = 3
5: #= REPL[125]:4 =#
6: c = 4
7: #= REPL[125]:5 =#
8: d = 5
9: #= REPL[125]:6 =#
10: e = sum([a, b, c, d])

As you can see, the expression P contains a number of sub-expressions. We can modify this expression quite easily; for example, we can change the last line of the expression to use prod() rather than sum(), so that, when P is evaluated, it will return the product rather than the sum of the variables.

julia> eval(P)
14

julia> P.args[end] = quote prod([a,b,c,d]) end
quote                  
   #= REPL[133]:1 =#  
   prod([a, b, c, d]) 
end                   

julia> eval(P)
120

Alternatively, you can target the sum() symbol directly by burrowing into the expression:

julia> P.args[end].args[end].args[1]
:sum

julia> P.args[end].args[end].args[1] = :prod
:prod

julia> eval(P)
120

The Abstract Syntax Tree

[edit | edit source]

This way of representing your code once it's been parsed is referred to as the AST (Abstract Syntax Tree). It's a nested hierarchical structure that's designed to allow both you and Julia to easily process and modify the code.

The very useful dump function lets you easily visualise the hierarchical nature of an expression. For example, the expression :(1 * sin(pi/2)) is represented like this:

julia> dump(:(1 * sin(pi/2)))
 Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol *
    2: Int64 1
    3: Expr
      head: Symbol call
      args: Array{Any}((2,))
        1: Symbol sin
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol /
            2: Symbol pi
            3: Int64 2
          typ: Any
      typ: Any
  typ: Any

You can see that the AST consists entirely of Exprs and atoms (e.g. symbols and numbers).

Expression interpolation

[edit | edit source]

In a way, strings and expressions are similar — any Julia code they happen to contain is usually unevaluated, but you can have some of the code evaluated using interpolation. We've met the string interpolation operator, the dollar sign ($). When used inside a string, and possibly with parentheses to enclose the expression, this evaluates the Julia code and inserts the resulting value into the string at that point:

julia> "the sine of 1 is $(sin(1))"
"the sine of 1 is 0.8414709848078965"

In just the same way, you can use the dollar sign to include the results of executing Julia code interpolated into an expression (which is otherwise unevaluated):

 julia> quote s = $(sin(1) + cos(1)); end
quote  # none, line 1:
    s = 1.3817732906760363
end

Even though this is a quoted expression and hence unevaluated, the value of sin(1) + cos(1) was calculated and inserted into the expression, replacing the original code. This operation is called "splicing".

As with string interpolation, the parentheses are needed only if you want to include the value of an expression — a single symbol can be interpolated using just a single dollar sign.

Macros

[edit | edit source]

Once you know how to create and handle unevaluated Julia expressions, you'll want to know how you can modify them. A macro is a way of generating a new output expression, given an unevaluated input expression. When your Julia program runs, it first parses and evaluates the macro, and the processed code produced by the macro is eventually evaluated like an ordinary expression.

Here's the definition of a simple macro that prints out the contents of the thing you pass to it, and then returns the expression to the calling environment (here, the REPL). The syntax is very similar to the way you define functions:

macro p(n)
    if typeof(n) == Expr 
       println(n.args)
    end
    return n
end

You run macros by preceding the name with the @ prefix. This macro is expecting a single argument. You're providing unevaluated Julia code, you don't have to enclose it with parentheses, like you do for function arguments.

First, let's call this with a single numeric argument:

julia> @p 3
3

Numbers aren't expressions, so the if condition inside the macro didn't apply. All the macro did was return n. But if you pass an expression, the code in the macro has the opportunity to inspect and/or process the expression's content before it is evaluated, using the .args field:

julia> @p 3 + 4 - 5 * 6 / 7 % 8
Any[:-,:(3 + 4),:(((5 * 6) / 7) % 8)]
2.7142857142857144

In this case, the if condition was triggered, and the arguments of the incoming expression were printed in unevaluated form. So you can see the arguments as an array of expressions after being parsed by Julia but before being evaluated. You can also see how the different precedence of arithmetic operators has been taken into account in the parsing operation. Notice how the top-level operators and subexpressions are quoted with a colon (:).

Also notice that the macro p returned the argument, which was then evaluated, hence the 2.7142857142857144. But it doesn't have to — it could return a quoted expression instead.

As an example, the built-in @time macro returns a quoted expression rather than using eval() to evaluate the expression inside the macro. The quoted expression returned by @time is evaluated in the calling context when the macro has done its work. Here's the definition:

macro time(ex)
    quote
        local t0 = time()
        local val = $(esc(ex))
        local t1 = time()
        println("elapsed time: ", t1-t0, " seconds")
        val
    end
end

Notice the $(esc(ex)) expression. This is the way that you 'escape' the code you want to time, which is in ex, so that it isn't evaluated in the macro, but left intact until the entire quoted expression is returned to the calling context and executed there. If this just said $ex, then the expression would be interpolated and evaluated immediately.

If you want to pass a multi-line expression to a macro, use the begin ... end form:

@p begin
    2 + 2 - 3
end
Any[:( # none, line 2:),:((2 + 2) - 3)]
1

(You can also call macros with parentheses similar to the way you do when calling functions, using the parentheses to enclose the arguments:

julia> @p(2 + 3 + 4 - 5)
Any[:-,:(2 + 3 + 4),5]
4

This would allow you to define macros that accepted more than one expression as arguments.)

eval() and @eval

[edit | edit source]

There's an eval() function, and an @eval macro. You might be wondering what's the difference between the two?

julia> ex = :(2 + 2)
:(2 + 2) 

julia> eval(ex)
4

julia> @eval ex
:(2 + 2)

The function version (eval()) expands the expression and evaluates it. The macro version doesn't expand the expression you supply to it automatically, but you can use the interpolation syntax to evaluate the expression and pass it to the macro.

julia> @eval $(ex)
4

In other words:

julia> @eval $(ex) == eval(ex)
true

Here's an example where you might want to create some variables using some automation. We'll create the first ten squares and ten cubes, first using eval():

for i in 1:10
   symbolname = Symbol("var_squares_$(i)")
   eval(quote $symbolname = $(i^2) end)
end

which creates a load of variables named var_squares_n, such as:

julia> var_squares_5
25

and then using @eval:

for i in 1:10
   symbolname = Symbol("var_cubes_$(i)")
   @eval $symbolname = $(i^3)
end

which similarly creates a load of variables named var_cubes_n, such as:

julia> var_cubes_5
125

Once you feel confident, you might prefer to write like this:

julia> [@eval $(Symbol("var_squares_$(i)")) = ($i^2) for i in 1:10]

Scope and context

[edit | edit source]

When you use macros, you have to keep an eye out for scoping issues. In the previous example, the $(esc(ex)) syntax was used to prevent the expression from being evaluated in the wrong context. Here's another contrived example to illustrate this point.

macro f(x)
    quote
        s = 4
        (s, $(esc(s)))
    end
end

This macro declares a variable s, and returns a quoted expression containing s and an escaped version of s.

Now, outside the macro, declare a symbol s:

julia> s = 0

Run the macro:

julia> @f 2
(4,0)

You can see that the macro returned different values for the symbol s: the first was the value inside the macro's context, 4, the second was an escaped version of s, that was evaluated in the calling context, where s has the value 0. In a sense, esc() has protected the value of s as it passes unharmed through the macro. For the more realistic @time example, it's important that the expression you want to time isn't modified in any way by the macro.

Expanding macros

[edit | edit source]

To see what the macro expands to just before it's finally executed, use the macroexpand() function. It expects a quoted expression containing one or more macro calls, which are then expanded into proper Julia code for you so that you can see what the macro would do when called.

julia> macroexpand(Main, quote @p 3 + 4 - 5 * 6 / 7 % 8 end)
Any[:-,:(3 + 4),:(((5 * 6) / 7) % 8)]
quote
   #= REPL[158]:1 =#
   (3 + 4) - ((5 * 6) / 7) % 8
end

(The #none, line 1: is a filename and line number reference that's more useful when used inside a source file than when you're using the REPL.)

Here's another example. This macro adds a dotimes construction to the language.

macro dotimes(n, body)
    quote
        for i = 1:$(esc(n))
            $(esc(body))
        end
    end
end

This is used as follows:

julia> @dotimes 3 println("hi there")
hi there
hi there
hi there

Or, less likely, like this:

julia> @dotimes 3 begin    
   for i in 4:6            
       println("i is $i")  
   end                     
end                        
i is 4
i is 5
i is 6
i is 4
i is 5
i is 6
i is 4
i is 5
i is 6

If you use macroexpand() on this, you can see what happens to the symbol names:

macroexpand(Main, # we're working in the Main module
    quote  
        @dotimes 3 begin
            for i in 4:6
                println("i is $i")
            end
        end
    end 
)

with the following output:

quote
    #= REPL[160]:3 =#
    begin
        #= REPL[159]:3 =#
        for #101#i = 1:3
            #= REPL[159]:4 =#
            begin
                #= REPL[160]:4 =#
                for i = 4:6
                    #= REPL[160]:5 =#
                    println("i is $(i)")
                end
            end
        end
    end
end

The i local to the macro itself has been renamed to #101#i, so as not to clash with the original i in the code we passed to it.

A more useful example: @until

[edit | edit source]

Here's how to define a macro that is more likely to be useful in your code.

Julia doesn't have an until condition ... do some stuff ... end statement. Perhaps you'd like to type something like this:

until x > 100
    println(x)
end

You'll be able to write your code using the new until macro like this:

until <condition>
    <block_of_stuff>
end

but, behind the scenes, the work will be done by actual code with the following structure:

while true
    <block_of_stuff>
    if <condition>
        break
    end
end

This forms the body of the new macro, and it will be enclosed in a quote ... end block, like this, so that it executes when evaluated, but not before:

quote
    while true
        <block_of_stuff>
        if <condition>
            break
        end
    end
end

So the nearly-finished macro code is like this:

macro until(<condition>, <block_of_stuff>)
    quote
        while true
            <block_of_stuff>
            if <condition>
                break
            end
        end
    end
end

All that remains to be done is to work out how to pass in our code for the <block_of_stuff> and the <condition> parts of the macro. Recall that $(esc(...)) allows code to pass through 'escaped' (i.e. unevaluated). We'll protect the condition and block code from being evaluated before the macro code runs.

The final macro definition is therefore:

macro until(condition, block)
    quote
        while true
            $(esc(block))
            if $(esc(condition))
                break
            end
        end
    end
end

The new macro is used like this:

julia> i = 0
0

julia> @until i == 10 begin   
           global i += 1               
           println(i)          
       end                      
1
2
3
4
5
6
7
8
9
10

or

julia> x = 5
5

julia> @until x < 1 (println(x); global x -= 1)
5
4
3
2
1

Under the hood

[edit | edit source]

If you want a more complete explanation of the compilation process than that provided here, visit the links shown in Further Reading, below.

Julia performs multiple 'passes' to transform your code to native assembly code. As described above, the first pass parses the Julia code and builds the 'surface-syntax' AST, suitable for manipulation by macros. A second pass lowers this high-level AST into an intermediate representation, which is used by type inference and code generation. In this intermediate AST format all macros have been expanded and all control flow has been converted to explicit branches and sequences of statements. At this stage the Julia compiler attempts to determine the types of all variables so that the most suitable method of a generic function (which can have many methods) is selected.

Further reading

[edit | edit source]