Lesson 05 / 24
Operators
Introduces arithmetic, comparison and range operators; measures that the right-hand side of `||`, `&&` and the ternary operator may never be evaluated.
Published Updated
In this lesson
- Read the results of arithmetic, comparison and range operators
- Show when the right-hand side of the `||` and `&&` operators is not evaluated
- Explain that short-circuit evaluation lives in the parameter type, not in the operator
Contents
The interpolation of a string was rebuilt on every use. An operator also joins two expressions; the question is this: are both of them evaluated? In an earlier course (M10/K03) “how many times did it run” was a number; this course asks for a moment, and the most striking moment among operators is never.
Arithmetic, Comparison and Range
The arithmetic operators are +, -, *, / and %. Dividing two Int values gives an Int: 7 / 2 is 3 and 7 % 2 is the remainder of the division, 1; 7.0 / 2 gives 3.5. Comparison operators such as <, > and == return a Bool. Range operators build a sequence of values: 1...4 includes both ends, 1..<4 leaves the upper end out. contains tells whether a range holds a value.
Operator precedence is like in mathematics: multiplication and division come before addition, and parentheses change the order. Because comparison operators return a Bool, they can be an if condition directly; we will use this in the next lesson. == asks whether two values are equal, while a single = assigns, and mixing the two up is a common mistake. Ranges are values too: 1...4 can be assigned to a constant, walked in a loop or turned into an array with Array(...).
The measuring tool is again the same helper file. Each Probe counts how many times the expression it wraps is evaluated and how many times it is used; the relation between the two gives the moment.
// Probe.swift — records when an expression is evaluated
final class Probe {
let label: String
private(set) var evaluations = 0
private(set) var uses = 0
private var evaluatedBeforeUse = false
init(_ label: String) {
self.label = label
}
func eval<T>(_ value: T) -> T {
evaluations += 1
if uses == 0 { evaluatedBeforeUse = true }
return value
}
func use<T>(_ access: () -> T) -> T {
uses += 1
return access()
}
var moment: String {
if evaluations == 0 { return "never" }
if evaluations > 1 { return "on-every-use" }
return evaluatedBeforeUse ? "immediately" : "on-first-use"
}
func report() {
print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
}
}
func tally(_ probes: [Probe]) {
for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
let count = probes.filter { $0.moment == moment }.count
print("\(moment): \(count)")
}
}
Now the measurement block. Each line shows a result, then three Probe objects print their own reports. While reading, look at this: first the results of the operators, then the three expressions whose right-hand side is measured. Because isOpen is true, the right side of || is never needed; the same goes for the branch of the ternary operator (condition ? a : b) that is not chosen.
// main.swift — results of operators and their right-hand side
print(7 / 2, 7 % 2, 7.0 / 2)
print((1...4).contains(4), (1..<4).contains(4))
print(Array(1..<4))
let orProbe = Probe("or-operator-right")
let andProbe = Probe("and-operator-right-true")
let ternaryProbe = Probe("ternary-branch")
let isOpen = true
_ = orProbe.use { isOpen || orProbe.eval(true) }
_ = orProbe.use { isOpen || orProbe.eval(true) }
_ = andProbe.use { isOpen && andProbe.eval(true) }
_ = andProbe.use { isOpen && andProbe.eval(true) }
_ = ternaryProbe.use { isOpen ? 1 : ternaryProbe.eval(2) }
_ = ternaryProbe.use { isOpen ? 1 : ternaryProbe.eval(2) }
let allProbes = [orProbe, andProbe, ternaryProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
3 1 3.5 true false [1, 2, 3] or-operator-right: 0 evaluations, 2 uses -> never and-operator-right-true: 2 evaluations, 2 uses -> on-every-use ternary-branch: 0 evaluations, 2 uses -> never immediately: 0 on-first-use: 0 on-every-use: 1 never: 2
or-operator-right showed zero evaluations in two uses: while isOpen is true, || does not look at the right-hand side. The branch of the ternary operator that is not chosen is the same: ternary-branch is never in two uses. &&, on the other hand, moves on to the right because the left side is true, and the right-hand side is evaluated again on every use. Three lines, two results: the same right-hand expression is evaluated either never or every time, depending on the left side of the operator.
LB32. isOpen is a constant; the evaluation of the right-hand side for another value of the left side was not measured in this lesson.
LB33. The results of 7 / 2 and 7 % 2 are seen in the output of this block; other operator precedences were not measured.
Range Operators
Swift offers two range operators: the closed range 1...3 includes both ends, the half-open range 1..<3 leaves the upper end out. Ranges can be used in a loop, in slicing or in a comparison, and can be turned into an array with Array(...). The end values of a range are ordinary expressions; the operator only joins these two ends.
The block below builds two ranges. The upper end of the closed range comes from one Probe, and the upper end of the half-open range from another. Each range is read twice: once as count, once as an array. Then the contents of the two ranges are printed.
// Range.swift — closed and half-open range
let closedProbe = Probe("closed-range")
let halfProbe = Probe("half-open-range")
let closed = 1...closedProbe.eval(3)
let half = 1..<halfProbe.eval(3)
_ = closedProbe.use { closed.count }
_ = closedProbe.use { Array(closed) }
_ = halfProbe.use { half.count }
_ = halfProbe.use { Array(half) }
print(Array(closed), Array(half))
print(closed.contains(3), half.contains(3))
closedProbe.report()
halfProbe.report()
[1, 2, 3] [1, 2] true false closed-range: 1 evaluations, 2 uses -> immediately half-open-range: 1 evaluations, 2 uses -> immediately
The closed range gave [1, 2, 3] and the half-open range gave [1, 2]; the same upper end 3 stayed inside in one place and outside in the other. The contains(3) line confirms this. Both Probe objects showed a single evaluation: the upper end was computed once while the range was being built, and the later reads did not compute it again. Because a range is a value, the expression that computes its end runs on the definition line; the reads use the ready value.
The safety of range operators comes from here: the end expression does not short-circuit, all of it is computed immediately. This differs from the && and || operators we saw earlier, because their right side depended on the condition.
LB34. Ranges were measured with integer ends and Int values; the behavior of decimal ranges and of ranges that skip steps with stride is outside this lesson.
Compound Assignment and Operator Precedence
Compound assignment operators read a variable and write the new value to the same place: score += 3 and score = score + 3 are for the same job. -=, *= and /= follow the same pattern. Operators also have an order of precedence: multiplication works before addition, so 2 + 3 * 4 gives 14; parentheses change this order and (2 + 3) * 4 gives 20.
What is to be measured is that the right-hand side of a compound assignment is evaluated again each time it runs. The block below increases a score variable twice. Each increase includes a multiplier that comes from a Probe. Both results of precedence are printed in the same block.
// Compound.swift — compound assignment and precedence
let stepProbe = Probe("step-multiplier")
var score = 2
_ = stepProbe.use { score += stepProbe.eval(3) * 2 }
_ = stepProbe.use { score += stepProbe.eval(3) * 2 }
print(score)
print(2 + 3 * 4, (2 + 3) * 4)
stepProbe.report()
14 14 20 step-multiplier: 2 evaluations, 2 uses -> on-every-use
score started at 2 and became 14 after two increases; each increase added 6, which is 3 * 2. The expression 2 + 3 * 4 came out as 14 and the parenthesized form as 20. The measurement line showed two evaluations: the right-hand side of a compound assignment is computed again every time it runs. This is why the moment is on-every-use; += was written once but ran twice.
You can read compound assignment as an abbreviation, but the right-hand side of the abbreviation is an expression that is evaluated again. If the right-hand side is a call with side effects, you should expect this on every run of the assignment.
LB35. Compound assignment was measured with Int; the behavior of += on decimal numbers or strings is outside this lesson.
Division and Remainder
Integer division truncates the result to an integer: 7 / 2 gives 3, not 3.5. The % operator returns the remainder of the division: 7 % 2 gives 1. With negative numbers the sign of the remainder follows the sign of the dividend: -7 % 3 comes out as -1. If you want a fractional result, you write both sides as decimals: 7.0 / 2 gives 3.5. Dividing by zero stops the program; the lesson does not run it.
The block below writes a function that uses an integer both as the dividend and for the divisor. The divisor is read twice through a Probe: once for the quotient, once for the remainder. The function is called with one positive and one negative number.
// Division.swift — division and remainder
func divide(_ number: Int, probe: Probe) -> (Int, Int) {
(number / probe.eval(2), number % probe.eval(2))
}
let divisorProbe = Probe("divisor")
print(divisorProbe.use { divide(7, probe: divisorProbe) })
print(divisorProbe.use { divide(-7, probe: divisorProbe) })
print(7.0 / 2, -7 % 3)
divisorProbe.report()
(3, 1) (-3, -1) 3.5 -1 divisor: 4 evaluations, 2 uses -> on-every-use
divide(7) gave (3, 1) and divide(-7) gave (-3, -1): the quotient was truncated toward zero and the remainder took the sign of the dividend. The decimal division came out as 3.5 and -7 % 3 as -1. There are four evaluations in the measurement line, because each call writes the divisor expression twice. So the two calls brought four evaluations and the moment reads as on-every-use.
There is also a small trap this measurement shows: writing the same expression in two places means two evaluations. Computing the value once and taking it into a constant both lowers the count and simplifies the reading.
LB36. Division and remainder were measured with Int; the stopping cases such as division by zero and Int.min / -1 are not run in this lesson.
The Ternary Conditional Operator
The ternary operator in the form condition ? a : b picks one of two values according to the condition. If the condition is true, a is the result, otherwise b. The operator has three operands and their order is always the same: first the condition, then the “if true” expression, then the “if false” expression. It is the short form of a small if-else that produces a value.
The rule to be measured here is that the branch that is not chosen never runs. The block below ties each of the two branches to a separate Probe and runs the operator twice with a true condition.
// Ternary.swift — ternary conditional operator
let yesProbe = Probe("true-branch")
let noProbe = Probe("false-branch")
func choose(_ flag: Bool, yes: Probe, no: Probe) -> Int {
flag ? yes.eval(10) : no.eval(20)
}
_ = yesProbe.use { choose(true, yes: yesProbe, no: noProbe) }
_ = noProbe.use { choose(true, yes: yesProbe, no: noProbe) }
print(choose(true, yes: yesProbe, no: noProbe))
yesProbe.report()
noProbe.report()
10 true-branch: 3 evaluations, 1 uses -> on-every-use false-branch: 0 evaluations, 1 uses -> never
choose(true) gave 10 on every call. true-branch showed three evaluations: the two use calls and the call inside the last print, all three went to the true branch. false-branch, on the other hand, came out never with zero evaluations, because it was not chosen in any call. When the condition is true, the expression of the false branch is compiled but never run.
This rests on the same rule as && and ||: when the result is known, the remaining expression is not evaluated. When you put a call with side effects in a branch of the ternary operator, you know that call will run only when that branch is chosen.
Because the ternary operator is an expression, it produces a value; so it can be assigned directly to a constant or given to a function as an argument. When the condition grows long or the branches get complicated, if-else and a separate constant are the way to write the same rule more readably.
If a branch has no side effect, its not being chosen goes unnoticed; if it does, for example if it increases a counter or changes a record, knowing that this effect never happens in the branch that is not chosen prevents a mistake. The measurement showed exactly this difference with a number.
LB37. The ternary operator was measured with two Int branches; the readability of nested ternary operators is outside this lesson.
The Limiting Measurement
Short-circuit evaluation comes from the || and && operators taking their second parameter with @autoclosure. An operator is a function that takes its second parameter with @autoclosure; if the right-hand side is not needed, it is never called. A function that does the same job but takes its second parameter as an ordinary Bool evaluates the right-hand side before the call. Below, these two signatures stand side by side. We call both of them twice with a false left side; so the right-hand side does not need to be evaluated, and the difference comes only from the signature.
// main.swift — type of the right side: Bool and autoclosure
func both(_ left: Bool, _ right: Bool) -> Bool { left && right }
func lazyBoth(_ left: Bool, _ right: @autoclosure () -> Bool) -> Bool { left && right() }
let eagerProbe = Probe("both-right")
let autoProbe = Probe("lazyboth-right")
_ = eagerProbe.use { both(false, eagerProbe.eval(true)) }
_ = eagerProbe.use { both(false, eagerProbe.eval(true)) }
_ = autoProbe.use { lazyBoth(false, autoProbe.eval(true)) }
_ = autoProbe.use { lazyBoth(false, autoProbe.eval(true)) }
let allProbes = [eagerProbe, autoProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
both-right: 2 evaluations, 2 uses -> on-every-use lazyboth-right: 0 evaluations, 2 uses -> never immediately: 0 on-first-use: 0 on-every-use: 1 never: 1
The bodies of the two functions are the same: left && right. both evaluates the right-hand side before the call, even though the body does not use it because left is false; the rule says on-every-use. lazyBoth wraps the right-hand side in a closure and the body never touches it: never. What makes the difference is not the operator but the parameter type.
LB38. @autoclosure was measured in this lesson only as a signature that defers the right-hand side; closure details are left to the closures lesson.
The practical result learned here is this: if there is a call with side effects on the right-hand side of an expression, assuming that the call will run unconditionally is wrong. Conditions built with && and || are read in order, and if the left-hand expression has decided the result, the right-hand one never runs. If you want a computation to be done in every case, take it into a constant outside the operator and use the result in the operator.
Summary
/gives anIntfor twoIntvalues and%gives the remainder;...and..<build ranges.- The right-hand side of
||,&&and the ternary operator is never evaluated if it is not needed. - Short-circuit lives not in the operator but in the second parameter being an
@autoclosure. - If you write the same body without
@autoclosure, the right-hand side is evaluated on every call. - The moment of this lesson: never (the right side of the
||operator when the left side is true).
Next Step
There is another place where an expression is never evaluated: the branches of a switch after the first match. The next lesson looks at control flow, conditions and loops.
To keep your progress and take notes, Log in
My notes
Log in to take notes.