One of my goals for this book has been to teach you as little Julia as possible. When there were two ways to do something, I picked one and avoided mentioning the other. Or sometimes I put the second one into an exercise.
Now I want to go back for some of the good bits that got left behind. Julia provides a number of features that are not really necessary—you can write good code without them—but with them you can sometimes write code that’s more concise, readable, or efficient (and sometimes all three).
This chapter and the next discuss the things I have left out in the previous chapters:
Syntax supplements
Functions, types, and macros directly available in Base
Functions, types, and macros in the standard library
A function in Julia can be defined by a compact syntax:
julia>
f
(
x
,
y
)
=
x
+
y
f (generic function with 1 method)
We can define a function without specifying a name:
julia>
x
->
x
^
2
+
2
x
-
1
#1 (generic function with 1 method)
julia>
function
(
x
)
x
^
2
+
2
x
-
1
end
#3 (generic function with 1 method)
These are examples of anonymous functions. Anonymous functions are often used as arguments to another function:
julia>
using
Plots
julia>
plot
(
x
->
x
^
2
+
2
x
-
1
,
0
,
10
,
xlabel
=
"x"
,
ylabel
=
"y"
)
Figure 19-1 shows the output of the plotting command.
Function arguments can also be named:
julia>
function
myplot
(
x
,
y
;
style
=
"solid"
,
width
=
1
,
color
=
"black"
)
###
end
myplot (generic function with 1 method)
julia>
myplot
(
0
:
10
,
0
:
10
,
style
=
"dotted"
,
color
=
"blue"
)
Keyword arguments in a function are specified after a semicolon in the signature but can also be called with a comma.
A closure is a technique allowing a function to capture a variable defined outside the calling scope of the function:
julia>
foo
(
x
)
=
()
->
x
foo (generic function with 1 method)
julia>
bar
=
foo
(
1
)
#1 (generic function with 1 method)
julia>
bar
()
1
In this example, the function foo
returns an anonymous function that has access to the x
argument of the function foo
. bar
points to the anonymous function and returns the value of the argument of foo
.
A block is a way to group a number of statements. A block starts with the keyword begin
and ends with end
.
In Chapter 4, the @svg
macro was introduced:
?
Turtle
()
@svg
begin
forward
(
?
,
100
)
turn
(
?
,
-
90
)
forward
(
?
,
100
)
end
In this example the macro @svg
has a single argument, a block grouping three function calls.
A let
block is useful to create new bindings—i.e., locations that can refer to values:
julia>
x
,
y
,
z
=
-
1
,
-
1
,
-
1
;
julia>
let
x
=
1
,
z
@show
x
y
z
;
end
x = 1
y = -1
ERROR: UndefVarError: z not defined
julia>
@show
x
y
z
;
x = -1
y = -1
z = -1
In this example, the first @show
macro shows the local variable x
, the global variable y
, and the undefined local variable z
. As the second @show
macro shows, the global variables are untouched.
In “Reading and Writing” I showed you how to close a file when you’re done writing. This can be done automatically using a do
block:
julia>
data
=
"This here's the wattle,
the emblem of our land.
"
"This here's the wattle, the emblem of our land. "
julia>
open
(
"output.txt"
,
"w"
)
do
fout
write
(
fout
,
data
)
end
48
In this example fout
is the file stream used for output.
This is functionally equivalent to:
julia>
f
=
fout
->
begin
write
(
fout
,
data
)
end
#3 (generic function with 1 method)
julia>
open
(
f
,
"output.txt"
,
"w"
)
48
The anonymous function is used as the first argument of the function open
:
function
open
(
f
::
Function
,
args
...
)
io
=
open
(
args
...
)
try
f
(
io
)
finally
close
(
io
)
end
end
A do
block can “capture” variables from its enclosing scope. For example, the variable data
in the open ... do
example is captured from the outer scope.
In the previous chapters we used if-elseif
statements to make choices. Ternary operators and short-circuit evaluations are more compact ways to do the same. A task is an advanced control structure that directly modifies the flow of the program.
The ternary operator, ?:
, is an alternative to an if-elseif
statement used when you need to make a choice between single expression values:
julia>
a
=
150
150
julia>
a
%
2
==
0
?
println
(
"even"
)
:
println
(
"odd"
)
even
The expression before the ?
is a conditional expression. If the condition is true
, the expression before the :
is evaluated; otherwise, the expression after the :
is evaluated.
The operators &&
and ||
do a short-circuit evaluation: the next argument is only evaluated when it is needed to determine the final value.
For example, a recursive factorial routine could be defined like this:
function
fact
(
n
::
Integer
)
n
>=
0
||
error
(
"n must be non-negative"
)
n
==
0
&&
return
1
n
*
fact
(
n
-
1
)
end
A task is a control structure that can pass control cooperatively without returning. In Julia, a task can be implemented as a function having as its first argument a Channel
object. A Channel
is used to pass values from the function to the callee.
The Fibonacci sequence can be generated using a task:
function
fib
(
c
::
Channel
)
a
=
0
b
=
1
put!
(
c
,
a
)
while
true
put!
(
c
,
b
)
(
a
,
b
)
=
(
b
,
a
+
b
)
end
end
put!
stores values in a Channel
object and take!
reads values from it:
julia>
fib_gen
=
Channel
(
fib
);
julia>
take!
(
fib_gen
)
0
julia>
take!
(
fib_gen
)
1
julia>
take!
(
fib_gen
)
1
julia>
take!
(
fib_gen
)
2
julia>
take!
(
fib_gen
)
3
The constructor Channel
creates the task. The function fib
is suspended after each call to put!
and resumed after take!
. For performance reasons, several values of the sequence are buffered in the Channel
object during a resume/suspend cycle.
A Channel
object can also be used as an iterator:
julia>
for
val
in
Channel
(
fib
)
(
val
,
" "
)
val
>
20
&&
break
end
0 1 1 2 3 5 8 13 21
Structs are the only user-defined types we have defined. Julia provides some extensions (primitive types, parametric types, and type unions), giving more flexibility to the programmer.
A concrete type consisting of plain old bits is called a primitive type. Unlike most languages, Julia allows you to declare your own primitive types. The standard primitive types are defined in the same way:
primitive type
Float64
<:
AbstractFloat
64
end
primitive type
Bool
<:
Integer
8
end
primitive type
Char
<:
AbstractChar
32
end
primitive type
Int64
<:
Signed
64
end
The number in the statement specifies how many bits are required.
The following example creates a primitive type Byte
and a constructor:
julia>
primitive type
Byte
8
end
julia>
Byte
(
val
::
UInt8
)
=
reinterpret
(
Byte
,
val
)
Byte
julia>
b
=
Byte
(
0x01
)
Byte(0x01)
The function reinterpret
is used to store the bits of an unsigned integer with 8 bits (UInt8
) into the Byte
.
Julia’s type system is parametric, meaning that types can have parameters.
Type parameters are introduced after the name of the type, surrounded by curly braces:
struct
Point
{
T
<:
Real
}
x
::
T
y
::
T
end
This defines a new parametric type, Point{T<:Real}
, holding two “coordinates” of type T
, which can be any type having Real
as supertype:
julia>
Point
(
0.0
,
0.0
)
Point{Float64}(0.0, 0.0)
In addition to composite types, abstract types and primitive types can also have a type parameter.
A type union is an abstract parametric type that can act as any of its argument types:
julia>
IntOrString
=
Union
{
Int64
,
String
}
Union{Int64, String}
julia>
150
::
IntOrString
150
julia>
"Julia"
::
IntOrString
"Julia"
A type union in most computer languages is an internal construct for reasoning about types. Julia, however, exposes this feature to its users because efficient code can be generated when the type union has a small number of types. This feature gives the Julia programmer tremendous flexibility for controlling dispatch.
Methods can also be parametric, and objects can behave as functions.
Any arbitrary Julia object can be made “callable.” Such callable objects are sometimes called functors. For example:
struct
Polynomial
{
R
}
coeff
::
Vector
{
R
}
end
function
(
p
::
Polynomial
)(
x
)
val
=
p
.
coeff
[
end
]
for
coeff
in
p
.
coeff
[
end
-
1
:-
1
:
1
]
val
=
val
*
x
+
coeff
end
val
end
To evaluate the polynomial, we simply have to call it:
julia>
p
=
Polynomial
([
1
,
10
,
100
])
Polynomial{Int64}([1, 10, 100])
julia>
p
(
3
)
931
Parametric types can be explicitly or implicitly constructed:
julia>
Point
(
1
,
2
)
# implicit T
Point{Int64}(1, 2)
julia>
Point
{
Int64
}(
1
,
2
)
# explicit T
Point{Int64}(1, 2)
julia>
Point
(
1
,
2.5
)
# implicit T
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Default inner and outer constructors are generated for each T
:
struct
Point
{
T
<:
Real
}
x
::
T
y
::
T
Point
{
T
}(
x
,
y
)
where
{
T
<:
Real
}
=
new
(
x
,
y
)
end
Point
(
x
::
T
,
y
::
T
)
where
{
T
<:
Real
}
=
Point
{
T
}(
x
,
y
);
and both x
and y
have to be of the same type.
When x
and y
have a different type, the following outer constructor can be defined:
Point
(
x
::
Real
,
y
::
Real
)
=
Point
(
promote
(
x
,
y
)
...
);
The promote
function is detailed in “Promotion”.
Julia has a system for promoting arguments to a common type. This is not done automatically but can be easily extended.
A value can be converted from one type to another:
julia>
x
=
12
12
julia>
typeof
(
x
)
Int64
julia>
convert
(
UInt8
,
x
)
0x0c
julia>
typeof
(
ans
)
UInt8
We can also add our own convert
methods:
julia>
Base
.
convert
(
::
Type
{
Point
{
T
}},
x
::
Array
{
T
,
1
})
where
{
T
<:
Real
}
=
Point
(
x
...
)
julia>
convert
(
Point
{
Int64
},
[
1
,
2
])
Point{Int64}(1, 2)
Promotion is the conversion of values of mixed types to a single common type:
julia>
promote
(
1
,
2.5
,
3
)
(1.0, 2.5, 3.0)
Methods for the promote
function are normally not directly defined, but the auxiliary function promote_rule
is used to specify the rules for promotion:
promote_rule
(
::
Type
{
Float64
},
::
Type
{
Int32
})
=
Float64
Julia code can be represented as a data structure of the language itself. This allows a program to transform and generate its own code.
Every Julia program starts as a string:
julia>
prog
=
"1 + 2"
"1 + 2"
The next step is to parse each string into an object called an expression, represented by the Julia type Expr
:
julia>
ex
=
Meta
.
parse
(
prog
)
:(1 + 2)
julia>
typeof
(
ex
)
Expr
julia>
dump
(
ex
)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 2
The dump
function displays expression objects with annotations.
Expressions can be constructed directly by prefixing with :
inside parentheses or using a quote block:
julia>
ex
=
quote
1
+
2
end
;
Macros can include generated code in a program. A macro maps a tuple of Expr
objects directly to a compiled expression.
Here is a simple macro:
macro
containervariable
(
container
,
element
)
return
esc
(
:
(
$
(
Symbol
(
container
,
element
))
=
$
container
[
$
element
]))
end
Macros are called by prefixing their name with the at sign (@
). The macro call @containervariable
letters 1
is replaced by:
:
(
letters1
=
letters
[
1
])
@macroexpand @containervariable letters 1
returns this expression, which is extremely useful for debugging.
This example illustrates how a macro can access the name of its arguments, something a function can’t do. The return expression needs to be “escaped” with esc
because it has to be resolved in the macro call environment.
Why use macros?
Macros generate and include fragments of customized code during parse time, thus before the full program is run.
The macro @generated
creates specialized code for methods depending on the types of the arguments:
@generated
function
square
(
x
)
println
(
x
)
:
(
x
*
x
)
end
The body returns a quoted expression like a macro.
For the caller, the generated function behaves as a regular function:
julia>
x
=
square
(
2
);
# note: output is from println() statement in the body
Int64
julia>
x
# now we print x
4
julia>
y
=
square
(
"spam"
);
String
julia>
y
"spamspam"
Missing values can be represented via the missing
object, which is the singleton instance of the type Missing
.
Arrays can contain missing values:
julia>
a
=
[
1
,
missing
]
2-element Array{Union{Missing, Int64},1}:
1
missing
The element type of such an array is Union{Missing, T}
, with T
being the type of the non-missing values.
Reduction functions return missing
when called on arrays that contain missing values:
julia>
sum
(
a
)
missing
In this situation, use the skipmissing
function to skip missing values:
julia>
sum
(
skipmissing
([
1
,
missing
]))
1
A lot of code is written in C or Fortran. Reusing tested code is often better than writing your own version of an algorithm. Julia can call directly existing C or Fortran libraries using the ccall
syntax.
In “Databases” I introduced a Julia interface to the GDBM
library of database functions. The library is written in C. To close the database a function call to close(db)
has to be made:
Base
.
close
(
dbm
::
DBM
)
=
gdbm_close
(
dbm
.
handle
)
function
gdbm_close
(
handle
::
Ptr
{
Cvoid
})
ccall
((
:
gdbm_close
,
"libgdbm"
),
Cvoid
,
(
Ptr
{
Cvoid
},),
handle
)
end
A dbm
object has a field handle
of Ptr{Cvoid}
type. This field holds a C pointer that refers to the database. To close the database the C function gdbm_close
has to be called, having as its only argument the C pointer pointing to the database and no return value. Julia does this directly with the ccall
function having as arguments:
A tuple consisting of a symbol holding the name of the function we want to call, :gdbm_close
, and the shared library specified as a string, "libgdm"
The return type, Cvoid
A tuple of argument types, (Ptr{Cvoid},)
The argument values, handle
The complete mapping of the GDBM
library can be found as an example in the ThinkJulia
sources.
let
blockdo
blockA syntax construction used to define and call an anonymous function that looks like a normal code block.
A control flow operator taking three operands to specify a condition, an expression to be executed when the condition yields true
, and an expression to be executed when the condition yields false
.
Evaluation of a Boolean operator where the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression.
A control flow feature that allows computations to be suspended and resumed in a flexible manner.
A type that includes as objects all instances of any of its type parameters.
An object that has an associated method, so that it is callable.
A way to include generated code in the final body of a program.
Functions capable of generating specialized code depending on the types of the arguments.