# Control flow

## Logic

### Truth

In order to determine the flow of operations, conditions must be tested at each branching of a program's flow. A condition, in natural language, could be that "a note be part of a scale", or that "the sum of a sequence of durations fit a musical measure". The tests could be formulated by the questions : "is the note Eb part of the scale C Major ?", and "for a measure of common time, do the note durations of this sequence sum to a whole note ?". In the Boolean logic of programming with Python, there are only two possible results when testing a condition : `True` and `False`. Continuing with the first example, the note Eb cannot both be part of the scale and not be part of the scale. We could say that the statement : "the note Eb is part of the C Major scale" is `False`, or that the statement : "the note G is part of the C Major scale" is `True`.

If you check the type of `True` or of `False`, you will find that they are of the built-in type `bool` (for Boolean). They are part of a handful of built-in constants.

``````>>> type(True)
bool``````

### Relational operators

Objects can relate to one another in different ways. An object can be greater than, lesser than, or equivalent to another. The basic relational operators, in the same order, are `>`, `<`, and `==`. We can use these operators to define conditions based on the relation between one object and another. Some examples :

``````>>> 60 > 48
True``````

``````>>> True == False
False``````

To these basic relational operators Python adds `>=` (greater than or equivalent), `<=` (lesser than or equivalent), `!=` (not equivalent).

``````>>> 60 >= 48
True``````

``````>>> 60 <= 60
True``````

``````>>> True != False
True``````

### Boolean operators

Boolean (sometimes called propositional) logic deals with expressions (also called propositions) that evaluate to either `True` or `False`. It is the basis of the mathematical and philosophical disciplines of Logic. There exists a Boolean algebra that can be used to form compound conditional expressions and the basic operators of this algebra are available in Python. They are : `and` (conjunction), `or` (disjunction), and `not` (negation). The unary operator `not` negates the truth-value of a condition :

``````>>> not True
False``````

``````>>> not (60 >= 48)
False``````

For an `and` expression to evaluate to `True`, both of its operands must evaluate to `True` :

``````>>> True and False
False``````

``````>>> True and True
True``````

``````>>> (60 >= 48) and (60 < 48)
False``````

An `or` expression will evaluate to `True` if either of its two operands evaluates to `True` :

``````>>> True or False
True``````

``````>>> False or False
False``````

``````>>> (60 >= 48) or (60 < 48)
True``````

Short-circuit operators

The following tables summarize the results of the possible combinations of operand truth values for each of the two binary boolean operators. The operands are represented by the letters p and q.

#### `and` (Conjunction)

p q p and q
True True True
True False False
False True False
False False False

#### `or` (Disjunction)

p q p or q
True True True
True False True
False True True
False False False

## Presence and identity

### in

We can build a conditional expression that asks : "is this object a part of that container ?" by using the `in` keyword. For example, to see if a MIDI note belongs to a scale, we can do the following :

``````>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> (50 % 12) in scale
True``````

First, we defined a scale as a set of pitch classes. We then brought the MIDI note down to its pitch class value (by using the `% 12` operation) and checked if it was `in` the `scale` container.

### is

There exists another way (besides `==`) to evaluate something similar to equality in Python. An expression using the keyword `is` will evaluate to `True` if both of its operands are identical. Identical means that not only are they equivalent (which is what we verify when we use `==`), but that they share the same identity. So far as Python is concerned, things are identical when they occupy the same space in memory. We haven't yet acquired the tools needed to demonstrate an example of `is` yielding a result that is different from `==`, but we will start using `is` where appropriate. It is preferred as the more conventional way to build conditional expressions that compare an object to the `bool` or `None` types.

``````>>> pitches = None
>>> pitches is None
True
>>> pitches is not None
False``````

## Conditional

### if

`if` adds the question mark to the conditional, it asks the question. As is often the case in Python, the syntax is close to natural language. Given these assignments :

``````>>> durations = [1/4, 1/8, 1/8, 1/2]
>>> measure_duration = 4/4``````

An `if` condition can be expressed like this :

``````>>> if sum(durations) == measure_duration:
print('The durations fit and fill the measure.')``````

In the example above, we are comparing for equivalence (using the relational operator `==`) the value of `sum(durations)`, which gives `1.0`, with the value of `measure_duration`, which is also `1.0`. The condition is met and the `print()` expression is executed.

If the condition was not met, the `print()` expression would not have been executed. We can catch a case that doesn't pass the condition by using `else` :

``````>>> if sum(durations) == measure_duration:
print('The durations fit and fill the measure.')
else:
print('The durations do not fit the measure.')``````

If the condition is not met, the program will skip the first and instead execute the second `print()` expression.

We can further add to the construct by checking for more than one condition before passing to the default `else` clause by using `elif` (short for else, if) :

``````>>> if sum(durations) == measure_duration:
print('The durations fit and fill the measure.')
elif sum(durations) > measure_duration:
print('The durations excede the measure.')
elif sum(durations) < measure_duration:
print("The durations don't fill the measure.")
else:
print('This will never execute.')``````

It is legitimate to claim that the `else` clause above, being useless because `sum(durations)` must be either `==`, `>`, or `<` to `measure_duration`, should just be dropped and the final `elif` should be replaced by an `else`. It's fine to do so, but the final condition will have to be guessed by someone (including yourself in the future) reading your code. In this case, there's only a single expression between each of the conditions and it is trivial to imply the third condition if it isn't explicitly stated, but in the case of more extensive code it quickly becomes much less trivial. From the Zen of Python remember : explicit is better than implicit. It is better for readability to keep both the last `elif` as well as the final `else` clause.

## Iteration

If there's one thing computers are good it, it's doing the same thing over and over again.

### for, in

We can iterate over the items of a container using the `for` item `in` iterable construct. We can use this, for example, to transpose a pitch sequence like so :

``````>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> transposed = []
>>> for pitch in pitches:
transposed.append(pitch + 5)
>>> print(transposed)
[48, 50, 52, 53, 55, 56, 58, 56, 55, 56]``````

We began by creating a list of pitches, which we assigned to the variable `pitches`. We then created an empty list which we assigned to `transposed`. The next two lines show the use of the `for` item `in` iterable construct. We chose the name `pitch` to represent the item. This step is like assigning a variable which will be available in the scope of the construct, we could have chosen any name. The iterable in our case was the list `pitches`. Within the scope of the construct we have a single expression, which appends the transposed pitch to the `transposed` list we prepared on the second line of this example. The expressions in the scope are executed once for each of the items in the iterable container.

### break

We can stop iterating using `break`. If, instead of quantizing pitches in the example above, we wanted to verify that the pitches were all part of the scale, we could iterate through the sequence of pitches until we find a pitch that is not part of the scale, print a warning, and stop iterating.

``````>>> for pitch in pitches:
if (pitch % 12) not in scale:
print('Not all pitches are part of the scale.')
break``````

### continue

Conversely, we can explicitly step forward to the next item in the iteration, skipping subsequent expressions, by using `continue`.

``````>>> for pitch in pitches:
if (pitch % 12) in scale:
continue
print('A pitch was found that doesn't belong to the scale.')``````

This will print the warning each time a pitch that is not a member of the scale is found.

### Comprehensions

The `for` item `in` iterable construct can be used many different ways, but the case demonstrated in the example given above is so common that there is a syntactical shortcut to achieve it. Creating a new list from the modified items of an iterable can be achieved using a list comprehension like so :

``>>> transposed = [pitch + 5 for pitch in pitches]``

The list comprehension construct `[`expression `for` item `in` iterable `]` results in a new list created by applying an expression to each item for all the items in an iterable.

The same syntactical shortcut exists for dictionaries. Here is an example of a dictionary comprehension :

dict comp example

Notice that tuple unpacking also works within the comprehension !

Comprehensions can be further expanded with conditionals. The following example will transpose only those pitches from the initial `pitches` list that are part of the `scale` :

``````>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> transposed = [pitch + 5 for pitch in pitches if (pitch % 12) in scale]
>>> print(transposed)
[48, 50, 52, 53, 55, 58, 55]``````

Finally, comprehensions can be nested :

``````>>> intervals = [0, 4, 7]
>>> pitches = [60, 65, 67, 60]
>>> chords = [[pitch + interval for interval in intervals] for pitch in pitches]
>>> print(chords)
[[60, 64, 67], [65, 69, 72], [67, 71, 74], [60, 64, 67]]``````

The innermost comprehension is evaluated first, so in the example above : from the first pitch, a chord is created using each of the intervals sequentially, then the chord is added to `chords`, then another pitch is taken and another chord is created and added to `chords`, and so on until there are no more pitches to take. The equivalent code using the `for` item `in` iterable construct looks like this :

``````>>> chords = []
>>> for pitch in pitches:
chord = []
for interval in intervals:
chord.append(pitch + interval)
chords.append(chord)``````

The expressive power of comprehensions should, by now, have seriously impressed you. You'll probably want to place them everywhere in your code. If you find you're asking yourself the question "should I use a comprehension ?" I offer simple advice : use comprehensions only if the expression is dead simple. Your code should be easy to read and understand. Comprehensions can quickly become incomprehensible.

### while

Here be dragons. If there's one thing computers are good it, it's doing the same thing over and over again (again), and they won't stop doing it until something tells them to stop. The `while` loop does exactly that, it does something over and over again as long as a given condition evaluates to `True`, and until the condition evaluates to `False`. If the condition evaluates to `False` to begin with, the expressions in the scope of the `while` loop are never executed. The following example uses a `while` loop to add semitones to a pitch until the resulting pitch is part of a scale :

``````>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> quantized = []
>>> for pitch in pitches:
while (pitch % 12) not in scale:
pitch = pitch + 1
quantized.append(pitch)
>>> print(quantized)
[43, 45, 47, 48, 50, 52, 53, 52, 50, 52]``````

When a `while` loop is used, there must to be a case in the scope that will cause the loop to stop, a terminating condition. The example above is a little risky, the loop will only stop once a pitch is found that fits the scale. We can be certain that if `scale` contains any integer smaller than 12, that by adding 1 repeatedly to any integer % 12 we will find a match. But if there is no integer smaller than 12 in the scale, we will never find a match and the loop will go on adding 1 for ever. In the present case, we can explicitly secure the loop by limiting the number of attempts to fit the pitch to the scale. Assuming we are operating in a 12 divisions of the octave system, we can be certain that if the scale is well constructed, that we reach a quantized pitch within 12 attempts (we will have tried all pitch classes in the system). Here is the application of this idea :

``````>>> for pitch in pitches:
max_attempts = 12
attempts = 0
while ((pitch % 12) not in scale) and (attempts < max_attempts):
pitch = pitch + 1
attempts = attempts + 1
quantized.append(pitch)``````

## Recursion

Recursively create an overtone series.

Recursively reduce a rhythmic structure

## Exercises

1. Write a function that checks if a list of durations fits a measure.

``````def check_durations(durations, measure_duration):
"""Checks if a list of durations fits a measure.

Durations should be represented as a tuple. A quarter note is (1, 4),
an eighth is (1, 8), a dotted quarter is (3, 8), etc. A measure of common
time is (4, 4), a 6/8 measure is (6, 8), etc.

A duration tuple is returned, with a negative numerator if the durations do
not fill the measure, a positive numerator if the durations exceed the
measure, and a numerator of 0 if the durations fit.

:param durations: Durations as described above.
:type durations: list of tuple
:param measure_duration: Measure duration tuple as described above.
:type measure_duration: tuple

:rtype: tuple

**Examples**

>>> check_durations([(1, 4), (1, 8), (1, 8), (1, 2)], (4, 4))
(0, 8)
>>> check_durations([(1, 4), (1, 8)], (3, 4))
(-3, 8)
>>> check_durations([(1, 2), (3, 8)], (3, 4))
(1, 8)
>>> check_durations([(1, 2), (1, 1)], (4, 4))
(1, 2)
"""