Chapter 15. Structs and Objects

At this point you know how to use functions to organize code and built-in types to organize data. The next step is to learn how to build your own types to organize both code and data. This is a big topic; it will take a few chapters to get there.

Composite Types

We have used many of Julia’s built-in types; now we are going to define a new type. As an example, we will create a type called Point that represents a point in two-dimensional space.

In mathematical notation, points are often written in parentheses with a comma separating the coordinates. For example, 0,0 represents the origin, and x,y represents the point x units to the right and y units up from the origin.

There are several ways we might represent points in Julia:

  • We could store the coordinates separately in two variables, x and y.

  • We could store the coordinates as elements in an array or tuple.

  • We could create a new type to represent points as objects.

Creating a new type is more complicated than the other options, but it has advantages that will be apparent soon.

A programmer-defined composite type is also called a struct. The struct definition for a point looks like this:

struct Point
    x
    y
end

The header indicates that the new struct is called Point. The body defines the attributes or fields of the struct. The Point struct has two fields: x and y.

A struct is like a factory for creating objects. To create a point, you call Point as if it were a function having as arguments the values of the fields. When Point is used as a function, it is called a constructor:

julia> p = Point(3.0, 4.0)
Point(3.0, 4.0)

The return value is a reference to a Point object, which we assign to p.

Creating a new object is called instantiation, and the object is an instance of the type.

When you print an instance, Julia tells you what type it belongs to and what the values of the attributes are.

Every object is an instance of some type, so “object” and “instance” are interchangeable. But in this chapter I use “instance” to indicate that I am talking about a programmer-defined type.

A state diagram that shows an object and its fields is called an object diagram; see Figure 15-1.

thju 1501
Figure 15-1. Object diagram

Structs Are Immutable

You can get the values of the fields using . notation:

julia> x = p.x
3.0
julia> p.y
4.0

The expression p.x means, “Go to the object p refers to and get the value of x.” In the example, we assign that value to a variable named x. There is no conflict between the variable x and the field x.

You can use dot notation as part of any expression. For example:

julia> distance = sqrt(p.x^2 + p.y^2)
5.0

Structs are, however, by default immutable; after construction the fields cannot change value:

julia> p.y = 1.0
ERROR: setfield! immutable struct of type Point cannot be changed

This may seem odd at first, but it has several advantages:

  • It can be more efficient.

  • It is not possible to violate the invariants provided by the type’s constructors (see “Constructors”).

  • Code using immutable objects can be easier to reason about.

Mutable Structs

Where required, mutable composite types can be declared with the keyword mutable struct. Here is the definition of a mutable point:

mutable struct MPoint
    x
    y
end

You can assign values to an instance of a mutable struct using dot notation:

julia> blank = MPoint(0.0, 0.0)
MPoint(0.0, 0.0)
julia> blank.x = 3.0
3.0
julia> blank.y = 4.0
4.0

Rectangles

Sometimes it is obvious what the fields of an object should be, but other times you have to make decisions. For example, imagine you are designing a type to represent rectangles. What fields would you use to specify the location and size of a rectangle? You can ignore angle; to keep things simple, assume that the rectangle is either vertical or horizontal.

There are at least two possibilities:

  • You could specify one corner of the rectangle (or the center), the width, and the height.

  • You could specify two opposing corners.

At this point it is hard to say whether one is better than the other, so we’ll implement the first one, just as an example:

"""
Represents a rectangle.

fields: width, height, corner
"""
struct Rectangle
    width
    height
    corner
end

The docstring lists the fields: width and height are numbers; corner is a Point object that specifies the lower-left corner.

To represent a rectangle, you have to instantiate a Rectangle object:

julia> origin = MPoint(0.0, 0.0)
MPoint(0.0, 0.0)
julia> box = Rectangle(100.0, 200.0, origin)
Rectangle(100.0, 200.0, MPoint(0.0, 0.0))

Figure 15-2 shows the state of this object. An object that is a field of another object is embedded. Because the corner attribute refers to a mutable object, the latter is drawn outside the Rectangle object.

thju 1502
Figure 15-2. Object diagram

Instances as Arguments

You can pass an instance as an argument in the usual way. For example:

function printpoint(p)
    println("($(p.x), $(p.y))")
end

printpoint takes a Point as an argument and displays it in mathematical notation. To invoke it, you can pass p as an argument:

julia> printpoint(blank)
(3.0, 4.0)

Exercise 15-1

Write a function called distancebetweenpoints that takes two points as arguments and returns the distance between them.

If a mutable struct object is passed to a function as an argument, the function can modify the fields of the object. For example, movepoint! takes a mutable Point object and two numbers, dx and dy, and adds the numbers to, respectively, the x and the y attribute of the Point:

function movepoint!(p, dx, dy)
    p.x += dx
    p.y += dy
    nothing
end

Here is an example that demonstrates the effect:

julia> origin = MPoint(0.0, 0.0)
MPoint(0.0, 0.0)
julia> movepoint!(origin, 1.0, 2.0)

julia> origin
MPoint(1.0, 2.0)

Inside the function, p is an alias for origin, so when the function modifies p, origin changes.

Passing an immutable Point object to movepoint! causes an error:

julia> movepoint!(p, 1.0, 2.0)
ERROR: setfield! immutable struct of type Point cannot be changed

You can, however, modify the value of a mutable attribute of an immutable object. For example, moverectangle! has as arguments a Rectangle object and two numbers, dx and dy, and uses movepoint! to move the corner of the rectangle:

function moverectangle!(rect, dx, dy)
  movepoint!(rect.corner, dx, dy)
end

Now p in movepoint! is an alias for rect.corner, so when p is modified, rect.corner changes also:

julia> box
Rectangle(100.0, 200.0, MPoint(0.0, 0.0))
julia> moverectangle!(box, 1.0, 2.0)

julia> box
Rectangle(100.0, 200.0, MPoint(1.0, 2.0))

You cannot reassign a mutable attribute of an immutable object:

julia> box.corner = MPoint(1.0, 2.0)
ERROR: setfield! immutable struct of type Rectangle
  cannot be changed

Instances as Return Values

Functions can return instances. For example, findcenter takes a Rectangle as an argument and returns a Point that contains the coordinates of the center of the rectangle:

function findcenter(rect)
    Point(rect.corner.x + rect.width / 2, rect.corner.y + rect.height / 2)
end

The expression rect.corner.x means, “Go to the object rect refers to and select the field named corner; then go to that object and select the field named x.”

Here is an example that passes box as an argument and assigns the resulting Point to center:

julia> center = findcenter(box)
Point(51.0, 102.0)

Copying

Aliasing can make a program difficult to read because changes in one place might have unexpected effects in another place. It is hard to keep track of all the variables that might refer to a given object.

Copying an object is often an alternative to aliasing. Julia provides a function called deepcopy that performs a deep copy and can duplicate any object, including the contents of any embedded objects:

julia> p1 = MPoint(3.0, 4.0)
MPoint(3.0, 4.0)
julia> p2 = deepcopy(p1)
MPoint(3.0, 4.0)
julia> p1  p2
false
julia> p1 == p2
false

The operator indicates that p1 and p2 are not the same object, which is what we expected. But you might have expected == to yield true because these points contain the same data. In that case, you will be disappointed to learn that for mutable objects, the default behavior of the == operator is the same as the === operator; it checks object identity, not object equivalence (see “Objects and Values”). That’s because for mutable composite types, Julia doesn’t know what should be considered equivalent—at least, not yet.

Exercise 15-2

Create a Point instance, make a copy of it, and check the equivalence and the egality of the two objects. The result may surprise you, but it explains why aliasing is a nonissue for an immutable object.

Debugging

When you start working with objects, you are likely to encounter some new exceptions. If you try to access a field that doesn’t exist, you get:

julia> p = Point(3.0, 4.0)
Point(3.0, 4.0)
julia> p.z = 1.0
ERROR: type Point has no field z
Stacktrace:
 [1] setproperty!(::Point, ::Symbol, ::Float64) at ./sysimg.jl:19
 [2] top-level scope at none:0

If you are not sure what type an object is, you can ask:

julia> typeof(p)
Point

You can also use isa to check whether an object is an instance of a type:

julia> p isa Point
true

If you are not sure whether an object has a particular attribute, you can use the built-in function fieldnames:

julia> fieldnames(Point)
(:x, :y)

or the function isdefined:

julia> isdefined(p, :x)
true
julia> isdefined(p, :z)
false

The first argument can be any object; the second argument is a symbol, :, followed by the name of the field.

Glossary

struct

A user-defined type consisting of a collection of named fields. Also called a composite type.

attribute

One of the named values associated with an object. Also called a field.

constructor

A function with the same name as a type that creates instances of the type.

instantiate

To create a new object.

instance

An object that belongs to a certain type.

object diagram

A diagram that shows objects, their fields, and the values of the fields.

embedded object

An object that is stored as a field of another object.

deep copy

To copy the contents of an object as well as any embedded objects, and any objects embedded in them, and so on; implemented by the deepcopy function.

Exercises

Exercise 15-3

  1. Write a definition for a type named Circle with fields center and radius, where center is a Point object and radius is a number.

  2. Instantiate a Circle object that represents a circle with its center at 150,100 and radius 75.

  3. Write a function named pointincircle that takes a Circle object and a Point object and returns true if the point lies in or on the boundary of the circle.

  4. Write a function named rectincircle that takes a Circle object and a Rectangle object and returns true if the rectangle lies entirely in or on the boundary of the circle.

  5. Write a function named rectcircleoverlap that takes a Circle object and a Rectangle object and returns true if any of the corners of the rectangle fall inside the circle. Or, as a more challenging version, return true if any part of the rectangle falls inside the circle.

Exercise 15-4

  1. Write a function called drawrect that takes a Turtle object and a Rectangle object and uses the turtle to draw the rectangle. See Chapter 4 for examples using Turtle objects.

  2. Write a function called drawcircle that takes a Turtle object and a Circle object and draws the circle.

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

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