Thursday, April 1, 2021

Loops

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:

In [1]:
Out[1]:
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:

In [2]:
Out[2]:
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]:

In [3]:
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:

In [4]:
Out[4]:
5-element Vector{Int64}:
  2
  4
  6
  8
 10
In [5]:
true
false

The syntax for creating a range that skips steps is start:step:stop, and this can be used for counting backwards:

In [6]:
Out[6]:
10-element Vector{Int64}:
 10
  9
  8
  7
  6
  5
  4
  3
  2
  1

We can also use fractional steps:

In [7]:
Out[7]:
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:

In [8]:
Out[8]:
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
In [9]:
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:

In [10]:
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:

In [11]:
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:

In [12]:
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:

In [13]:
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():

In [14]:
Out[14]:
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:

In [15]:
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 the if 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:

In [16]:

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:

In [17]:
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:

In [18]:
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:

In [19]:
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:

In [20]:
   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:

In [21]:
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:

In [22]:
*
**
***
****
*****
******
*******
********
*********
**********

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:

In [23]:
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:

In [24]:
   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 num
  • finally, once we've determined that a number goes evenly into num, we can break out of the loop!
In [25]:
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 of num

Idea is to keep increasing test until either it is a divisor of num, or it is greater than num

  • 14 - the start of the loop
  • 15 - does test divide evenly into num?
  • 16-17 - if so, the number is composite, so set flag to be false, and save test into smallestFactor
  • 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