Last updated on
Debugging
The best way to improve your debugging skills is to apply the debugging guide to your own code in the labs. For more focused training, however, we provide a few exercises below. Happy hacking!
These exercises cover the first two debugging lessons: you won’t be able to complete the tracing exercises before the second lecture!
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.
It may sound surprising for a debugging exercise set, but even here, we strongly encourage you to think about the exercises on paper first, in groups. Only then should you start working with the actual code. For that, you can download the scaffold code (ZIP).
Encoding issues ⭐️
Prof. Pit-Claudel recently received a spam email with the following title:
Re: Design Query for √âcole polytechnique f√(c)d√(c)rale de ...
Explain what happened.
Debugging warnings
When Prof. Pit-Claudel demoed sbt new
in class, the following appeared on screen:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
What happened?
Debugging with the substitution model ⭐️
In case you forgot how to debug with the substitution model, refer to the week 1 debrief!
Lists
removeDuplicates
-
removeDuplicates
takes a list and produces a new list with all duplicated elements removed, keeping only the first one. For simplicity, we use1 :: 2 :: 3 :: IntNil
to denoteIntCons(1, IntCons(2, IntCons(3, IntNil)))
. For example, for a list1 :: 2 :: 4 :: 2 :: 3 :: 3 :: IntNil
, the second2
and3
are duplicated elements, so the returned list should be1 :: 2 :: 4 :: 3 :: IntNil
.The following is an (incorrect) implementation of
removeDuplicates
, using a functioncontains(l, x)
that checks whether listl
contains valuex
:def removeDuplicates(l: IntList): IntList = l match case IntNil() => IntNil() case IntCons(hd, tl) => if contains(tl, hd) then removeDuplicates(tl) else IntCons(hd, removeDuplicates(tl))
src/main/scala/debugging/ListOps.scala
-
What is the expected return value of this program for the input
1 :: 2 :: 1 :: IntNil
? -
Write down, step by step, the evaluation of
removeDuplicates(IntCons(1, IntCons(2, IntCons(1, IntNil()))))
, using the substitution model. You may assume thatcontains
correctly computes its result and substitute calls to it in a single step.(Consider using an erasable surface, such as a whiteboard, tablet, or writing slate, to reduce repetition).
Where does the computation diverge from the expected result?
-
Fix the implementation.
-
Binary search trees
isBST
A Binary Search Tree (BST) is a binary tree such that the value carried by each node is greater than all values in the left subtree and smaller than all values in the right subtree. For example, the following is a binary search tree:
8
/ \
3 10
/ \ \
1 6 14
-
Can a binary search tree contain duplicate values?
-
Is the following tree a BST?
val t0 = IntBranch(5, IntBranch(2, IntEmptyTree(), IntEmptyTree()), IntBranch(10, IntBranch(4, IntEmptyTree(), IntEmptyTree()), IntEmptyTree())))
-
The function
isBST
takes a binary tree and returns true if the tree is a binary search tree, false otherwise. Here is one candidate implementation of it:def isBST(t: IntTree): Boolean = t match case IntEmptyTree() => true case IntBranch(v, l, r) => (l == IntEmptyTree() || v > l.value) && (r == IntEmptyTree() || v < r.value) && isBST(l) && isBST(r)
src/main/scala/debugging/TreeOps.scala
-
There is something wrong with this implementation. But what? Using the substitution method, show what
isBST(t0)
evaluates to (t0
refers to the tree above):For succinctness, we recommend writing
IntEmptyTree
asε
,IntBranch(v, l, r)
as[v, l, r]
, andIntBranch(v, IntEmptyTree, IntEmptyTree)
as[v]
, so that this input is written as follows:isBST([5 [2] [10 [4] ε]])
-
-
Fix the implementation.
insert
Binary search trees are convenient to represent sets of values. To use them in this way, e need a contains
function and an insert
function.
Let’s focus on the insert
function. insert
takes a binary search tree representing a set of values $V$ and a new value $v_1$, and returns a new binary search tree representing the set $V \cup {v_1}$.
The following is an attempt at implementing insert
:
def insert(t: IntTree, v1: Int): IntTree =
t match
case IntEmptyTree() => IntBranch(v1, IntEmptyTree(), IntEmptyTree())
case IntBranch(v0, l, r) =>
if v1 < v0 then IntBranch(v0, insert(l, v1), r)
else IntBranch(v0, l, insert(r, v1))
src/main/scala/debugging/TreeOps.scala
-
Find a counterexample: an binary search tree
t
and an inputv1
such thatinsert(t, v1)
produces an incorrect result. -
Fix the implementation.
Debugging expressions with side effects 🔜
We will study stateful code in depth in week 10. Since some of you have already started using var
s, however, here is an exercise on this topic.
Pure expressions can be evaluated in any order. Given def f(x: Int) = x + 1
, you can substitute f(1)
with 1 + 1
whenever you like and it doesn’t affect the final result.
On the other hand, the evaluation order matters for expression with side effects. Think about a function g
that prints a message: in the program g("Hello "); g("world!")
, it’s important that g("Hello ")
be evaluated before g("world!")
, otherwise "world!Hello "
will printed instead of "Hello world!"
.
Reminder: Scala uses the call-by-value evaluation strategy by default.
upgradeUserToVip
Someone is writing a user-management program for a supermarket like Aligro. It maintains two global lists: users
contains all users, vipUsers
contains the VIP users. Any VIP user is in the users
list as well.
var users: IntList = IntCons(101, IntCons(102, IntCons(103, IntNil())))
var vipUsers: IntList = IntCons(102, IntNil())
src/main/scala/debugging/ListOps.scala
createNewUser
takes a user ID and creates a new user using this ID. It returns true
if the new user was successfully created, and false
if the ID was already in use (in that case, the user database is left unmodified).
def createNewUser(id: Int): Boolean =
if contains(users, id) then false
else
users = IntCons(id, users)
true
src/main/scala/debugging/ListOps.scala
createNewVipUser
is similar to createNewUser
, but it creates a new VIP user if it’s not already a VIP. Like createNewUser
, it returns true
if a new VIP user was successfully created.
def createNewVipUser(id: Int): Boolean =
if contains(vipUsers, id) then false
else
vipUsers = IntCons(id, vipUsers)
true
src/main/scala/debugging/ListOps.scala
Finally, upgradeUserToVip
takes a user ID, and if this ID indeed belongs to a user and this user is not a VIP yet, it upgrades the user to VIP. It returns true if the upgrade was successful.
def upgradeUserToVip(id: Int): Boolean =
val canCreateNewVipUser = createNewVipUser(id)
contains(users, id) && canCreateNewVipUser
src/main/scala/debugging/ListOps.scala
-
This program contains a bug. What’s the bug?
-
Fix the bug.
Tracing code execution
Wait until the tracing and tests lecture to tackle this!
Polish notation (from week 1) ⭐️
Use tracing to gain a better understanding of the following function, which computes the value of a polish-notation expression:
enum Operand[+T]:
case Add extends Operand[Nothing]
case Mul extends Operand[Nothing]
case Num(t: T)
type OpStack[T] = List[Operand[T]]
src/main/scala/debugging/Tracing.scala
def polishEval(ops: OpStack[Int]): (Int, OpStack[Int]) =
ops match
case Nil => throw IllegalArgumentException()
case op :: afterOp =>
op match
case Operand.Num(n) =>
(n, afterOp)
case Operand.Add =>
val (l, afterL) = polishEval(afterOp)
val (r, afterR) = polishEval(afterL)
(l + r, afterR)
case Operand.Mul =>
val (l, afterL) = polishEval(afterOp)
val (r, afterR) = polishEval(afterL)
(l * r, afterR)
src/main/scala/debugging/Tracing.scala
McCarthy ⭐️
What does the following function do? Use tracing annotations to figure it out!
def mc(n: Int): Int =
if n > 100 then n - 10
else mc(mc(n + 11))
src/main/scala/debugging/Tracing.scala
Takeuchi 🔥
What do the following functions do? Use tracing annotations to figure it out!
def t(x: Int, y: Int, z: Int): Int =
if x <= y then y
else
t(
t(x - 1, y, z),
t(y - 1, z, x),
t(z - 1, x, y)
)
src/main/scala/debugging/Tracing.scala
Tree folds
Two functions pairs
and foldt
are defined below. Use examples, pictures, the substitution method, refactoring, or tracing to figure out what foldt
does.
extension [T](l: List[T])
def pairs(op: (T, T) => T): List[T] = l match
case a :: b :: tl => op(a, b) :: tl.pairs(op)
case _ => l
def foldt(z: T)(op: (T, T) => T): T = l match
case Nil => z
case List(t) => t
case _ :: tail => l.pairs(op).foldt(z)(op)
src/main/scala/debugging/Tracing.scala
Armed with that knowledge, can you figure what algorithm the function ms
below implements? Is it efficient? How does it differ from another version of the same algorithm that you saw previously?
extension (l: List[Int])
def ms: List[Int] =
l.map(List(_)).foldt(Nil)(merge)
src/main/scala/debugging/Tracing.scala
(merge
is the usual function that takes two sorted lists and merges them in sorted order:)
def merge(l1: List[Int], l2: List[Int]): List[Int] =
(l1, l2) match
case (Nil, l) => l
case (l, Nil) => l
case (h1 :: t1, h2 :: t2) =>
if h1 <= h2 then h1 :: merge(t1, l2)
else h2 :: merge(l1, t2)
src/main/scala/debugging/Tracing.scala
Tracing performance issues
Sometimes, tracing can even help find performance issues: the longer a calculation takes, and the slower tracing statements will appear on screen. Can you use this insight to demonstrate that something is wrong with each of the following functions?
- Reverse
def badReverse[T](l: List[T], acc: List[T] = Nil): List[T] =
l match
case Nil => acc.reverse
case h :: t => badReverse(t, acc ++ List(h))
src/main/scala/debugging/Tracing.scala
- Map
def badMap[T1, T2](l: List[T1], f: T1 => T2): List[T2] =
if l.length == 0 then Nil
else f(l.head) :: badMap(l.tail, f)
src/main/scala/debugging/Tracing.scala