Introducing Julia/print
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.
External links
[edit | edit source]- The Julia manual – the official rulebook: current and in-development documentation is available here.
- The JuliaLang Discourse - the main Julia programming language discussion forum
- JuliaHub – a Julia Computing service that includes search of all registered open source package documentation, code search, and navigation by tags/keywords. It is powered by Julia Team.
- JuliaPackages – see what packages are popular and/or trending, navigate by package categories.
- Julia.jl – a manually curated taxonomy of Julia packages
- A Month of Julia – 38 blog posts about using Julia.
Getting Started
[edit | edit source]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]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 filepathname
for editing
@edit rand()
– launch the default editor and open the file containing the definition of the built-in functionrand()
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 objectx
on the screen
names(x)
– gets an array of the names exported by the modulex
fieldnames(typeof(x))
– gets an array of the data fields that belong to a symbol of typex
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+b
goes back one word, while Alt+f
goes 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)
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]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 Vectorappend!()
push a second collection at the back of the first oneprepend!()
insert elements at the beginning of the first Vectorempty!(a)
remove all elementsunique(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]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.
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
, andFloat64
- more advanced numeric types:
BigFloat
,BigInt
- Boolean and character types:
Bool
andChar
- 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.
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 struct
keyword, 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]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]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
Map
[edit | edit source]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]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
[edit | edit source]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)]
Sets
[edit | edit source]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]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 expressionv" "
indicates a version stringb" "
indicates a byte literalraw" "
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 expressionsmatch()
returns the first match or nothingeachmatch()
returns an iterator that lets you search through all matchessplit()
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 stringsizeof(str)
length/sizestartswith(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 uppercaselowercase(str)
return a copy of str converted to lowercasetitlecase(str)
return copy of str with the first character of each word converted to uppercaseuppercasefirst(str)
return copy of str with first character converted to uppercaselowercasefirst(str)
return copy of str with first character converted to lowercasechop(str)
return a copy with the last character removedchomp(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]Reading from files
[edit | edit source]The standard approach for getting information from a text file is using the open()
, read()
, and close()
functions.
Open
[edit | edit source]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]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.
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.Day
s or Dates.Millisecond
s 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]DateTime
s 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]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 )
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)
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))
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)
Metaprogramming
[edit | edit source]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]- https://docs.julialang.org/en/v1/devdocs/reflection/ more about Abstract Syntax Trees in the official Julia documentation
- http://blog.leahhanson.us/post/julia/julia-introspects.html Julia Introspects, Leah Hanson's useful article from 2013