Introducing Julia/Controlling the flow
Different ways to control the flow
editTypically each line of a Julia program is evaluated in turn. There are various ways to control and modify the flow of evaluation. These correspond with the constructs used in other languages:
- ternary and compound expressions
- Boolean switching expressions
- if elseif else end — conditional evaluation
- for end — iterative evaluation
- while end — iterative conditional evaluation
- try catch error throw exception handling
- do blocks
Ternary expressions
editOften you'll want to do job A (or call function A) if some condition is true, or job B (function B) if it isn't. The quickest way to write this is using the ternary operator ("?" and ":"):
julia> x = 1
1
julia> x > 3 ? "yes" : "no"
"no"
julia> x = 5
5
julia> x > 3 ? "yes" : "no"
"yes"
Here's another example:
julia> x = 0.3
0.3
julia> x < 0.5 ? sin(x) : cos(x)
0.29552020666133955
and Julia returned the value of sin(x)
, because x was less than 0.5. cos(x)
wasn't evaluated at all.
Boolean switching expressions
editBoolean operators let you evaluate an expression if a condition is true. You can combine the condition and expression using &&
or ||
. &&
means "and", and ||
means "or". Since Julia evaluates expressions one by one, you can easily arrange for an expression to be evaluated only if a previous condition is true or false.
The following example uses a Julia function that returns true or false depending on whether the number is odd: isodd(n)
.
With &&
, both parts have to be true, so we can write this:
julia> isodd(1000003) && @warn("That's odd!")
WARNING: That's odd!
julia> isodd(1000004) && @warn("That's odd!")
false
If the first condition (number is odd) is true, the second expression is evaluated. If the first isn't true, the expression isn't evaluated, and just the condition is returned.
With the ||
operator, on the other hand:
julia> isodd(1000003) || @warn("That's odd!")
true
julia> isodd(1000004) || @warn("That's odd!")
WARNING: That's odd!
If the first condition is true, there's no need to evaluate the second expression, since we already have the one truth value we need for "or", and it returns the value true. If the first condition is false, the second expression is evaluated, because that one might turn out to be true.
This type of evaluation is also called "short-circuit evaluation".
If and Else
editFor a more general — and traditional — approach to conditional execution, you can use if
, elseif
, and else
. If you're used to other languages, don't worry about white space, braces, indentation, brackets, semicolons, or anything like that, but remember to finish the conditional construction with end
.
name = "Julia"
if name == "Julia"
println("I like Julia")
elseif name == "Python"
println("I like Python.")
println("But I prefer Julia.")
else
println("I don't know what I like")
end
The elseif
and else
parts are optional too:
name = "Julia"
if name == "Julia"
println("I like Julia")
end
Just don't forget the end
!
How about 'switch' and 'case' statements? Well, you don't have to learn the syntax for those, because they don't exist!
ifelse
editThere's an ifelse
function, too. It looks like this in action:
julia> s = ifelse(false, "hello", "goodbye") * " world"
ifelse
is an ordinary function, which evaluates all the arguments, and returns the second or third, depending on the value of the first. With the conditional if
or ? ... :
, only the expressions in the chosen route are evaluated. Alternatively, it is possible to write things like:
julia> x = 10 10
julia> if x > 0 "positive" else "negative or zero" end "positive"
julia> r = if x > 0 "positive" else "negative or zero" end "positive" julia> r "positive"
For loops and iteration
editWorking through a list or a set of values or from a start value to a finish value are all examples of iteration, and the for
... end
construction can let you iterate through a number of different types of object, including ranges, arrays, sets, dictionaries, and strings.
Here's the standard syntax for a simple iteration through a range of values:
julia> for i in 0:10:100 println(i) end 0 10 20 30 40 50 60 70 80 90 100
The variable i
takes the value of each element in the array (which is built from a range object) in turn — here stepping from 0 to 100 in steps of 10.
julia> for color in ["red", "green", "blue"] # an array print(color, " ") end red green blue
julia> for letter in "julia" # a string print(letter, " ") end j u l i a
julia> for element in (1, 2, 4, 8, 16, 32) # a tuple print(element, " ") end 1 2 4 8 16 32
julia> for i in Dict("A"=>1, "B"=>2) # a dictionary println(i) end "B"=>2 "A"=>1
julia> for i in Set(["a", "e", "a", "e", "i", "o", "i", "o", "u"]) println(i) end e o u a i
We haven't yet met sets and dictionaries, but iterating through them is exactly the same.
You can iterate through a 2D array, stepping "down" through column 1 from top to bottom, then through column 2, and so on:
julia> a = reshape(1:100, (10, 10)) 10x10 Array{Int64,2}: 1 11 21 31 41 51 61 71 81 91 2 12 22 32 42 52 62 72 82 92 3 13 23 33 43 53 63 73 83 93 4 14 24 34 44 54 64 74 84 94 5 15 25 35 45 55 65 75 85 95 6 16 26 36 46 56 66 76 86 96 7 17 27 37 47 57 67 77 87 97 8 18 28 38 48 58 68 78 88 98 9 19 29 39 49 59 69 79 89 99 10 20 30 40 50 60 70 80 90 100
julia> for n in a print(n, " ") end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
You can use =
instead of in
.
Iterating over an array and updating it
editWhen you're iterating over an array, the array is checked each time through the loop, in case it's changed. A mistake you should avoid making is to use push!
to make an array grow in the middle of a loop. Run the following text carefully, and be ready to Ctrl-C
when you've seen enough (otherwise your computer will eventually crash):
julia> c = [1] 1-element Array{Int64,1}: 1 julia> for i in c push!(c, i) @show c sleep(1) end c = [1,1] c = [1,1,1] c = [1,1,1,1] ...
Loop variables and scope
editThe variable that steps through each item—the 'loop variable'—exists only inside the loop, and disappears as soon as the loop finishes.
julia> for i in 1:10 @show i end i = 1 i = 2 i = 3 i = 4 i = 5 i = 6 i = 7 i = 8 i = 9 i = 10 julia> i ERROR: UndefVarError: i not defined
If you want to remember the value of the loop variable outside the loop (eg if you had to exit the loop and needed to know the value you'd reached), use the global
keyword to define a variable that outlasts the loop.
julia> for i in 1:10 global howfar if i % 4 == 0 howfar = i end end
julia> howfar 8
Here, howfar
didn't exist before the loop, but it survived to tell its story when the looping was over. If howfar
existed before the loop started, you can change its value only if you use global
in the loop.
Working in the REPL is slightly different from how you write code inside functions. In a function, you would write this:
function f()
howfar = 0
for i in 1:10
if i % 4 == 0
howfar = i
end
end
return howfar
end
@show f()
8
Variables declared inside a loop
editIn a similar way, if you declare a new variable inside a loop, it won't exist once the loop finishes. In this example, k
is created inside:
julia> for i in 1:5 k = i^2 println("$(i) squared is $(k)") end
1 squared is 1 2 squared is 4 3 squared is 9 4 squared is 16 5 squared is 25
so it doesn't exist after the loop has finished:
julia> k ERROR: UndefVarError: k not defined
Variables created inside one iteration of a loop are forgotten at the end of each iteration. In this loop:
for i in 1:10
z = i
println("z is $z")
end
z is 1 z is 2 z is 3 z is 4 z is 5 z is 6 z is 7 z is 8 z is 9 z is 10
z
is created afresh each time. If you want a variable to persist from iteration to iteration, it has to be global:
julia> counter = 0 0 julia> for i in 1:10 global counter counter += i end julia> counter 55
To see this in more detail, consider the following code.
for i in 1:10
if ! @isdefined z
println("z isn't defined")
end
z = i
println("z is $z")
end
Perhaps you expected only the first loop to produce the "z isn't defined error"? In fact, even if z
is created in the body of the loop, it is undefined at the start of the next iteration.
z isn't defined
z is 1
z isn't defined
z is 2
z isn't defined
z is 3
z isn't defined
z is 4
z isn't defined
z is 5
z isn't defined
z is 6
z isn't defined
z is 7
z isn't defined
z is 8
z isn't defined
z is 9
z isn't defined
z is 10
Again, use the global
keyword to force z
to be available outside the loop once it's been created:
for i in 1:10
global z
if ! @isdefined z
println("z isn't defined")
else
println("z was $z")
end
z = i
println("z is $z")
end
z isn't defined z is 1 z was 1 z is 2 z was 2 ... z is 9 z was 9 z is 10
although, if you're working in global scope, z
is now available everywhere, with the value 10.
This behaviour is because we're working in the REPL. It's generally better practice to put your code inside functions, where you don't need to mark variables inherited from outside the loop as global:
function f()
counter = 0
for i in 1:10
counter += i
end
return counter
end
julia> f() 55
Fine tuning the loop: Continue
editSometimes on a particular iteration you might want to skip to the next value. You can use continue
to skip the rest of the code inside the loop and start the loop again with the next value.
for i in 1:10
if i % 3 == 0
continue
end
println(i) # this and subsequent lines are
# skipped if i is a multiple of 3
end
1
2
4
5
7
8
10
Comprehensions
editThis oddly-named concept is simply a way of generating and collecting items. In mathematical circles you would say something like this:
"Let S be the set of all elements n where n is greater than or equal to 1 and less than or equal to 10".
In Julia, you can write this as:
julia> S = Set([n for n in 1:10]) Set([7,4,9,10,2,3,5,8,6,1])
and the [n for n in 1:10]
construction is called array comprehension or list comprehension ('comprehension' in the sense of 'getting everything' rather than 'understanding'). The outer brackets collect together the elements generated by evaluating the expression placed before the for
iteration. Instead of end
, use a square bracket to finish.
julia> [i^2 for i in 1:10] 10-element Array{Int64,1}: 1 4 9 16 25 36 49 64 81 100
The type of elements can be specified:
julia> Complex[i^2 for i in 1:10] 10-element Array{Complex,1}: 1.0+0.0im 4.0+0.0im 9.0+0.0im 16.0+0.0im 25.0+0.0im 36.0+0.0im 49.0+0.0im 64.0+0.0im 81.0+0.0im 100.0+0.0im
But Julia can work out the types of the results you're producing:
julia> [(i, sqrt(i)) for i in 1:10] 10-element Array{Tuple{Int64,Float64},1}: (1,1.0) (2,1.41421) (3,1.73205) (4,2.0) (5,2.23607) (6,2.44949) (7,2.64575) (8,2.82843) (9,3.0) (10,3.16228)
Here's how to make a dictionary via comprehension:
julia> Dict(string(Char(i + 64)) => i for i in 1:26) Dict{String,Int64} with 26 entries: "Z" => 26 "Q" => 17 "W" => 23 "T" => 20 "C" => 3 "P" => 16 "V" => 22 "L" => 12 "O" => 15 "B" => 2 "M" => 13 "N" => 14 "H" => 8 "A" => 1 "X" => 24 "D" => 4 "G" => 7 "E" => 5 "Y" => 25 "I" => 9 "J" => 10 "S" => 19 "U" => 21 "K" => 11 "R" => 18 "F" => 6
Next, here are two iterators in a comprehension, separated with a comma, which makes generating tables very easy. Here we're making a tuple-table:
julia> [(r,c) for r in 1:5, c in 1:2] 5×2 Array{Tuple{Int64,Int64},2}: (1,1) (1,2) (2,1) (2,2) (3,1) (3,2) (4,1) (4,2) (5,1) (5,2)
r
goes through five cycles, one cycle for every value of c
. Nested loops work in the opposite manner. Here the column-major order is respected, as shown when the array is filled with nanosecond time values:
julia> [Int(time_ns()) for r in 1:5, c in 1:2] 5×2 Array{Int64,2}: 1223184391741562 1223184391742642 1223184391741885 1223184391742817 1223184391742067 1223184391743009 1223184391742256 1223184391743184 1223184391742443 1223184391743372
You can supply a test expression as well to filter the production. For example, produce all the integers between 1 and 100 that are exactly divisible by 7:
julia> [x for x in 1:100 if x % 7 == 0] 14-element Array{Int64,1}: 7 14 21 28 35 42 49 56 63 70 77 84 91 98
Generator expressions
editLike comprehensions, generator expressions can be used to produce values from iterating a variable, but, unlike comprehensions, the values are produced on demand.
julia> sum(x^2 for x in 1:10) 385
julia> collect(x for x in 1:100 if x % 7 == 0) 14-element Array{Int64,1}: 7 14 21 28 35 42 49 56 63 70 77 84 91 98
Enumerating arrays
editOften you want to go through an array element by element while also keeping track of the index number of each element. The enumerate()
function gives you an iterable version of something, producing both an index number and the value at each index number:
julia> m = rand(0:9, 3, 3) 3×3 Array{Int64,2}: 6 5 3 4 0 7 1 7 4 julia> [i for i in enumerate(m)] 3×3 Array{Tuple{Int64,Int64},2}: (1, 6) (4, 5) (7, 3) (2, 4) (5, 0) (8, 7) (3, 1) (6, 7) (9, 4)
The array is checked for possible changes at each iteration of the loop.
Zipping arrays
editSometimes you want to work through two or more arrays at the same time, taking the first element of each array first, then the second, and so on. This is possible using the well-named zip()
function:
julia> for i in zip(0:10, 100:110, 200:210) println(i) end
(0,100,200) (1,101,201) (2,102,202) (3,103,203) (4,104,204) (5,105,205) (6,106,206) (7,107,207) (8,108,208) (9,109,209) (10,110,210)
You'd think it would all go wrong if the arrays were different sizes. What if the third array is too big, or too small?
julia> for i in zip(0:10, 100:110, 200:215) println(i) end (0,100,200) (1,101,201) (2,102,202) (3,103,203) (4,104,204) (5,105,205) (6,106,206) (7,107,207) (8,108,208) (9,109,209) (10,110,210)
but Julia isn't fooled — any oversupply or undersupply in any one of the arrays is handled gracefully.
julia> for i in zip(0:15, 100:110, 200:210) println(i) end (0,100,200) (1,101,201) (2,102,202) (3,103,203) (4,104,204) (5,105,205) (6,106,206) (7,107,207) (8,108,208) (9,109,209) (10,110,210)
This however does not work in case of filling of arrays, in this case dimensions must match:
(v1.0) julia> [i for i in zip(0:4, 100:102, 200:202)] ERROR: DimensionMismatch("dimensions must match") Stacktrace: [1] promote_shape at ./indices.jl:129 [inlined] [2] axes(::Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}}) at ./iterators.jl:371 [3] _array_for at ./array.jl:611 [inlined] [4] collect(::Base.Generator{Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}},getfield(Main, Symbol("##5#6"))}) at ./array.jl:624 [5] top-level scope at none:0
(v1.0) julia> [i for i in zip(0:2, 100:102, 200:202)] 3-element Array{Tuple{Int64,Int64,Int64},1}: (0, 100, 200) (1, 101, 201) (2, 102, 202)
Iterable objects
editThe "for something in something" construction is the same for everything that you can iterate through: arrays, dictionaries, strings, sets, ranges, and so on. In Julia this is a general principle: there are a number of ways in which you can create an "iterable object", an object that is designed to be used as part of the iteration process that provides the elements one at a time.
The most obvious example we've already met is the range object. It doesn't look much when you type it into the REPL:
julia> ro = 0:2:100 0:2:100
But it gives you the numbers when you start iterating through it:
julia> [i for i in ro] 51-element Array{Int64,1}: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 ⋮ 74 76 78 80 82 84 86 88 90 92 94 96 98 100
Should you want the numbers from a range (or other iterable object) in an array, you can use collect()
to collect them up:
julia> collect(0:25:100) 5-element Array{Int64,1}: 0 25 50 75 100
You don't have to collect every element of an iterable object, you can just iterate through it. This can be particularly helpful when you have iterable objects created by other Julia functions. For example, permutations()
creates an iterable object containing all the permutations of an array. You could of course use collect()
to grab them and make a new array:
julia> collect(permutations(1:4))
24-element Array{Array{Int64,1},1}:
[1,2,3,4]
[1,2,4,3]
…
[4,3,2,1]
but on anything large there are going to hundreds or thousands of permutations. That's the reason why iterator objects don't produce all the values from the iteration at the same time: memory and performance. A range object doesn't take up much room, even if iterating over it might take ages, depending on how big the range is. If you generate all the numbers at once, rather than only producing them when they're needed, they would all have to be stored somewhere until you need them…
Julia provides iterable objects for working with other types of data. For example, when you're working with files, you can treat an open file as an iterable object:
filehandle = "/Users/me/.julia/logs/repl_history.jl"
for line in eachline(filehandle)
println(length(line), line)
end
Use eachindex()
editA common pattern when iterating through arrays is to perform some task for each value of i
, where i
is the index number of each element, not the element:
for i in eachindex(A)
# do something with i or A[i]
end
That is idiomatic Julia code and correct in all cases, and faster in some situations (than the alternative following code). A bad code pattern to do the same, in cases where it works (which isn't always), is:
for i = 1:length(A)
# do something with i or A[i]
end
Note for advanced users
editFor the purposes of this introduction, it's probably OK to assume that arrays and matrices are indexed starting at 1 (it's not for fully generic code, i.e. for introducing in registered packages). However, it's certainly possible to use other indexing bases in Julia — for example, the OffsetArrays.jl package lets you choose any starting index. It's a good idea to read the official documentation at [1] once you start working with more advanced types of array indexing.
Even more iterators
editThere's a Julia package called IterTools.jl that provides some advanced iterator functions.
julia> ] (v1.0) pkg> add IterTools julia> using IterTools
For example, partition()
groups the objects in the iterator into easily-handled chunks:
julia> collect(partition(1:10, 3, 1)) 8-element Array{Tuple{Int64,Int64,Int64},1}: (1, 2, 3) (2, 3, 4) (3, 4, 5) (4, 5, 6) (5, 6, 7) (6, 7, 8) (7, 8, 9) (8, 9, 10)
chain()
works through all the iterators one after the other:
for i in chain(1:3, ['a', 'b', 'c'])
@show i
end
i = 1
i = 2
i = 3
i = 'a'
i = 'b'
i = 'c'
subsets()
works through all subsets of an object. You can specify a size:
for i in subsets(collect(1:6), 3)
@show i
end
i = [1,2,3]
i = [1,2,4]
i = [1,2,5]
i = [1,2,6]
i = [1,3,4]
i = [1,3,5]
i = [1,3,6]
i = [1,4,5]
i = [1,4,6]
i = [1,5,6]
i = [2,3,4]
i = [2,3,5]
i = [2,3,6]
i = [2,4,5]
i = [2,4,6]
i = [2,5,6]
i = [3,4,5]
i = [3,4,6]
i = [3,5,6]
i = [4,5,6]
Nested loops
editIf you want to nest one loop inside another, you don't have to duplicate the for
and end
keywords. Just use a comma:
julia> for x in 1:10, y in 1:10
@show (x, y)
end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (1,4)
(x,y) = (1,5)
(x,y) = (1,6)
(x,y) = (1,7)
(x,y) = (1,8)
(x,y) = (1,9)
(x,y) = (1,10)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (2,4)
(x,y) = (2,5)
(x,y) = (2,6)
(x,y) = (2,7)
(x,y) = (2,8)
(x,y) = (2,9)
(x,y) = (2,10)
(x,y) = (3,1)
(x,y) = (3,2)
...
(x,y) = (9,9)
(x,y) = (9,10)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)
(x,y) = (10,4)
(x,y) = (10,5)
(x,y) = (10,6)
(x,y) = (10,7)
(x,y) = (10,8)
(x,y) = (10,9)
(x,y) = (10,10)
(The useful @show
macro prints out the names of things and their values.)
One difference between the shorter and longer forms of nesting loops is the behaviour of break
:
julia> for x in 1:10
for y in 1:10
@show (x, y)
if y % 3 == 0
break
end
end
end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (3,1)
(x,y) = (3,2)
(x,y) = (3,3)
(x,y) = (4,1)
(x,y) = (4,2)
(x,y) = (4,3)
(x,y) = (5,1)
(x,y) = (5,2)
(x,y) = (5,3)
(x,y) = (6,1)
(x,y) = (6,2)
(x,y) = (6,3)
(x,y) = (7,1)
(x,y) = (7,2)
(x,y) = (7,3)
(x,y) = (8,1)
(x,y) = (8,2)
(x,y) = (8,3)
(x,y) = (9,1)
(x,y) = (9,2)
(x,y) = (9,3)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)
julia> for x in 1:10, y in 1:10
@show (x, y)
if y % 3 == 0
break
end
end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
Notice that break
breaks out of both inner and outer loops in the shorter form, but only out of the inner loop in the longer form.
Optimizing nested loops
editWith Julia, inner loops should concern rows rather than columns. This is due to how arrays are stored in memory. In this Julia array, for example, cells 1, 2, 3, and 4 are stored next to each other in memory (the 'column-major' format). So moving down the columns from 1 to 2 to 3 is faster than moving along rows, because jumping across from column to column, from 1 to 5 to 9, requires an extra calculation:
+-----+-----+-----+--+ | 1 | 5 | 9 | | | | | +--------------------+ | 2 | 6 | 10 | | | | | +--------------------+ | 3 | 7 | 11 | | | | | +--------------------+ | 4 | 8 | 12 | | | | | +-----+-----+-----+--+
The following examples consist of simple loops, but the way the rows and columns are iterated differ. The "bad" version looks along the first row column by column, then moves down to the next row, and so on.
function laplacian_bad(lap_x::Array{Float64,2}, x::Array{Float64,2})
nr, nc = size(x)
for ir = 2:nr-1, ic = 2:nc-1 # bad loop nesting order
lap_x[ir, ic] =
(x[ir+1, ic] + x[ir-1, ic] +
x[ir, ic+1] + x[ir, ic-1]) - 4*x[ir, ic]
end
end
In the "good" version, the two loops are nested properly, so that the inner loop moves down through the rows, following the memory layout of the array:
function laplacian_good(lap_x::Array{Float64,2}, x::Array{Float64,2})
nr,nc = size(x)
for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
lap_x[ir,ic] =
(x[ir+1,ic] + x[ir-1,ic] +
x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
end
end
Another way to increase the speed is to remove the array bounds checking, using the macro @inbounds
:
function laplacian_good_nocheck(lap_x::Array{Float64,2}, x::Array{Float64,2})
nr,nc = size(x)
for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
@inbounds begin lap_x[ir,ic] = # no array bounds checking
(x[ir+1,ic] + x[ir-1,ic] +
x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
end
end
end
Here's the test function:
function main_test(nr, nc)
field = zeros(nr, nc)
for ic = 1:nc, ir = 1:nr
if ir == 1 || ic == 1 || ir == nr || ic == nc
field[ir,ic] = 1.0
end
end
lap_field = zeros(size(field))
t = @elapsed laplacian_bad(lap_field, field)
println(rpad("laplacian_bad", 30), t)
t = @elapsed laplacian_good(lap_field, field)
println(rpad("laplacian_good", 30), t)
t = @elapsed laplacian_good_nocheck(lap_field, field)
println(rpad("laplacian_good no check", 30), t)
end
and the results show the difference in performance just based on the row/column scanning order. The "no check" version is even faster....
julia> main_test(10000,10000) laplacian_bad 1.947936034 laplacian_good 0.190697149 laplacian_good no check 0.092164871
Making your own iterable objects
editIt's possible to design your own iterable objects. When you're defining your type, you add a couple of methods to Julia's iterate()
function. Then you can use something like for
.. end
loop to work through the components of your object, and these iterate()
methods are called automatically as necessary.
The following example shows how you can create an iterable object that generates the sequence of strings combining an uppercase letter with a number from 1 to 9. So the first item in our sequence is "A1", followed by "A2", "A3", up to "A9", then "B1", "B2", and so on, finishing at "Z9".
First, we'll define a new type called SN (StringNumber):
mutable struct SN
str::String
num::Int64
end
Later we'll create an iterable object of this type using something like this:
sn = SN("A", 1)
and the iterator will yield all the strings up to "Z9".
We must now add two methods to the iterate()
function. This function already exists in Julia (that's why you can iterate over all the basic data objects), so the Base
prefix is required: we're adding a new method to the existing iterate()
function, one which is designed to handle these special objects.
The first method takes no arguments, except for the type, and is for starting the iteration process off.
function Base.iterate(sn::SN)
str = sn.str
num = sn.num
if num == 9
nextnum = 1
nextstr = string(Char(Int(str[1])) + 1)
else
nextnum = num + 1
nextstr = str
end
return (sn, SN(nextstr, nextnum))
end
This returns a tuple: the first value, and the future value of the iterator, which we've calculated (just in case we ever want to start the iterator at a point other than "A1").
The second method of iterate()
takes two arguments: an iterable object and the current state. It again returns a tuple of two values, the next item and the next state. But first, if there are no more values available, the iterate()
function should return nothing.
function Base.iterate(sn::SN, state)
# check if we've finished?
if state.str == "[" # when Z changes to [ we're done
return
end
# we haven't finished, so we'll use the incoming one immediately
str = state.str
num = state.num
# and prepare the one after that, to be saved for later
if num == 9
nextnum = 1
nextstr = string(Char(Int(str[1])) + 1)
else
nextnum = num + 1
nextstr = state.str
end
# return: the one to use next, the one after that
return (SN(str, num), SN(nextstr, nextnum))
end
Telling the iterator when it's finished is easy, because as soon as the incoming state contains a "[" we've finished, because the code for "[" (91) is immediately after the code for "Z" (90).
With these two methods added to handle the SN type, it's now possible to iterate through them. It's also useful to add methods for a few other Base functions, such as show()
and length()
. The length()
method works out how many more SN strings are available starting at sn
.
Base.show(io::IO, sn::SN) = print(io, string(sn.str, sn.num))
function Base.length(sn::SN)
cn1 = Char(Int(Char(sn.str[1]) + 1))
cnz = Char(Int(Char('Z')))
(length(cn1:cnz) * 9) + (10 - sn.num)
end
The iterator is now ready for use:
julia> sn = SN("A", 1) A1 julia> for i in sn @show i end
i = A1
i = A2
i = A3
i = A4
i = A5
i = A6
i = A7
i = A8
...
i = Z6
i = Z7
i = Z8
i = Z9
julia> for sn in SN("K", 9) print(sn, " ") end
K9 L1 L2 L3 L4 L5 L6 L7 L8 L9 M1 M2 M3 M4 M5 M6 M7 M8 M9 N1 N2 N3 N4 N5 N6 N7 N8 N9 O1 O2 O3 O4 O5 O6 O7 O8 O9 P1 P2 P3 P4 P5 P6 P7 P8 P9 Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8 Q9 R1 R2 R3 R4 R5 R6 R7 R8 R9 S1 S2 S3 S4 S5 S6 S7 S8 S9 T1 T2 T3 T4 T5 T6 T7 T8 T9 U1 U2 U3 U4 U5 U6 U7 U8 U9 V1 V2 V3 V4 V5 V6 V7 V8 V9 W1 W2 W3 W4 W5 W6 W7 W8 W9 X1 X2 X3 X4 X5 X6 X7 X8 X9 Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Y9 Z1 Z2 Z3 Z4 Z5 Z6 Z7 Z8 Z9
julia> collect(SN("Q", 7)), (Any[Q7, Q8, Q9, R1, R2, R3, R4, R5, R6, R7 … Y9, Z1, Z2, Z3, Z4, Z5, Z6, Z7, Z8, Z9],)
While loops
editTo repeat some expressions while a condition is true, use the while
... end
construction.
julia> x = 0 0 julia> while x < 4 println(x) global x += 1 end 0 1 2 3
If you're working outside a function, you'll need the global
declaration of x
before you can change its value. Inside a function, you don't need global
.
If you want the condition to be tested after the statements, rather than before, producing a "do .. until" form, use the following construction:
while true
println(x)
x += 1
x >= 4 && break
end
0
1
2
3
Here we're using a Boolean switch rather than an if
... end
statement.
Template for while loops
editHere is a basic template for a while
loop that will run the function find_value
repeatedly until it returns a value that's no greater than 0.
function find_value(n) # find next value if current value is n
return n - 0.5
end
function main(start=10)
attempts = 0
value = start # starting value
while value > 0.0
value = find_value(value) # next value given this value
attempts += 1
println("value: $value after $attempts attempts" )
end
return value, attempts
end
final_value, number_of_attempts = main(0)
println("The final value was $final_value, and it took $number_of_attempts attempts.")
For example, with small changes this code explores the famous Collatz_conjecture
function find_value(n)
ifelse(iseven(n), n ÷ 2, 3n + 1) # Collatz calculation
end
function main(start=10)
attempts = 0
value = start # starting value
while value > 1 # while greater than 1
value = find_value(value)
attempts += 1
println("value: $value after $attempts attempts" )
end
return value, attempts
end
final_value, number_of_attempts = main(27)
println("The final value was $final_value, and it took $number_of_attempts attempts.")
main(12)
takes 9 attempts, whereas main(27)
takes 111 attempts.
Using Julia's macros, you can create your own control structures. See Metaprogramming.
Exceptions
editIf you want to write code that checks for errors and handles them gracefully, use the try
... catch
construction.
With a catch
phrase, you can handle problems that occur in your code, possibly allowing the program to continue rather than grind to a halt.
In the next example, our code attempts to change the first character of a string directly (which isn't allowed, because strings in Julia can't be modified in place):
julia> s = "string";
julia> try
s[1] = "p"
catch e
println("caught an error: $e")
println("but we can continue with execution...")
end
caught an error: MethodError(setindex!,("string","p",1)) but we can continue with execution...
The error()
function raises an error exception with a given message.
Do block
editFinally, let's look at a do
block, which is another syntax form that, like the list comprehension, looks at first sight to be a bit backwards (i.e. it can perhaps be better understood by starting at the end and working towards the beginning).
Remember the find()
example from earlier?
julia> smallprimes = [2,3,5,7,11,13,17,19,23];
julia> findall(x -> isequal(13, x), smallprimes) 1-element Array{Int64,1}: 6
The anonymous function (x -> isequal(13, x)
) is the first argument of find()
, and it operates on the second. But with a do
block, you can lift the function out and put it in between a do ... end
block construction:
julia> findall(smallprimes) do x isequal(x, 13) end 1-element Array{Int64,1}: 6
You just lose the arrow and change the order, putting the find()
function and its target argument first, then adding the anonymous function's arguments and body after the do
.
The idea is that it's easier to write a longer anonymous function on multiple lines at the end of the form, rather than wedged in as the first argument.