Loops are used when actions must be repeatedly performed. Julia has two types of loops:
- for-loops
- while-loops
Julia also has things called "comprehensions", "broadcasting", and "maps" that act like loops. We'll cover those in later posts.
Just Enough Information About Ranges¶
Before getting to for-loops, we need to quickly discuss the concept of a range.
A range is a compact, memory efficient, notation for a sequence of numbers. And remember, a sequence is ordered! Here's a range of numbers representing the integers between 1 and 10, inclusive:
xxxxxxxxxx
r = 1:10
1:10
That doesn't look much like a sequence of numbers! The reason why is that the range does not store all the integers between 1 and 10, it just stores the start and stop values. To see all the elements in a range, use the collect()
function:
xxxxxxxxxx
collect(r)
10-element Vector{Int64}: 1 2 3 4 5 6 7 8 9 10
To test whether a number is in a range, just use the in
keyword, which can also be written using ∈, entered as \in [TAB]:
xxxxxxxxxx
println(4 in r)
println(10 in r)
println(27 ∈ r)
println(-3 in r)
println(2.7 ∈ r)
true true false false false
We've seen how to make a range of integers going from 1 to 10. How do me make a range for only the even numbers between 1 and 10? What if we wanted to count downward from 10 to 1?
Here's how to make a range where we skip numbers - counting by 2 - which will give us the even numbers if we start at an even number:
xxxxxxxxxx
e = 2:2:10
collect(e)
5-element Vector{Int64}: 2 4 6 8 10
xxxxxxxxxx
println(4 ∈ e)
println(3 ∈ e)
true false
The syntax for creating a range that skips steps is start:step:stop
, and this can be used for counting backwards:
xxxxxxxxxx
b = 10:-1:1
collect(b)
10-element Vector{Int64}: 10 9 8 7 6 5 4 3 2 1
We can also use fractional steps:
xxxxxxxxxx
r1 = 1:0.5:10
collect(r1)
19-element Vector{Float64}: 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0
Something to be aware of is that with fractional steps, the ending value may or may not be in the range! For example:
xxxxxxxxxx
s = 1:0.5:10.1
collect(s)
19-element Vector{Float64}: 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0
xxxxxxxxxx
println(10.1 ∈ s)
false
For now, that's all we need to know about ranges. Now we're ready to discuss loops!
For-Loops¶
For-loops are ideal for repeating actions when the number of repetitions is known ahead of time.
The syntax of a for-loop that loops over the numbers in a range looks like this:
for i in range
statement1
statement2
...
end
The variable i
is called a loop control variable.
The loop is said to iterate over the elements of range
.
The statements inside the loop, after the for
statement but before the matching end
keyword, are called the body of the loop.
When control reaches the for i in range
statement, the variable i
is assigned the first value of range
. Then the statements in the loop body are executed, using whatever the value of i
is. At the end of the loop body, Julia checks whether there is another item in range
. If so, assign it to i
, otherwise control exits the loop.
As a flowchart, this process looks like this:
For our first example of a for-loop, here is how to print the numbers from 1 to 5 as well as their squares:
xxxxxxxxxx
for i in 1:5
println("The square of $(i) is $(i^2)");
end
The square of 1 is 1 The square of 2 is 4 The square of 3 is 9 The square of 4 is 16 The square of 5 is 25
For-loops are not limited to looping over ranges! As it goes, for loops can be used on other things, such as strings. Here's an example of looping over the characters of a string:
xxxxxxxxxx
msg = "Hear ye! Hear ye!"
for c in msg
println(c)
end
H e a r y e ! H e a r y e !
Notice that we did not need to first figure out the length of the string and access the elements by index. We could do it like this:
xxxxxxxxxx
msg = "Hear ye! Hear ye!"
len = length(msg)
# Assuming the string contains only ASCII characters:
for i in 1:len
println(i, " ", msg[i])
end
1 H 2 e 3 a 4 r 5 6 y 7 e 8 ! 9 10 H 11 e 12 a 13 r 14 15 y 16 e 17 !
Notice that in this loop body we have a bit more information than the body of the for c in msg
loop - we know the value of i
, index of the character in the string. What if we want to get both the index and the corresponding character? There's a function called enumerate()
that does just that:
xxxxxxxxxx
msg = "Hear ye! Hear ye!"
for (i, c) in enumerate(msg)
println("$i $c")
end
1 H 2 e 3 a 4 r 5 6 y 7 e 8 ! 9 10 H 11 e 12 a 13 r 14 15 y 16 e 17 !
Just as an experiment, let's apply the collect() function on the result of enumerate():
xxxxxxxxxx
msg = "Hear ye! Hear ye!"
e = enumerate(msg)
collect(e)
17-element Vector{Tuple{Int64, Char}}: (1, 'H') (2, 'e') (3, 'a') (4, 'r') (5, ' ') (6, 'y') (7, 'e') (8, '!') (9, ' ') (10, 'H') (11, 'e') (12, 'a') (13, 'r') (14, ' ') (15, 'y') (16, 'e') (17, '!')
As more data structures (arrays, dictionaries, etc.) are introduced, we'll see that we can use for-loops on some of them, too.
Break and Continue¶
Sometimes you will want to either exit a loop early, or skip to the bottom of the loop. That's what the break
and continue
statements do. Consider this needlessly complex example:
xxxxxxxxxx
for i in 0:10
if i % 2 == 0
continue
end
println(i)
end
1 3 5 7 9
Note the decision statement within that for-loop. More on that in a minute.
Here's what's going on:
- line 1 -
i
is given the value 0 - line 2 - is
i
even? Yes, so control follows the true branch and moves to line 3 - line 3 - the
continue
statement jumps us to the end of the loop (line 6), thus skipping the output statement on line 5 - control then moves back up to line 1 where
i
is given the value 1 - line 2 - is
i
even? No! - the
continue
statement is guarded by theif
statement, so control moves to line 5 - line 5 - the current value of
i
is printed - line 6 - we're back a line 6, so control moves back up to line 1 and the loop continues...
Because the println(i)
statement is skipped on each even value of i
, only the odd numbers in 1:10 are printed. That's what the continue
statement does - it skips the rest of the current iteration.
Let's try the same thing but use the break
statement instead:
xxxxxxxxxx
for i in 0:10
if i % 2 == 0
break
end
println(i)
end
There is no output! The first time through the loop, i
has value 1, which is even, so control reaches line 3, which breaks us out of the loop. There are no further iterations, and the println(i)
statement is never executed.
While-Loops¶
While-loops are used when we don't know how many times the loop is to be repeated. A perfect time for this is when reading text from a file, since we almost never know a file's exact character count! When we get to the post on file I/O, we'll see this in action.
While-loops make use of conditions (just like decision statements like we saw in the previous post):
while condition
statement1
statement2
...
end
The idea is that when control hits the while condition
statement, the condition is evaluated. If it is false, control will skip the loop body and exit the loop. But if condition
is true, then control will execute statement1
, statement2
, etc., then will return to the top of the loop.
Here's what this while-loop look like as a flowchart:
If the condition
is false right away, the while-loop will not execute its loop body even once! If you believe that this can never happen with for-loops, try an experiment using the range 5:1
.
The first for-loop we considered printed the numbers from 1 to 5 along with their squares:
for i in 1:5
println("The square of $(i) is $(i^2)");
end
Here's a while-loop that has the same output:
i = 1
while i <= 10
println("The square of $(i) is $(i^2)");
i += 1
end
The square of 1 is 1 The square of 2 is 4 The square of 3 is 9 The square of 4 is 16 The square of 5 is 25 The square of 6 is 36 The square of 7 is 49 The square of 8 is 64 The square of 9 is 81 The square of 10 is 100
Notice that we have to create a loop control variable outside the loop, that's line 1.
This, by the way, leads to a subtle difference between this while-loop and the earlier for-loop: the value of i
is available and can be, e.g., printed after the while-loop is done; with the for-loop, i
is not available for later use.
Also notice that we, the software developer, are responsible for incrementing the value of i
at line 5. If we don't increment i
, the condition will never be false and we would be in what is called an infinite loop!
To break out of an infinite loop, do one of the following:
- in Jupyter, click the "interrupt the kernel" button (which looks like a stop button)
- in the Julia REPL, press CTRL-C
- in VS Code, click in the output window and press CTRL-C
The break
and continue
statements can be used with while-loops, too:
i = 1
while true
println("The square of $(i) is $(i^2)");
i += 1
if i > 10
break
end
end
The square of 1 is 1 The square of 2 is 4 The square of 3 is 9 The square of 4 is 16 The square of 5 is 25 The square of 6 is 36 The square of 7 is 49 The square of 8 is 64 The square of 9 is 81 The square of 10 is 100
Nested Loops¶
A loop can be in the body of other loop. These are called nested loops. For example, here is how to print a multiplication table:
xxxxxxxxxx
for i = 1:3
for j = 1:3
println(i * j)
end
end
1 2 3 2 4 6 3 6 9
That didn't work out as expected! The source of the problem is that we want to print the numbers in the inner loop on one line, then skip to the next line once that inner loop is done. We'll also pad the output so that the columns line up. Here's how to do it, over a bigger range:
x
for i in 1:12
for j in 1:12
print(lpad(i * j, 4, " "))
end
println()
end
1 2 3 4 5 6 7 8 9 10 11 12 2 4 6 8 10 12 14 16 18 20 22 24 3 6 9 12 15 18 21 24 27 30 33 36 4 8 12 16 20 24 28 32 36 40 44 48 5 10 15 20 25 30 35 40 45 50 55 60 6 12 18 24 30 36 42 48 54 60 66 72 7 14 21 28 35 42 49 56 63 70 77 84 8 16 24 32 40 48 56 64 72 80 88 96 9 18 27 36 45 54 63 72 81 90 99 108 10 20 30 40 50 60 70 80 90 100 110 120 11 22 33 44 55 66 77 88 99 110 121 132 12 24 36 48 60 72 84 96 108 120 132 144
Much better!
Now that the code works, let's understand why it works... that isn't the way things are ordinarily done, but let's go with it.
- at the start,
i
is given the value of 1 on line 1 - at line 2,
j
is given the value of 1 - at line 3, we print the value of 1 * 1, padded on the left by spaces so that the length of the output is 4 characters
- control returns back to line 2, where
j
is given the value 2 - the whole process repeats, and at line 3 the value of 1 * 2 is printed
The net effect of lines 2-4 is to print the values 1 * 1, 1 * 2, 1 * 3, ..., 1 * 12 all on one line.
- After we exit the inner loop, a carriage return is printed, which puts subsequent output on the next line
- back at line 1,
i
is assigned the value 2 - now, lines 2-4 print the values 2 * 1, 2 * 2, 2 * 3, ..., 2 * 12 all on one line
And so on. To make this clear, let's go back to the previous, broken, example and print out the values of i
and j
separately as opposed to their product:
x
for i = 1:3
for j = 1:3
println("$i * $j")
end
end
1 * 1 1 * 2 1 * 3 2 * 1 2 * 2 2 * 3 3 * 1 3 * 2 3 * 3
Line 3 of this example (as well as line 3 in the multiplication table example) depends on both i
and j
.
Notice that i
does not appear in line 2. Here's what happens when we make the inner loop run j
times, instead of a constant number of times:
xxxxxxxxxx
for i = 1:10
for j = 1:i
print("*")
end
println()
end
* ** *** **** ***** ****** ******* ******** ********* **********
When for-loops are nested inside each other, they can be merged. For example, this
for i = 1:3
for j = 1:3
println("$i * $j")
end
end
can be rewritten as follows:
for i = 1:3, j = 1:3
println("$i * $j")
end
1 * 1 1 * 2 1 * 3 2 * 1 2 * 2 2 * 3 3 * 1 3 * 2 3 * 3
The output is the same. Returning to the multiplication table example, it takes a little extra work to get the formatting correct, but it can be done:
for i in 1:12, j in 1:12
print(lpad(i * j, 4, " "))
if j == 12
println()
end
end
1 2 3 4 5 6 7 8 9 10 11 12 2 4 6 8 10 12 14 16 18 20 22 24 3 6 9 12 15 18 21 24 27 30 33 36 4 8 12 16 20 24 28 32 36 40 44 48 5 10 15 20 25 30 35 40 45 50 55 60 6 12 18 24 30 36 42 48 54 60 66 72 7 14 21 28 35 42 49 56 63 70 77 84 8 16 24 32 40 48 56 64 72 80 88 96 9 18 27 36 45 54 63 72 81 90 99 108 10 20 30 40 50 60 70 80 90 100 110 120 11 22 33 44 55 66 77 88 99 110 121 132 12 24 36 48 60 72 84 96 108 120 132 144
Mixing Decision Statements With Loops¶
Decision statements can be placed in the body of a loop, and a loop can be in a branch of a decision statement.
Let's write some code that determines whether a number is prime.
A positive integer is prime if it is greater than 1 and the only positive numbers that go evenly into it are 1 and itself. So:
- 1 is not prime
- 2 is prime, since it is only divisible by 1 and 2
- 3 is prime, since it is only divisible by 1 and 3
- 4 is not prime, since 2 goes evenly into it.
- 5 is prime, since it is only divisible by 1 and 5
- 6 is not prime, since 2 goes evenly into it, as well as 3
And so on.
Let num
be the number we'll be testing for primality.
It seems like we would have to test all the positive integers less than num
for divisibility in order to determine that number is prime. Consider this, though:
- 2 is the only even prime, all other even numbers are not prime
- after checking whether
num
is greater than 2, we then need to only check odd n - this means we only need test the odd numbers less than
num
- further, we don't need to test all the numbers less than
num
- we just check all numbers less than - finally, once we've determined that a number goes evenly into
num
, we can break out of the loop!
xxxxxxxxxx
num = 23
flag = true
smallestFactor = 2
if num == 1
flag = false
elseif num == 2
flag = true
elseif num % 2 == 0
flag = false # Since num > 2 and is even
else
test = 3
while test <= sqrt(num) && flag
if num % test == 0
flag = false
smallestFactor = test
end
test += 2
end
end
flag ? println("$(num) is prime") : println("$(num) is not prime since $(smallestFactor) is a factor.")
23 is prime
Syntatically this is kind of complex: we have a decision statement (lines 15-18) nested inside a while loop (lines 14 - 20) and all of that is nested inside another decision statement (lines 6-21)!
Let's review this code line by line:
- 1 - Is
num
prime? That's what we're trying to determine! - 3 - flag will be true until we find a factor of
num
- 4 - if
num
is smallest factor greater than 1 should num be composite - 6-7 - 1 is not prime
- 8-9 - 2 is prime
- 10-11 - all even numbers greater than 2 are not prime
- 12 - if we're here,
num
is an odd number greater than 2 - 13 -
test
is the number we'll check to see if is a factor ofnum
Idea is to keep increasing test
until either it is a divisor of num
, or it is greater than
- 14 - the start of the loop
- 15 - does
test
divide evenly intonum
? - 16-17 - if so, the number is composite, so set flag to be false, and save
test
intosmallestFactor
- 19 - we update
test
for the next pass through the loop.
Notice we add 2 to test
, and not 1. This is because test starts out at 3 (which is odd), and we want to move to the next odd number.
After test
is updated, control goes up to the top of the loop (line 14) and checks the condition
- 23 - finally we display the result of the above calculations.
Conclusion¶
In this chapter we saw:
- the concept of ranges
- for-loops and while-loops
- infinite loops (and how to stop them!)
- nested loops
We also combined decision statements and loops to determine whether a number is prime.
In a later blog post, we'll see how to apply loops to arrays, external files, and etc.
No comments:
Post a Comment