5 Scala Puzzlers That Will Make Your Brain Hurt
Hunting for non-intuitive cases of exceptions and errors in Scala
For this post we got in touch with Nermin Serifovic and Andrew Phillips who you might know from Scala Puzzlers. Together, we’ve selected a few questions to explore errors and exceptions in Scala. Some of the questions might seem weird at first, but it all makes sense when you dig down to uncover what’s really going on under the hood. We hope this post will help make you better familiar with the ins and outs of Scala, and nonetheless important, that you’ll find it enjoyable. Let’s dig in.
The answers are at the bottom of the post. But hey, no peeking!
1. An Exceptional Failure
What is the result of executing the following code?
import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Failure, Success} val f = Future { throw new Error("fatal!") } recoverWith { case err: Error => Future.successful("Ignoring error: " + err.getMessage) } f onComplete { case Success(res) => println("Yay: " + res) case Failure(e) => println("Oops: " + e.getMessage) }
Answers
- Prints:
Yay: Ignoring error: fatal! - Throws an error
- Prints:
Oops: fatal! - Prints:
Oops: Boxed Error
2. $!.*% Iterators!
What is the result of executing the following code?
val t = "this is a test" val rx = " ".r val m = rx.findAllIn(t) println(m) println(m.end) println(rx.findAllIn(t).end)
Answers
- non-empty iterator
5
5 - empty iterator
java.lang.IllegalStateException: No match available
java.lang.IllegalStateException: No match available - non-empty iterator
5
java.lang.IllegalStateException: No match available - non-empty iterator
java.lang.IllegalStateException: No match available
java.lang.IllegalStateException: No match available
3. What’s in a Name?
What is the result of executing the following code?
class C { def sum(x: Int = 1, y: Int = 2): Int = x + y } class D extends C { override def sum(y: Int = 3, x: Int = 4): Int = super.sum(x, y) } val d: D = new D val c: C = d c.sum(x = 0) d.sum(x = 0)
Answers
- 2
3 - 1
3 - 4
3 - 3
3
4. (Ex)Stream Surprise
What is the result of executing the following code?
val nats: Stream[Int] = 1 #:: (nats map { _ + 1 }) val odds: Stream[Int] = 1 #:: (odds map { _ + 1 } filter { _ % 2 != 0 }) nats filter { _ % 2 != 0 } take 2 foreach println odds take 2 foreach println
Answers
- Prints:
1
3
1
3 - Prints:
1
2
1
3 - The first statement prints:
1
3 and the second throws a runtime exception - The first statement throws a runtime exception and the second prints:
1
3
5. A Case of Strings
What is the result of executing the following code?
def objFromJava: Object = "string" def stringFromJava: String = null def printLengthIfString(a: AnyRef) { a match { case s: String => println("String of length " + s.length) case _ => println("Not a string") } } printLengthIfString(objFromJava) printLengthIfString(stringFromJava)
Answers
- Prints:
Not a string
String of length 0 - The first prints:
Not a string and the second throws a NullPointerException - Prints:
String of length 6
Not a string - The first prints:
String of length 6 and the second throws a NullPointerException
Solutions
1. An Exceptional Failure
The correct answer is: 4 => This snippet will print “Oops: Boxed Error”.
val f = Future { throw new Error("fatal!") } recoverWith { case err: Error => Future.successful("Ignoring error: " + err.getMessage) } f onComplete { case Success(res) => println("Yay: " + res) case Failure(e) => println("Oops: " + e.getMessage) }
So what do we have here?
There’s a Future that throws an error with the message “fatal!”, naively you might think that it will be able to recover and finish with a successful status. That’s not the case. What’s thrown here is an Exception not an Error, so we’re not entering the the err case in the recoverWith block.
Now that we know that the Future fails, there’s another caveat. The message that we print out is not the error message that we threw. The Exception that was thrown is a java.util.concurrent.ExecutionException which comes out of the box with a… “Boxed Error” message when it’s created from a failed Future.
Hence the result of the Future is:
Failure(new ExecutionException(“Boxed Error”, new Error(“fatal!”)))
2. $!.*% Iterators!
The correct answer is: 3 =>
non-empty iterator
5
java.lang.IllegalStateException: No match available
val t = "this is a test" val rx = " ".r val m = rx.findAllIn(t) println(m) println(m.end) println(rx.findAllIn(t).end)
So what do we have here?
This snippet creates a regular expression and finds all its matches (space characters) in the provided string. Makes sense, but findAllIn returns an Iterator. When printed, its toString gives us either “empty iterator” or “non-empty iterator”. Since we have 3 hits on that string, it’s non-empty.
Going forward, end returns the index of the character after the end of current hit. So it’s the index after the first space, which is 5.
Now comes the surprising part. For the finale, we get an IllegalStateException. Indeed $!.*% Iterators! When it comes down to business, unless you ask for or check the first element, the regex will not be activated. For the first println we asked for the toString and as a “side effect” we activated the regex, so there was no issue with the second println. For the third println we’re left hanging and get hit by an exception.
3. What’s in a Name?
The correct answer is: 3 => Prints 4, 3
class C { def sum(x: Int = 1, y: Int = 2): Int = x + y } class D extends C { override def sum(y: Int = 3, x: Int = 4): Int = super.sum(x, y) } val d: D = new D val c: C = d c.sum(x = 0) d.sum(x = 0)
So what do we have here?
There are 2 issues:
- The binding of parameter names is done by the compiler and the only information the compiler can use is the static type of the variable.
- For parameters with a default value the compiler creates methods which compute the default argument expressions. In the above example, both classes C and D contain the methods sum$default$1 and sum$default$2 for the two default parameters. When a parameter is missing, the compiler uses the result of these methods, and these methods are invoked at run-time.
Starting with c.sum(x=0), we’re calling the sum of class C with a missing y parameter. Which is the second parameter. What happens behind the scenes, because c was created from d, it carries the default methods created for missing parameters from class d. When it chooses which method to use, it does this regardless of the names which are the opposite for x and y. The second parameter is missing, so it assumes it is 4, while the first one is 0. The first result is 0+4=4. Woah, my brain hurts.
With d.sum(x=0), the situation is a bit different, we go straight to D’s sum, and the first y parameter is missing. We assume it’s 3, and the result is 0+3.
Josh Suereth summarizes this with the rule: “Names are static; values are runtime”.
4. (Ex)Stream Surprise
The correct answer is: 3 => The first statement prints: 1 3 and the second throws a runtime exception
val nats: Stream[Int] = 1 #:: (nats map { _ + 1 }) val odds: Stream[Int] = 1 #:: (odds map { _ + 1 } filter { _ % 2 != 0 }) nats filter { _ % 2 != 0 } take 2 foreach println odds take 2 foreach println
So what do we have here?
This snippet creates 2 Streams, nats and odds. The first contains (1,?) and holds the rule for adding more items to it, simple +1 increments. Similarly, the second has the same rule with a filter which allows it to only hold odd numbers. The trouble comes when we try to print it.
For nats, as we unfold there are no issues and we simply take the first 2 odd values since the filter is applied after the lazy creation of the stream: (1, (2, (3, ?)))
For odds, we can’t really take 2. Just the first one. We enter an endless recursive state. Since 2 isn’t odd, and the filter is activated right as the Stream is created with the +1 rule, we never reach 3 and get hit by a runtime exception.
5. A Case of Strings
The correct answer is: 3 => Prints: String of length 6, Not a string
def objFromJava: Object = "string" def stringFromJava: String = null def printLengthIfString(a: AnyRef) { a match { case s: String => println("String of length " + s.length) case _ => println("Not a string") } } printLengthIfString(objFromJava) printLengthIfString(stringFromJava)
So what do we have here?
For objFromJava, we’re getting to deal with a String even though it was initially created as an object. Since pattern matching resolution is based on the runtime-type, the length of the string “string” is printed out.
For stringFromJava, which is null, we’re getting “Not a string”. Since null is a whole different issue, we need to explicitly check for it as one of the cases and give it some special care.
Sources
Visit Scala Puzzlers to view the full list.
Final Thoughts
Thanks again for Nermin and Andrew for sharing their puzzlers with us! We hope that you’ve enjoyed reading through them and trying to solve them. While writing the post we’ve certainly learned some new stuff and we hope that so did you. Although if you’re ever running into puzzlers like these in your own code, it might not be so fun. For these kind of situations, we’ve built Takipi for Scala. Takipi is a native agent that knows how to track uncaught exceptions, caught exceptions and log errors on servers in production. It lets you see the variable values that cause errors, all across the stack, and overlays them on your code.
Reference: | 5 Scala Puzzlers That Will Make Your Brain Hurt from our JCG partner Alex Zhitnitsky at the Takipi blog. |