Last updated on
Specifications
Reasoning about your code requires specifying what it is supposed to do?
By popular request, here are extra exercises focused on the contents of the SE lectures. These are intended to complement the exercises already included in the slides.
As usual, ⭐️ indicates the most important exercises and questions; 🔥, the most challenging ones; and 🔜, the ones that are useful to prepare for future lectures.
You do not need to complete all exercises to succeed in this class, and you do not need to do all exercises in the order they are written.
We strongly encourage you to solve the exercises on paper first, in groups. After completing a first draft on paper, you may want to check your solutions on your computer. To do so, you can download the scaffold code (ZIP).
In Scala, it’s common to use require
and ensuring
to specify pre- and post-conditions for functions, respectively.
The require
method checks a given condition (usually an input validation) and throws an IllegalArgumentException
if the condition is not met. On the other hand, ensuring
is used to validate the result of a function. It takes a predicate that the result must satisfy, and if not, an assertion error is thrown.
For example:
val eps = 0.00001f
def sqrt(x: Double): Double = {
require(x >= 0)
Math.sqrt(x)
} ensuring (res =>
(x - res * res) <= eps && (x - res * res) >= -eps
)
This exercise set will make writing such specs second nature!
Debugging with tests and specs
Bad combinatorics ⭐️
Consider the following incorrect function:
/** Construct all subsets of `s` of size `k` (INCORRECT!) */
def badCombinations[T](set: Set[T], k: Int): Set[Set[T]] = {
if k == 0 then Set(Set())
if set.isEmpty then Set()
else
for
item <- set
rest = set - item
subset <- Iterable.concat(
for s <- badCombinations(rest, k - 1) yield rest + item,
for s <- badCombinations(rest, k) yield rest
)
yield subset
}
src/main/scala/specs/Tracing.scala
-
Read this function carefully.
-
Which inputs are meaningful for this function? Write a
require
clause to rule out meaningless arguments. Check that yourrequire
clause works by calling this function with invalid arguments in a worksheet and confirming that it raises an exception. -
Unit tests are a great way to ensure that corner cases are correctly handled. Create a new test suite class in
src/test/scala/specs/SpecsSuite.scala
(you can consult a previous-week exercise set or lab for inspiration, or the munit documentation), then write a unit test that asserts that this function computes the right result when called with an empty set as input (useassertEquals
andSet()
). -
Make sure that your test runs and reports a failure (it should, since this function is incorrect). You can use
sbt testOnly -- *your-test-name*
to run your test. -
Find the bug that leads to an empty set being returned, and confirm that the unit test now passes.
-
What does the documentation of this function promise? Write an
ensuring
clause to capture this formally. Is this a complete specification? (A specification is complete if any function that respects it is a correct implementation of the original requirements — here, the documentation string). -
Find an input for which this function violates the postcondition that you just wrote. Confirm your conjecture by calling this function in a worksheet and confirming that the
ensuring
clause throws an exception. -
Correct the remain bugs in this function. You may find it useful to add tracing instructions to observe its behavior.
-
Bonus question: Is your corrected function efficient? Look carefully at the work it does on a simple example. Do you spot any redundant work?
Bad transactions
The following code defines a class BankAccount
and associated operations deposit
, withdraw
and transfer
:
protected class BankAccount(private var _balance: Double):
import AccOpResult.*
def balance = _balance
private def updateBalance(amount: Double): AccOpResult =
val oldBalance = balance
_balance = amount
Ok(oldBalance)
/** Deposits the specified amount into the bank account.
*
* @param amount
* The amount to be deposited. Must be non-negative.
*/
def deposit(amount: Double): AccOpResult = {
updateBalance(balance + amount)
}
/** Withdraws the specified amount from the bank account.
*
* @param amount
* The amount to be withdrawn. Must be non-negative.
*/
def withdraw(amount: Double): AccOpResult = {
if balance >= amount then
updateBalance(balance - amount)
else
InsufficientFund(balance)
}
/** Transfers the specified amount from this bank account to `that` bank
* account.
*
* @param amount
* The amount to be transferred. Must be non-negative.
*/
def transfer(that: BankAccount, amount: Double): (AccOpResult, AccOpResult) = {
if this.balance >= amount then
(this.withdraw(amount), that.deposit(amount))
else
(InsufficientFund(this.balance), Ok(that.balance))
}
src/main/scala/specs/BankAccount.scala
Type AccOpResult
is used to memorize a “screenshot” of the balance before the operation and whether the operation is successful:
enum AccOpResult:
case Ok(oldBalance: Double)
case InsufficientFund(oldBalance: Double)
src/main/scala/specs/BankAccount.scala
- Write the preconditions and postconditions for function
deposit
,withdraw
andtransfer
usingrequire
andensuring
. - Can you identify the bugs in the code that violate these conditions?
Translating specs from English
Ambiguous specs
The following statements are ambiguous. For each of them, explain the ambiguity (either in words, or by giving at least two different, unambiguous expressions in Scala code that are compatible with the original sentence):
- “Access to the system is restricted to senior engineers and developers.”
- “The argument to this function must be a supertype of a serializable type that implements
IsFinite
” - “Only finite
Double
lists are supported.” - “Any sibling of a node that makes a request must be ready to process answers.”
- “Objects passed to this method must be unlockable.”
A particularly good source of examples of ambiguities is newspapers; just look up “Ambiguous newspaper headlines” on Google.
Translating simple specs ⭐️
Translate the following English statements to mathematical statements or to Scala code. Aim to be as succinct and clear as possible; do not optimize for performance. For some statement, it may not be possible to write Scala code (which ones?).
- The list of integers
l
is sorted in ascending order - All values in map
m1
are keys in mapm2
. - Even numbers are always immediately followed by odd numbers in list
l
. - There are no negative numbers in the output of function
f
. - Functions
f
andg
compute the same output when given a positive number. - List
l1
is a permutation of listl2
. - Number
p
is prime.
Paths and cycles
Translate the following English statements to Scala code:
A path is a nonempty sequence of edges (pairs of points) such that the endpoint of each edge is the starting point of the next edge.
A cycle is a path whose starting point equals its endpoint.
Course policies
This course has a reasonably complex grading policy: one lab is dropped, we accept medical reasons for skipping a lab, callbacks combine with labs, we have a midterm and a final, etc.
Translate the grading policy of this course (found in Overall Grade) into a Scala function, OverallGrade
. First, start with the easy version:
def OverallGrade(labScore: Double, midtermScore: Double, finalScore: Double): Double =
???
src/main/scala/specs/Specs.scala
🔥 Then, try the full grading policy.
Finding incompleteness in specs ⭐️
The following ensuring
specifications are incomplete: they do not detect all incorrect behaviors.
For each one, replace the existing function body with an incorrect implementation that still obeys the ensuring
clause, then find an input that proves that your modified function is incorrect and confirm that the ensuring
clause is incomplete by checking that it does not raise an assertion.
-
def filterWithIncompleteSpec[T](l: List[T], p: T => Boolean) = { l.filter(p) } ensuring (res => res.forall(p))
src/main/scala/specs/Specs.scala
-
def mapWithIncompleteSpec[T, Q](ts: List[T])(f: T => Q) = { ts.map(f) } ensuring (qs => qs.length == ts.length && qs.forall(q => ts.exists(t => f(t) == q)) )
src/main/scala/specs/Specs.scala
-
def flattenWithIncompleteSpec[T](tss: List[List[T]]) = { tss.flatten } ensuring (ts => ts.length == tss.map(_.length).sum && tss.forall(ts.containsSlice(_)) )
src/main/scala/specs/Specs.scala