174 lines
6.1 KiB
Julia
174 lines
6.1 KiB
Julia
|
||
# User defined types are made with the struct keyword. Member variables are
|
||
# listed in the struct body as follows.
|
||
# Keep in mind that julia does not support redefining of types by default,
|
||
# so whenever you change anything in the struct (field names/types, etc.)
|
||
# you have to restart the REPL.
|
||
# Alternatively you could look into the package Revise.jl which solves
|
||
# some of these problems.
|
||
struct TestType
|
||
x
|
||
y
|
||
z
|
||
end
|
||
|
||
# Every struct gets a default constructor that is the typename taking
|
||
# in all the member variables in order.
|
||
t = TestType(1, 2, 3)
|
||
|
||
# Every struct also gets a default print function that shows the variable
|
||
# in style of the constructor.
|
||
@show t
|
||
|
||
# Accessing the member variables of the struct is done simply by just
|
||
# writing variable_name.field_name
|
||
# every field is always public as julia does not implement any heavy
|
||
# object oriented design.
|
||
@show t.x
|
||
|
||
# This line is illegal since struct are immutable by default.
|
||
# t.y = 5
|
||
|
||
# Field mutability can be achieved by adding the keyword "mutable" in front.
|
||
# However for simple structs, like this xyz-point like structure, it is
|
||
# much more efficient if you can avoid making it mutable. This has to do
|
||
# with how mutable struct are allocated differently then immutable ones.
|
||
# Read more in the "Types" section of the julia documentation.
|
||
mutable struct TestType2
|
||
x::Float64
|
||
y::Float64
|
||
z::Float64
|
||
end
|
||
|
||
# Also as illustrated above, the fields in a struct can be strictly typed.
|
||
# This is usually a good idea since it makes the struct take a constant
|
||
# amount of space, and removes a lot of type bookkeping.
|
||
|
||
t2 = TestType2(1.68, 3.14, 2.71)
|
||
|
||
# With this mutable struct, the variable now behaves more like a general
|
||
# container (Array/vector) so you can change the fields.
|
||
t2.y = 1.23
|
||
@show t2
|
||
|
||
# You can also make parametric types, kind of like templates in C++.
|
||
# This essentially creates a new type for each new T, so it alleviates the
|
||
# problem of keeping track of the types of the individual fields.
|
||
struct TestType3{T}
|
||
x::T
|
||
y::T
|
||
z::T
|
||
end
|
||
|
||
# This now constructs a TestType3{Int64}
|
||
t3 = TestType3(1, 2, 3)
|
||
@show t3
|
||
|
||
|
||
# This will be the example struct throughout the rest of the file.
|
||
struct Polar{T<:Real}
|
||
r::T
|
||
θ::T
|
||
|
||
# You might want to make your own constructors for a type.
|
||
# These are typically made inside the struct body since this overrides
|
||
# the creation of the default constructor described above.
|
||
function Polar(r::T, θ::T) where T <: Real
|
||
# Doing some constructy stuff
|
||
println("Creating Polar number (r = $r, θ = $θ)")
|
||
|
||
# The actual variable is created by calling the "new()" function
|
||
# which acts as the default constructor. This however is just
|
||
# accessible to functions defined inside the struct body.
|
||
new{T}(r, θ)
|
||
end
|
||
|
||
# You might want to implement multiple different constructors.
|
||
# This one is short and simple so it can be created with the inline syntax.
|
||
# The zero(T) function finds the apropriate "zero" value
|
||
# for the given type T. That way no implicit conversions have to be done.
|
||
Polar{T}() where T <: Real = new{T}(zero(T), zero(T))
|
||
end
|
||
|
||
# Not all constructors have to be defined inside the struct body, but here
|
||
# outside the body we no longer have access to the "new()" function since
|
||
# the compiler doesn't have any way to infer which type "new" would refer
|
||
# to out here. So instead we have to use one of the constructors we defined
|
||
# inside the struct.
|
||
# This constructor is very similar to the Polar{T}() function but instead
|
||
# of writing p = Polar{Int}() to take the type in as a template argument
|
||
# you would pass the type in as an actual argument; p = Polar(Int)
|
||
Polar(::Type{T}) where T <: Real = Polar{T}()
|
||
|
||
|
||
# Constructing with the first constructor
|
||
p = Polar(3.14, 2.71)
|
||
@show p
|
||
|
||
# Constructing with the empty constructor
|
||
p = Polar{Int}()
|
||
@show p
|
||
|
||
# Constructing with the constructor taking the type as an argument
|
||
p = Polar(Float16)
|
||
@show p
|
||
|
||
|
||
# You might want to overload som basic operators and functions on your
|
||
# type. One common thing to want to override is how your variables are printed.
|
||
# Since there are so many different ways of converting a variable to text
|
||
# (print, println, @printf, string, @show, display, etc.)
|
||
# it can be difficult to find out which function is actually responsible
|
||
# for converting to text.
|
||
# This function turns out to be the Base.show() function.
|
||
# It takes in an IO stream object and and the variable you want to show.
|
||
|
||
# Since the funtion is from the Base module
|
||
# we have to override Base.show specifically
|
||
# Also it is very important to actually specify the type of the variable here
|
||
# so that we are actually specifying the show function for our type only
|
||
# and not for any general type.
|
||
function Base.show(io::IO, p::Polar)
|
||
# Say we want to print our polar number as r * e^(i θ) style string
|
||
show(io, p.r) # non strings we want to recursively show
|
||
write(io, " ⋅ ℯ^(i ⋅ ") # strings we want to write directly with write
|
||
show(io, p.θ)
|
||
write(io, ")")
|
||
end
|
||
|
||
# Now our polar numbers are printed as we specified
|
||
p = Polar(3.14, 2.71)
|
||
@show p
|
||
println(p)
|
||
display(p)
|
||
|
||
# Even converting to a string is now done with our show functon
|
||
s = string(p)
|
||
@show s
|
||
|
||
|
||
# Overloading operators is even simpler as every infix operator is just a
|
||
# function; a + b is equivalent to +(a, b) (for all you lisp lovers)
|
||
|
||
# For some reason we cannot write the Base.* inline for infix operators
|
||
import Base.*
|
||
*(p1::Polar{T}, p2::Polar{T}) where T = Polar(p1.r * p2.r, p1.θ + p2.θ)
|
||
|
||
# Multiplication between different types might also be useful
|
||
*(x::T, p::Polar{T}) where T = Polar(x * p.r, p.θ)
|
||
# Implementing the reverse mulitplication such that it becomes commutative
|
||
*(p::Polar{T}, x::T) where T = x * p
|
||
|
||
p1 = Polar(2.0, 15.0)
|
||
p2 = Polar(3.0, 30.0)
|
||
p3 = p1 * p2
|
||
@show p3
|
||
|
||
# The in-place arithmetic operators (+=, -=, *=, /=, etc.) are not actual
|
||
# operators but rather just an alias for a = a * b so they need not be
|
||
# implemented seperately. If the memory copying is a problem and you
|
||
# really have to do it in-place the way to do that would be to make some
|
||
# sort of mult!(p, x) function.
|
||
p3 *= 0.5
|
||
@show p3
|