Book Image

Mastering Julia - Second Edition

By : Malcolm Sherrington
Book Image

Mastering Julia - Second Edition

By: Malcolm Sherrington

Overview of this book

Julia is a well-constructed programming language which was designed for fast execution speed by using just-in-time LLVM compilation techniques, thus eliminating the classic problem of performing analysis in one language and translating it for performance in a second. This book is a primer on Julia’s approach to a wide variety of topics such as scientific computing, statistics, machine learning, simulation, graphics, and distributed computing. Starting off with a refresher on installing and running Julia on different platforms, you’ll quickly get to grips with the core concepts and delve into a discussion on how to use Julia with various code editors and interactive development environments (IDEs). As you progress, you’ll see how data works through simple statistics and analytics and discover Julia's speed, its real strength, which makes it particularly useful in highly intensive computing tasks. You’ll also and observe how Julia can cooperate with external processes to enhance graphics and data visualization. Finally, you will explore metaprogramming and learn how it adds great power to the language and establish networking and distributed computing with Julia. By the end of this book, you’ll be confident in using Julia as part of your existing skill set.
Table of Contents (14 chapters)

Integers, bits, bytes, and Booleans

While Julia is usually dynamically typed—that is, in common with most interpreted languages, it does not require the type to be specified when a variable is declared; rather, it infers it from the form of the declaration. However, it also can be considered as a strongly typed language and, in this case, allows the programmer to specify a variable’s type precisely.

A variable in Julia is any combination of upper- or lowercase letters, digits, and the underscore (_) and exclamation (!) characters. It must start with a letter or an underscore.

Conventionally, variable names consist of lowercase letters with long names separated by underscores rather than using camel case.

To determine a variable type, we can use the typeof() function, as follows:

julia> x = 2;   typeof(x) # => gives Int
julia> x = 2.0; typeof(x) # => gives Float

Notice that the type (see the preceding code) starts with a capital letter and ends with a number, which indicates the number of bit length of the variable. The bit length defaults to the word length of the operating system, and this can be determined by examining the WORD_SIZE built-in constant, as follows:

julia> WORD_SIZE # => 64 (on my MacPro computer)

In this section, we will be dealing first with integer and Boolean types.

Integers

An integer type can be any of Int8, Int16, Int32, Int64, and Int128, so the maximum integer can occupy 16 bytes of storage and be anywhere within the range of –2127 to (+2127 - 1).

If we need more precision than this, Julia core implements the BigInt type:

julia> x = BigInt(2^32)
6277101735386680763835789423207666416102355444464034512896

As well as the integer type, Julia provides the unsigned integer type, UInt; again, UInt ranges from 8 to 128 bytes, so the maximum UInt value is (2128 - 1).

We can use the typemax() and typemax() functions to output the ranges of the Int and UInt types, like so:

julia> for T =
  Any[Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UInt64,UInt128]
  println("$(lpad(T,7)): [$(typemin(T)),$(typemax(T))]")
end
Int8: [-128,127]
Int16: [-32768,32767]
Int32: [-2147483648,2147483647]
Int64: [-9223372036854775808,9223372036854775807]
Int128: [-170141183460469231731687303715884105728,
          170141183460469231731687303715884105727]
UInt8: [0, 255]
UInt16: [0, 65535]
UInt32: [0, 4294967295]
UInt64: [0, 18446744073709551615]
UInt128: [0, 340282366920938463463374607431768211455]

Particularly, notice the use of the form of the for statement, which we will discuss when we deal with arrays and matrices later in this chapter.

Suppose we type the following:

julia> x = 2^32; x*x # => the answer 0

The reason for the answer being 0 is that the integer “wraps” around, so squaring 232 gives 0, not 264, since my WORD_SIZE value is 64:

julia> x = int128(2^32); x*x
# => the answer we would expect 18446744073709551616

We can use the typeof() function on a type such as Int64 in order to see what its parent type is:

# So typeof(Int64) gives DataType and typeof(UInt128) also gives DataType.

A definition of DataType is hinted at in the boot.jl core file; I say hinted at because the actual definition is implemented in C, and the Julia equivalent is commented out.

Definitions of the integer types can also be found in boot.jl, this time not commented out.

In the next chapter, we will discuss the Julia type system in some detail. Here, it is worth noting that we distinguish between two kinds of data types: abstract and primitive (concrete).

The general syntax for declaring an abstract type is shown here:

abstract type «name» end
abstract type «name» <: «supertype» end

Typically, this is how it would look:

abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end

Here, the <: operator corresponds to a subclass of the parent.

Let’s suppose we type the following:

julia> x = 7; y = 5; x/y # => this gives 1.4

Here, the division of two integers produces a real result. In interactive mode, we can use the ans symbol to correspond to the last answer—that is, typeof(ans) gives Float.

To get the integer divisor, we use the div(x,y) function, which gives 1, as expected, and typeof(ans) is Int64. The remainder is obtained either by rem(x,y) or by using the % operator.

Julia has one curious operator—the backslash. Syntactically, x\y is equivalent to y/x. So, with x and y, as before, x\y gives 0.71428 (to 5 decimal places).

Primitive types

A primitive type is a concrete type whose data consists of a series of bits. Examples of primitive types are the (well-known) integers and floating-point values that we have met previously.

The general syntax for declaring a primitive type is like that of an abstract type but with the addition of the number of bits to be allocated:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

Since Julia is written (mostly) in Julia, a corollary is that Julia lets you declare your own primitive types, rather than providing only a fixed set of built-in ones.

That is, all the standard primitive types are defined in Base itself, as follows:

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
primitive type Int16 <: Signed 16 end
primitive type UInt16 <: Unsigned 16 end
primitive type Int32 <: Signed 32 end
primitive type UInt32 <: Unsigned 32 end
primitive type Int64 <: Signed 64 end
primitive type UInt64 <: Unsigned 64 end
primitive type Int128 <: Signed 128 end
primitive type UInt128 <: Unsigned 128 end

Note that only sizes that are multiples of 8 bits are supported, so Boolean values, although they really need just a single bit, cannot be declared to be any smaller than 8 bits. Figure 2.1 demonstrates a portion of the Julia hierarchical structure as it applies to simple numerical types:

Figure 2.1 – Tree structure for numerical types

Figure 2.1 – Tree structure for numerical types

Those above the line are abstract types beginning with Any and cascading down through Number and Real before splitting into Integer and AbstractFloat types, eventually reaching the primitive types defined in Julia Base, which are shown below the line.

Primitives can’t be subclassed further, hence terminating the various branches of the tree.

Logical and arithmetic operators

As well as decimal arguments it is possible to assign binary, octal, and hexadecimal ones using the 0b, 0o, and 0x prefixes.

So, x = 0b110101 creates the hexadecimal number 0x35 (that is, decimal 53), and typeof(ans) is UInt8 since 53 will “fit” into a single byte.

For larger values, the type is correspondingly higher—that is, x = 0b1000010110101 gives x = 0x10b5, and typeof(ans) is UInt.

When operating on bits, Julia provides ~ (not), | (or), & (and), and $ (xor):

julia> x = 0xbb31;  y = 0xaa5f;
julia> x$y
0x116e

Also, we can perform arithmetic shifts using the (LEFT) and (RIGHT) operators.

Note

Because x is of the UInt16 type, the shift operator retains that size, so x = 0xbb31; x<<8. This gives 0x3100 (the top two nibbles being discarded), and typeof(ans) is UInt.

Booleans

Julia has the Bool logical type. Dynamically, a variable is assigned a Bool type by equating it to the true or false constant (both lowercase), or alternatively, to a logical expression such as the following:

julia> p = (2 < 3) # => true
julia> typeof(p)   # => Bool

Many languages treat 0, empty strings, and NULL instances as representing false and anything else as true. This is NOT the case in Julia, however; there are cases where a Bool value may be promoted to an integer, in which case true corresponds to unity.

That is, an expression such as x + p (where x is of the Int type and p of the Bool type) will output the following:

julia> x = 0xbb31; p = (2 < 3);
julia> x + p
0xbb32
julia> typeof(ans) # => UInt16

Big integers

Let’s consider the factorial function defined by the usual recursive relation:

# n! = n*(n-1)! for integer values of n (> 0)
function fac(n::Integer)
  @assert n > 0
  (n == 1) ? 1 : n*fac(n-1)
end

Note that since normally, integers in Julia overflow (a feature of Low-Level Virtual Machine (LLVM), the preceding definition can lead to problems with large values of n, as illustrated here:

julia> using Printf
       for i = 20:30
         @printf "%3d : %d\n" i fac(i)
       end
 20 : 2432902008176640000
 21 : -4249290049419214848
 22 : -1250660718674968576
 23 : 8128291617894825984
 24 : -7835185981329244160
 25 : 7034535277573963776
 26 : -1569523520172457984
 27 : -5483646897237262336
 28 : -5968160532966932480
 29 : -7055958792655077376
 30 : -8764578968847253504
# Since a BigInt <: Integer,
# if we pass a BigInt the routine returns the correct value
julia> fac(big(30))
265252859812191058636308480000000
# See can check this since integer values: Γ(n+1) === n!
julia> gamma(31)
2.6525285981219107e32

The big() function uses string arithmetic, so it does not have a limit imposed by the WORD_SIZE constant but is clearly much slower than using conventional arithmetic. The big() function is not only restricted to integers but can be applied to reals (floats) or even complex numbers.

We can introduce a new function, |>, which applies a function to its preceding argument, providing a chaining functional style:

julia> 30 |> big |> fac
265252859812191058636308480000000

Here, the 30 argument is piped to the factorial function but after first being converted into a BigInt type.

Also, note that the syntax is equivalent to fac(big(30)).

For now, we are going to leave our discussion on functions and begin to study in depth how arrays are constructed and used in Julia.