Chapter 19. The Goodies: Syntax

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

Named Tuples

You can name the components of a tuple, creating a named tuple:

julia> x = (a=1, b=1+1)
(a = 1, b = 2)
julia> x.a
1

With named tuples, fields can be accessed by name using dot syntax (x.a).

Functions

A function in Julia can be defined by a compact syntax:

julia> f(x,y) = x + y
f (generic function with 1 method)

Anonymous Functions

We can define a function without specifying a name:

julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
           x^2 + 2x - 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 + 2x - 1, 0, 10, xlabel="x", ylabel="y")

Figure 19-1 shows the output of the plotting command.

thju 1901
Figure 19-1. Plot

Keyword Arguments

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.

Closures

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.

Blocks

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.

let Blocks

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.

do Blocks

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.

Control Flow

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.

Ternary Operator

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.

Short-Circuit Evaluation

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

Tasks (aka Coroutines)

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)
           print(val, " ")
           val > 20 && break
       end
0 1 1 2 3 5 8 13 21

Types

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.

Primitive Types

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.

Parametric Types

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.

Having concrete types for struct fields is absolutely recommended for performance reasons, so this is a good way to make Point both fast and flexible.

Type Unions

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

Methods can also be parametric, and objects can behave as functions.

Parametric Methods

Method definitions can also have type parameters qualifying their signature:

julia> isintpoint(p::Point{T}) where {T} = (T === Int64)
isintpoint (generic function with 1 method)
julia> p = Point(1, 2)
Point{Int64}(1, 2)
julia> isintpoint(p)
true

Function-like Objects

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

Constructors

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”.

Conversion and Promotion

Julia has a system for promoting arguments to a common type. This is not done automatically but can be easily extended.

Conversion

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

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

Metaprogramming

Julia code can be represented as a data structure of the language itself. This allows a program to transform and generate its own code.

Expressions

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;

eval

Julia can evaluate an expression object using eval:

julia> eval(ex)
3

Every module has its own eval function that evaluates expressions in its scope.

When you are using a lot of calls to the function eval, often this means that something is wrong. eval is considered “evil.”

Macros

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.

Generated Functions

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

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

Calling C and Fortran Code

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.

Glossary

named tuple

A tuple with named components.

anonymous function

A function defined without being given a name.

keyword arguments

Arguments identified by name instead of only by position.

closure

A function that captures variables from its defining scope.

block

A way to group a number of statements.

let block

A block allocating new variable bindings.

do block

A syntax construction used to define and call an anonymous function that looks like a normal code block.

ternary operator

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.

short-circuit evaluation

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.

task (aka coroutine)

A control flow feature that allows computations to be suspended and resumed in a flexible manner.

primitive type

A concrete type whose data consists of plain old bits.

parametric type

A type that is parameterized.

type union

A type that includes as objects all instances of any of its type parameters.

functor

An object that has an associated method, so that it is callable.

conversion

Changing a value from one type to another.

promotion

Converting values of mixed types to a single common type.

expression

A Julia type that holds a language construct.

macro

A way to include generated code in the final body of a program.

generated functions

Functions capable of generating specialized code depending on the types of the arguments.

missing values

Instances that represent data points with no value.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset