Programmer-defined 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 a list 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. A `struct` definition looks like this:

``````

In [1]:

struct Point
x :: Float64
y :: Float64
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. The fields can optionally be annotated with types using the `::` operator. Fields with no type annotation default to `Any`, and can accordingly hold any type of value.

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 attributes. When `Point` is used as a function, it is called a constructor.

``````

In [2]:

origine = Point(0.0, 0.0)
println(origine)
typeof(origine)

``````
``````

Point(0.0, 0.0)

Out[2]:

Point

``````

The return value is a `Point` object, which we assign to `origine`.

When you print an object, Julia tells you by default what type it belongs to and what the value of its fields are.

A state diagram that shows an object and its attributes is called an object diagram:

``````

In [1]:

using TikzPictures
TikzPicture(L"""
\node(origine) [draw, fill=lightgray, minimum width=2.5cm, minimum height=1cm]{};
\node(norigine)[left of = origine, xshift=-1.5cm]{origine};
\node(Point)[above of = origine, xshift=-0.7cm, , yshift=-0.2cm]{Point};
\draw[-latex] (norigine) -- (origine);
\node(nx) at(-1,0.25) {x};
\node(x) at(0.5,0.25) {0.0};
\draw[-latex](nx)--(x);
\node(ny) at(-1,-0.25) {y};
\node(y) at(0.5,-0.25) {0.0};
\draw[-latex](ny)--(y);
"""; options="very thick, scale=2, transform shape", preamble="""
\\usepackage{newtxmath}
\\renewcommand{\\familydefault}{\\sfdefault}
\\usepackage{cancel}
""")

``````
``````

Out[1]:

``````

Structs are immutable

To get the values of the fields of a struct, `.` notation is used:

``````

In [3]:

x = origine.x

``````
``````

Out[3]:

0.0

``````

The expression `origine.x` means, “Go to the object `origine` 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 attribute `x`.

Structs are however immutable, after construction the fields can not change value:

``````

In [4]:

origine.y = 1.0

``````
``````

type Point is immutable

``````

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.

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

An immutable composite object might contain mutable objects, such as arrays, as fields. Those contained objects will remain mutable; only the fields of the immutable object itself cannot be changed to point to different objects.

``````

In [5]:

struct APoint
v :: Array{Float64, 1}
end

``````

The struct `APoint` has one field that has type `Array{Float64, 1}`, i.e. an array of 1 dimension holding floating point values.

``````

In [6]:

a_point = APoint([0.0, 0.0])

``````
``````

Out[6]:

APoint([0.0, 0.0])

``````

`a_point` is an immutable object but we can modify the values stored in `a_point.v`:

``````

In [7]:

a_point.v[2] = 1.0
a_point

``````
``````

Out[7]:

APoint([0.0, 1.0])

``````

But we cannot reassign the field `v`:

``````

In [8]:

a_point.v = [1.0, 1.0]

``````
``````

type APoint is immutable

``````

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 class to represent rectangles. What attributes 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 either is better than the other, so we’ll implement the first one, just as an example.

Where required, mutable composite objects can be declared with the keyword `mutable struct`. Here is the definition of a mutable `Rectangle`:

``````

In [11]:

mutable struct Rectangle
width :: Number
height :: Number
corner :: Point
end

``````

`width` and `height` are numbers; `corner` is a `Point` object that specifies the lower-left corner. We can create a `Box` object:

``````

In [12]:

box = Rectangle(100, 200, Point(0.0, 0.0))

``````
``````

Out[12]:

Rectangle(100, 200, Point(0.0, 0.0))

``````

We have embedded a `Point` type inside a `Rectangle` type.

``````

In [2]:

using TikzPictures
TikzPicture(L"""
\node(box)[draw, fill=lightgray, minimum width=3cm, minimum height=1.5cm]{};
\node(nbox)[left of = box, xshift=-1.5cm]{box};
\node(Box)[above of = box, xshift=-1.1cm]{Box};
\draw[-latex] (nbox) -- (box);
\node(nw) at(-0.9,0.5) {width};
\node(w) at(0.75,0.5) {100.0};
\draw[-latex](nw)--(w);
\node(nh) at(-0.9,0) {height};
\node(h) at(0.75,0) {200.0};
\draw[-latex](nh)--(h);
\node(nc) at(-0.9,-0.5) {corner};
\node(origine) [draw, fill=lightgray, minimum width=2.5cm, minimum height=1cm] at (3.5, -0.5){};
\node(Point)[above of = origine, xshift=-0.7cm, , yshift=-0.2cm]{Origine};
\draw[-latex] (nc) -- (origine);
\node(nx) at(2.5,-0.25) {x};
\node(x) at(4,-.25) {0.0};
\draw[-latex](nx)--(x);
\node(ny) at(2.5,-0.75) {y};
\node(y) at(4,-.75) {0.0};
\draw[-latex](ny)--(y);
\node(nh) at(-0.9,0) {height};
\node(h) at(0.75,0) {200.0};
\draw[-latex](nh)--(h);
"""; options="very thick, scale=2, transform shape", preamble="""
\\usepackage{newtxmath}
\\renewcommand{\\familydefault}{\\sfdefault}
\\usepackage{cancel}
""")

``````
``````

Out[2]:

``````

To find the x-coordinate of the left corner fo the `box`, we can use two times the `.` notation:

``````

In [11]:

box.corner.x

``````
``````

Out[11]:

0.0

``````

Objects as Function arguments

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

``````

In [9]:

function printPoint(p)
println("(\$(p.x), \$(p.y))")
end
printPoint(origine)

``````
``````

(0.0, 0.0)

``````

`print_point` takes a point as an argument and displays it in mathematical notation.

Inside the function, `p` is an alias for `origine`.

You can change the state of an mutable object by making an assignment to one of its fields. For example, to change the size of a rectangle without changing its position, you can modify the values of `width` and `height`:

``````

In [13]:

box.width = box.width + 50
box.height = box.height + 100
box

``````
``````

Out[13]:

Rectangle(150, 300, Point(0.0, 0.0))

``````

You can also write functions that modify mutable objects. For example, `grow_rectangle` takes a `Rectangle` object and two numbers, dwidth and dheight, and adds the numbers to the width and height of the rectangle:

``````

In [14]:

function grow_rectangle(rect::Rectangle, dwidth, dheight)
rect.width += dwidth
rect.height += dheight
end

``````
``````

Out[14]:

grow_rectangle (generic function with 1 method)

``````

Here is an example that demonstrates the effect:

``````

In [15]:

grow_rectangle(box, 50, 100)
box

``````
``````

Out[15]:

Rectangle(200, 400, Point(0.0, 0.0))

``````

Inside the function, `rect` is an alias for `box`, so when the function modifies `rect`, `box` changes.

Objects as return values

Functions can return objects. For example, `find_center` takes a `Rectangle` as an argument and returns a `Point` that contains the coordinates of the center of the `Rectangle`:

``````

In [16]:

function find_center(rect::Rectangle)
xc = rect.corner.x + rect.width/2
yc = rect.corner.y + rect.height/2
Point(xc, yc)
end

``````
``````

Out[16]:

find_center (generic function with 1 method)

``````

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

``````

In [17]:

center = find_center(box)

``````
``````

Out[17]:

Point(100.0, 200.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 can duplicate any object:

``````

In [20]:

box2 = deepcopy(box)
box2 === box

``````
``````

Out[20]:

false

``````
``````

In [19]:

box2 == box

``````
``````

Out[19]:

false

``````

The is 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. That’s because for mutable composite types, Julia doesn’t know what should be considered equivalent. At least, not yet.

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:

``````

In [20]:

p = Point(0.0, 0.0)
p.z = 1.0

``````
``````

type Point has no field z

Stacktrace:

``````

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

``````

In [21]:

typeof(p)

``````
``````

Out[21]:

Point

``````

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

``````

In [23]:

fieldnames(p)

``````
``````

Out[23]:

2-element Array{Symbol,1}:
:x
:y

``````
``````

In [21]:

using Luxor
🐢 = Turtle()
fieldnames(🐢)

``````
``````

WARNING: using Luxor.Point in module Main conflicts with an existing identifier.
WARNING: using Luxor.box in module Main conflicts with an existing identifier.
WARNING: using Luxor.Rectangle in module Main conflicts with an existing identifier.

Out[21]:

5-element Array{Symbol,1}:
:xpos
:ypos
:pendown
:orientation
:pencolor

``````