Note: The materials of the this introduction are borrowed from [1]. Hence, it is highly recommended to read the Chapter 15 to Chapter 18 in [1].
repr
dump
::
operator::
operatordo
blockscondition ? true : false
@debug
macroAt 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.
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.
A programmer-defined composite type is also called a struct. For example, the struct
definition for a point looks like this:
struct IPoint
x
y
end
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.
p = IPoint(3.0, 4.0)
IPoint(3.0, 4.0)
To access the attributes of p
, we use the .
operator:
p
IPoint(3.0, 4.0)
As we can see from below, a Point is an immutable type
ismutable(p)
false
Note
Here are some advantages for a struct to be immutable:
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.
To define a mutable struct, we use the keyword mutable struct
in place of struct
:
mutable struct MPoint
x
y
end
mp = MPoint(1, 2)
MPoint(1, 2)
mp.x = 3
3
struct Rectangle
width::Real
height::Real
corner
end
A pure function is a function without side effects (e.g. modifying the arguments) after execution and the returned value only on the input passed to the function.
Here is an example of a pure function:
A modifier function, on the other hand, modifies the the its input and returns nothing
. The name of a modifier function, in Julia, is conventially ends with a exclamation mark !
. Learn more about this from the documentation.
Here is an example of a modifier:
function reset_point(p::MPoint)
(p.x, p.y)=(0, 0)
return nothing
end
reset_point (generic function with 1 method)
let
p=MPoint(1, 2)
reset_point(p)
println((p.x, p.y))
end
(0, 0)
The idea of function overloading is that you can define two functions taking different arguments with the same name. For example, we can difine a distance function calculating two different points:
A constructor is a special function that is called to create an object. The default constructor methods of Rectangle
have the following signatures:
Rectange(width::Real, height::Real, corner)
They simply initialize the attribute of the object. We can also define our own inner constructor, by using new
in a these constructors to generate a new object. An example is given as follows:
mutable struct Square
side
corner
#' customized constructor (override the default constructor)
function Square(side, corner = IPoint(0, 0))
@assert(side >= 0, "Side length is negative!")
return new(side, corner)
end
#' copy constructor
function Square(square::Square)
new(square.side, square.corner)
end
end
We can compare the following results:
let
square1 = Square(3, IPoint(0, 0))
square2 = Square(square1)
(square1 == square2), (square1 === square2)
end
(false, false)
let
square1 = Square(3, IPoint(0, 0))
square2 = square1
(square1 == square2), (square1 === square2)
end
(true, true)
Warning
The default constructor is not available if any inner constructor is defined. You have to write explicitly all the inner constructors you need.
In the code above, we have a constructor taking an argument of type Square
. It is useful when we decide to make a generate a new copy of the object later on.
show
We now override the show
function to generate an string expression for type Square
function Base.show(io::IO, p::MPoint)
print(io, "($(p.x), $(p.y))")
end
function Base.show(io::IO, p::IPoint)
print(io, "($(p.x), $(p.y))")
end
function Base.show(io::IO, square::Square)
print(io, "Square anchored at $(square.corner) with side length $(square.side)")
end
Square(2, IPoint(0, 0))
Square anchored at (0, 0) with side length 2
Square(2, MPoint(0, 0))
Square anchored at (0, 0) with side length 2
Some operators in Julia, like other functions, can be overloaded. These operators are in Base
module, and operator op
in the module should referred to as Base:op
or even Base:(op)
(e.g. mandatory for ==
)
function Base.:+(p::MPoint, s::Square)
t = Square(s)
s.corner.x += p.x
s.corner.y += p.y
return t
end
distance(p) = sqrt(p.x^2 + p.y^2)
distance (generic function with 1 method)
distance(p, q) = sqrt((p.x - q.x)^2 + (p.y - q.y)^2)
distance (generic function with 2 methods)
let
p=MPoint(1, 2)
println(distance(p))
println((p.x, p.y))
end
2.23606797749979
(1, 2)
let
p=MPoint(1, 2)
println(distance(p), distance(p, p))
end
2.236067977499790.0
MPoint(3, 4) + Square(3, MPoint(1, 2))
Square anchored at (4, 6) with side length 3
The choice of which method to execute when a function is applied is called dispatch.
Many of the functions we wrote for strings also work for other sequence types. For example, in Dictionary as a Collection of Counters we used histogram to count the number of times each letter appears in a word.
function histogram(s)
d = Dict()
for c in s
if c ∉ keys(d)
d[c] = 1
else
d[c] += 1
end
end
d
end
histogram (generic function with 1 method)
histogram(("spam", "egg", "spam", "spam", "bacon", "spam"))
Dict{Any, Any} with 3 entries:
"bacon" => 1
"spam" => 4
"egg" => 1
histogram(["spam", "egg", "spam", "spam", "bacon", "spam"])
Dict{Any, Any} with 3 entries:
"bacon" => 1
"spam" => 4
"egg" => 1
Functions that work with several types are called polymorphic. Polymorphism can facilitate code reuse.
In the following, we use the same example as [1] to demonstrate abstract type and subtyping.
Here is our setup
const suit_names = ["♣", "♦", "♥", "♠"]
4-element Vector{String}:
"♣"
"♦"
"♥"
"♠"
const rank_names = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
13-element Vector{String}:
"A"
"2"
"3"
"4"
"5"
"6"
"7"
"8"
"9"
"10"
"J"
"Q"
"K"
struct Card
suit :: Int64
rank :: Int64
#' constructor
function Card(suit::Int64, rank::Int64)
@assert(1 ≤ suit ≤ 4, "suit is not between 1 and 4")
@assert(1 ≤ rank ≤ 13, "rank is not between 1 and 13")
new(suit, rank)
end
#' methods overloading
function Base.show(io::IO, card::Card)
print(io, rank_names[card.rank], suit_names[card.suit])
end
#' operator overloading
function Base.:<(c1::Card, c2::Card)
(c1.suit, c1.rank) < (c2.suit, c2.rank)
end
end
We now define a virtual type CardSet
, to which both a deck and a hand belong to:
abstract type CardSet end
struct Hand <: CardSet
cards :: Array{Card, 1}
label :: String
function Hand(label::String="")
Hand(Card[], label)
end
end
We can defining generic functions on CardSet, which apply to both decks and hands:
begin
function Base.show(io::IO, cs::CardSet)
for card in cs.cards
print(io, card, " ")
end
end
function Base.pop!(cs::CardSet)
pop!(cs.cards)
end
function Base.push!(cs::CardSet, card::Card)
push!(cs.cards, card)
nothing
end
end
struct Deck <: CardSet
cards :: Array{Card, 1}
#' constructor
function Deck()
deck = new(Card[])
for suit in 1:4
for rank in 1:13
push!(deck.cards, Card(suit, rank))
end
end
deck
end
end
Deck()
A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ 10♣ J♣ Q♣ K♣ A♦ 2♦ 3♦ 4♦ 5♦ 6♦ 7♦ 8♦ 9♦ 10♦ J♦ Q♦ K♦ A♥ 2♥ 3♥ 4♥ 5♥ 6♥ 7♥ 8♥ 9♥ 10♥ J♥ Q♥ K♥ A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ 10♠ J♠ Q♠ K♠
deck = Deck()
A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ 10♣ J♣ Q♣ K♣ A♦ 2♦ 3♦ 4♦ 5♦ 6♦ 7♦ 8♦ 9♦ 10♦ J♦ Q♦ K♦ A♥ 2♥ 3♥ 4♥ 5♥ 6♥ 7♥ 8♥ 9♥ 10♥ J♥ Q♥ K♥ A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ 10♠ J♠ Q♠ K♠
repr
Create a string from any value using the show function. You should not add methods to repr; define a show method instead.
repr(zip("abc", [1 2 3]))
"zip(\"abc\", [1 2 3])"
dump
Show every part of the representation of a value.
dump(zip("abc", [1 2 3]))
Base.Iterators.Zip{Tuple{String, Matrix{Int64}}}
is: Tuple{String, Matrix{Int64}}
1: String "abc"
2: Array{Int64}((1, 3)) [1 2 3]
::
operatorWe may use a :: T
to assert if a variable a
is of type T
:
let
f(x::Int,y::Int) = x+y
try
println(f(1.0, 2.0))
catch e
println("Error: $e")
end
try
println(f(1, 2))
catch e
println("Error: $e")
end
end
Error: MethodError(Main.FD_SANDBOX_5514522325621768181.var"#f#1"(), (1.0, 2.0), 0x0000000000007b61)
3
It may also help us to perform assignment:
let
x::Int8 = round(Int, 2.0)
typeof(x)
end
Int8
A macro maps a tuple of arguments, expressed as space-separated expressions or a function-call-like argument list, to a returned expression. A macro named macro_name
can be called by @macro_name
.
::
operator:expr
quote an expression expr
, returning the abstract syntax tree (AST) of expr. The AST may be of type Expr
, Symbol
, or a literal value. The syntax :identifier evaluates to a Symbol
.
You can name the components of a tuple, creating a named tuple:
x = (a=1, b=1+1)
(a = 1, b = 2)
@isdefined x
true
x.a
1
do
blocksIn Reading and Writing we had to close the file after when where done writing. This can be done automatically using a do block:
let
data = "This here's the wattle,\nthe emblem of our land.\n"
open("output.txt", "w") do fout
write(fout, data)
end
end
48
In this example fout is the file stream used for output. This is functionally equivalent to
let
data = "This here's the wattle,\nthe emblem of our land.\n"
f = fout -> begin
write(fout, data)
end
open(f, "output.txt", "w")
end
48
"The way it works is the following:"
"The way it works is the following:"
function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end
Check out the documentation.
condition ? true : false
The is an alternative to an if-elseif
statement:
1 > 0 ? "It is true!" : "It is false!"
"It is true!"
The operators &&
and ||
do a short-circuit evaluation: a next argument is only evaluated when it is needed to determine the final value.
function fact(n::Integer)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact (generic function with 1 method)
fact(4)
24
Primitive types are a concrete types whose data consists of plain old bits, and their identity depend only on bits.
Composite type are called records, structs, or objects in various languages. A composite type is a collection of named fields, an instance of which can be treated as a single value. In many languages, composite types are the only kind of user-definable type, and they are by far the most commonly used user-defined type in Julia as well.
Primitive/composite/abstract types can have parameters. See the following example:
struct Point{T<:Real}
x::T
y::T
end
isa(8, Union{Real, String})
true
isa("s", Union{Int64, String})
true
Parametric Methods
Method definitions can also have type parameters qualifying their signature:
isintpoint(p::Point{T}) where {T} = (T === Int64)
isintpoint (generic function with 1 method)
isintpoint(Point(1, 2))
true
Any arbitrary Julia object can be made “callable”. Such “callable” objects are sometimes called functors.
let
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
p = Polynomial([1,10,100])
p(3)
end
931
Parametric types can be explicitly or implicitly constructed:
Point(1,2) # implicit T
Point{Int64}(1, 2)
Point{Int64}(1, 2) # explicit T
Point{Int64}(1, 2)
Point(1,2.5) # implicit T
MethodError: no method matching Main.FD_SANDBOX_5514522325621768181.Point(::Int64, ::Float64)
Closest candidates are:
Main.FD_SANDBOX_5514522325621768181.Point(::T, !Matched::T) where T<:Real
@ Main.FD_SANDBOX_5514522325621768181 none:2
To address the above ambiguous issue, we can use promote
function:
begin
struct PointMultitype{T<:Real}
x::T
y::T
PointMultitype{T}(x::T,y::T) where {T<:Real} = new{T}(x,y)
PointMultitype(x::Real, y::Real) = Point(promote(x,y)...);
end
PointMultitype(1,2.5)
end
Point{Float64}(1.0, 2.5)
Julia has a system for promoting arguments to a common type. This is not done automatically but can be easily extended.
We can add our own convert methods:
let
Base.convert(::Type{Point{T}}, x::Array{T, 1}) where {T<:Real} = Point(x...)
convert(Point{Int64},[1,2])
end
Point{Int64}(1, 2)
On the other hand, promotion is the conversion of values of mixed types to a single common type:
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
promote_rule (generic function with 1 method)
Julia code can be represented as a data structure of the language itself. This allows a program to transform and generate its own code.
Every Julia program starts as a string, which we can parse into an object called an expression
:
begin
ex = Meta.parse("1 + 2") # expression 1
ex = quote # expression 2
1 + 2
end
end
quote
#= none:4 =#
1 + 2
end
typeof(ex)
Expr
dump(ex)
Expr
head: Symbol block
args: Array{Any}((2,))
1: LineNumberNode
line: Int64 4
file: Symbol none
2: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 2
We can use eval
to evaluate expressions:
eval(ex)
3
Macros
can include generated code in a program. A macro maps a tuple of Expr objects directly to a compiled expression:
macro containervariable(container, element)
return esc(:($(Symbol(container,element)) = $container[$element]))
end
@containervariable (macro with 1 method)
The macro call @containervariable letters 1
is replaced by :(letters1 = letters[1])
.
@time sum([1 2 3 4])
0.000002 seconds (1 allocation: 96 bytes)
10
@code_lowered sum([1 2 3 4])
CodeInfo(
1 ─ nothing
│ %2 = Base.:(:)
│ %3 = Core.NamedTuple()
│ %4 = Base.pairs(%3)
│ %5 = Base.:(var"#sum#807")(%2, %4, #self#, a)
└── return %5
)
@code_typed sum([1 2 3 4])
CodeInfo(
1 ─ %1 = Base.identity::typeof(identity)
│ %2 = Base.add_sum::typeof(Base.add_sum)
│ %3 = invoke Base._mapreduce(%1::typeof(identity), %2::typeof(Base.add_sum), $(QuoteNode(IndexLinear()))::IndexLinear, a::Matrix{Int64})::Int64
└── return %3
) => Int64
@code_llvm sum([1 2 3 4])
; @ reducedim.jl:994 within `sum`
; Function Attrs: uwtable
define i64 @julia_sum_338({}* noundef nonnull align 16 dereferenceable(40) %0) #0 {
top:
; ┌ @ reducedim.jl:994 within `#sum#807`
; │┌ @ reducedim.jl:998 within `_sum`
; ││┌ @ reducedim.jl:998 within `#_sum#809`
; │││┌ @ reducedim.jl:999 within `_sum`
; ││││┌ @ reducedim.jl:999 within `#_sum#810`
; │││││┌ @ reducedim.jl:357 within `mapreduce`
; ││││││┌ @ reducedim.jl:357 within `#mapreduce#800`
; │││││││┌ @ reducedim.jl:365 within `_mapreduce_dim`
%1 = call i64 @j__mapreduce_340({}* nonnull %0) #0
; └└└└└└└└
ret i64 %1
}
@code_native sum([1 2 3 4])
.text
.file "sum"
.globl julia_sum_368 # -- Begin function julia_sum_368
.p2align 4, 0x90
.type julia_sum_368,@function
julia_sum_368: # @julia_sum_368
; ┌ @ reducedim.jl:994 within `sum`
.cfi_startproc
# %bb.0: # %top
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
; │┌ @ reducedim.jl:994 within `#sum#807`
; ││┌ @ reducedim.jl:998 within `_sum`
; │││┌ @ reducedim.jl:998 within `#_sum#809`
; ││││┌ @ reducedim.jl:999 within `_sum`
; │││││┌ @ reducedim.jl:999 within `#_sum#810`
; ││││││┌ @ reducedim.jl:357 within `mapreduce`
; │││││││┌ @ reducedim.jl:357 within `#mapreduce#800`
; ││││││││┌ @ reducedim.jl:365 within `_mapreduce_dim`
movabsq $j__mapreduce_370, %rax
callq *%rax
; │└└└└└└└└
addq $32, %rsp
popq %rbp
retq
.Lfunc_end0:
.size julia_sum_368, .Lfunc_end0-julia_sum_368
.cfi_endproc
; └
# -- End function
.section ".note.GNU-stack","",@progbits
Compare these with @which
@which sum([1 2 3 4])
sum(a::AbstractArray; dims, kw...)
@ Base reducedim.jl:994
@debug
macrolet
x=5
y=-5
while x > 0 && y < 0
x -= 1
y += 1
@debug "variables" x y
@debug "condition" x > 0 && y < 0
end
end
┌ Debug: variables
│ x = 4
│ y = -4
└ @ Main REPL[10]:7
┌ Debug: condition
│ x > 0 && y < 0 = true
└ @ Main REPL[10]:8
┌ Debug: variables
│ x = 3
│ y = -3
└ @ Main REPL[10]:7
┌ Debug: condition
│ x > 0 && y < 0 = true
└ @ Main REPL[10]:8
┌ Debug: variables
│ x = 2
│ y = -2
└ @ Main REPL[10]:7
┌ Debug: condition
│ x > 0 && y < 0 = true
└ @ Main REPL[10]:8
┌ Debug: variables
│ x = 1
│ y = -1
└ @ Main REPL[10]:7
┌ Debug: condition
│ x > 0 && y < 0 = true
└ @ Main REPL[10]:8
┌ Debug: variables
│ x = 0
│ y = 0
└ @ Main REPL[10]:7
┌ Debug: condition
│ x > 0 && y < 0 = false
└ @ Main REPL[10]:8
[1] | Think Julia: How to Think Like a Computer Scientist, https://benlauwens.github.io/ThinkJulia.jl/latest/book.html. |