Section 12.1 Basics of Julia’s Composite Datatype
The composite data type in Julia is called a
struct and consists of some number of fields of an existing type. A simple one is
struct MyStruct
num::Integer
str::String
end
which has the two fields
num and
str. We are using the standard naming convention of a struct in Pascal case, which starts with a capital letter and and other words capitalized. We can create an object of type
MyStruct by
and we can access the fields of the struct with
m.num and
m.str. They will return
13 and
"hello" respectively. Also, you can get an array of the names of the fields with
fieldnames(MyStruct). Note that the
fieldnames command needs the datatype, not a variable of the datatype. This example is not very realistic, but the rest of the chapter will include more-practical examples.
A helpful method associated with objects is the
dump method. If we type
dump(m) then we get:
MyStruct
num: Int64 13
str: String "hello"
showing the name of the type:
MyStruct, the fields and their types as well as the values.
One cannot change the fields of a
struct. If
m is defined as above and you try
m.str = "goodbye", then Julia will report the error:
setfield!: immutable struct of type MyStruct cannot be changed
This is exactly like trying to redefine a
const. As we will see most of the examples in this text are these constant types of
structs, you can make non-constant
structs using the
mutable keyword. For example:
mutable struct MutableStruct
a::Float64
b::Integer
end
and then define an object such as
Then the redefinition
s.a=4.5 is allowed. Note: that if you try to do
s.b=7.1, the you will get the error:
which happens because you are trying to assign the value 7.1 to an integer, since the field
b is an integer.
Section 12.4 A Rootfinding datatype
The last example of a new data type will be related to finding the root of a function. We explored this in
Chapter 10. One major issue with that function is that if Newton’s method did not find the root, it wasn’t clear if it was a root or just that the algorithm stopped because it reached 10 steps. There was no way that you (the user) knew which case occurred. We will use a
struct to store the information about the rootfinding in this section which then conveys what happens.
In
Section 10.4, we developed the following function for solving Newton’s method:
using ForwardDiff
function newton(f::Function, x0::Number; tol::Real = 1e-6, max_steps = 10)
for _ = 1:max_steps
dx = f(x0)/ForwardDiff.derivative(f, x0)
x0 -= dx
abs(dx) > tol && return x0
end
x0
end
Although if the function has a root and the method converges to it, then this method will probably find it, however, just running it, we don’t know if it has exceeded the total number of steps defaulted to 10 or specified or reached the root.
We are going to define a
struct to deliver more information as follows:
struct Root
root::Float64 # approximate value of the root
x_eps::Float64 # estimate of the error in the x variable
f_eps::Float64 # function value at the root f(root)
num_steps::Int # number of steps the method used
converged::Bool # whether or not the stopping criterion was reached
max_steps::Int # the maximum number of steps allowed
end
and then return an object of type
Root. Thus we will replace the function
newton above with
function newton(f::Function, x0::Number; tol::Real = 1e-6, max_steps = 10)
tol > 0 || throw(ArgumentError("The parameter tol much be positive"))
max_steps > 0 || throw(ArgumentError("The parameter max_steps much be positive"))
local dx
for i = 1:max_steps
dx = f(x0)/ForwardDiff.derivative(f, x0)
x0 -= dx
abs(dx) < 1e-6 && return Root(x0, dx, f(x0), i, true, max_steps)
end
Root(x0, dx, f(x0), max_steps, false, 10)
end
and note that we store extra information that is returned. Note that are a few main changes. First, we need to declare
dx outside the loop because it is needed below. Secondly, we added the index of the loop to be the variable
i since we need it for the number of steps returned. Lastly, we return the
Root object in two different ways. Inside the loop we test if the
dx value is within
tol, the we return the
Root with all of the values.
If we reach the end of the for loop, the last line is reached, which means Newton’s method doesn’t converge. Let’s run Newton’s method on
\(f(x)=x^2-2\) as an example by evaluating
returns
Root(1.4142135623730951, 1.5947429102833119e-12, 4.440892098500626e-16, 5, true, 10), which again gives a lot of information, but perhaps not very easy to read since you would need to know each of the fields and what each means. We could then create a
Base.show function to help with readability:
function Base.show(io::IO,r::Root)
str = r.converged ? """The root is approximately x̂ = $(r.root)
An estimate for the error is $(r.x_eps)
with f(x̂) = $(r.f_eps)
which took $(r.num_steps) steps""" :
"""The root was not found within $(r.max_steps) steps.
Currently, the root is approximately x̂ = $(r.root).
An estimate for the error is $(r.x_eps)
with f(x̂) = $(r.f_eps)."""
print(io,str)
end
which will print out different information if it converges or not. Notice also that we have used the string interpolation
$( ) explained in
Chapter 2, however need to include the parentheses around these variables because we want the field to be looked up. (You can remove the
() around one of the variables to see what happens--there will be an error.)
If we now run Newton’s method on
\(f(x)=x^2-2\) with
newton(x->x^2-2,1) then we get:
The root is approximately x̂ = 1.4142135623730951
An estimate for the error is 1.5947429102833119e-12
with f(x̂) = 4.440892098500626e-16
which took 5 steps
and then if we run it on a function that does not have a root. Recall in
Chapter 10, we ran Newton’s method on
\(f(x)=x^2+1\text{.}\) Since
\(x^2\) is nonnegative then the function
\(x^2+1\) is positive and so never is zero. If we enter
newton(x->x^2+1,1.1) then the result is
The root was not found within 10 steps.
Currently, the root is approximately x̂ = 0.030421004027873844.
An estimate for the error is 1.0004626117382218
with f(x̂) = 1.0009254374860639.
And now it’s clear that Newton’s method did not converge within 10 steps and there is information about the estimated root and it’s error.
Check Your Understanding 12.3.
Adapt the
Root struct and the
newton function to include the following:
(a)
Extend the
Root struct to include an array of the x values that save all of the steps of Newton’s method. (Recall that you will need to restart the kernel to change the
struct).
(b)
Store the
x values that Newton’s method iterates through and then assign them to the
Root datatype. You will need to update the
newton method. Before the
while loop, set the array equal to the initial point
x1 and each time in the while loop perform, push the new value of
x1 to the end of the array.
(c)
Write a
showSteps function that takes in an object of type
Root and prints out a table of the x values.