On the other day I saw a post asking for usual questions on Scala related job interviews. Among some answers I saw a reference to a GitHub repository with several questions, not only Scala specific, but also about functional programming, and even more generic questions.
And while one can argue about the effectiveness of those questions to filter job applicants, I found those questions an interesting exercise.
Here’s the list of questions I decided to answer to exercise myself, mostly from that repository:
- What is the difference between a
var
, aval
anddef
? - What is the difference between a
trait
and anabstract class
? - What is the difference between an
object
and aclass
? - What is a
case class
? - What is the difference between a Java future and a Scala future?
- What is the difference between
unapply
andapply
, when would you use them? - What is a companion object?
- What is the difference between the following terms and types in Scala:
Nil
,Null
,None
,Nothing
? - What is
Unit
? - What is the difference between a
call-by-value
andcall-by-name
parameter? - Define uses for the
Option
monad and good practices it provides. - How does
yield
work? - Explain the implicit parameter precedence.
- What operations is a
for comprehension
syntactic sugar for? -
Streams:
- What consideration you need to have when you use Scala’s
Streams
? - What technique does the Scala’s
Streams
use internally?
- What consideration you need to have when you use Scala’s
- What is a
value class
? - Option vs Try vs Either
- What is function currying?
- What is Tail recursion?
- What are High Order Functions?
Answers
Before you decide to read my answers (which may even not be right - but I do hope they are ), try to answer them yourself.
1. What is the difference between a var
, a val
and def
?
A var
is a variable. It’s a mutable reference to a value. Since it’s mutable, its value may change through the program lifetime. Keep in mind that the variable type cannot change in Scala. You may say that a var
behaves similarly to Java variables.
A val
is a value. It’s an immutable reference, meaning that its value never changes. Once assigned it will always keep the same value. It’s similar to constants in another languages.
A def
creates a method (and a method is different from a function - thanks to AP for his comment). It is evaluated on call.
var x = 3 // x is of type Int. If you force it to be of type Any then this example would work
x = 4 // accepted by the language/compiler
x = "error" // not accepted by the compiler
val y = 3
y = 4 // would produce an error 'error: reassignment to val'
def fun(name: String) = "Hey! My name is: " + name
fun("Scala") // "Hey! My name is: Scala"
fun("Java") // "Hey! My name is: Java"
Bonus: what’s a lazy val
? It’s almost like a val
, but its value is only computed when needed. It’s specially useful to avoid heavy computations (using short-circuit for instance).
lazy val x = {
println("computing x")
3
}
val y = {
println("computing y")
10
}
y + y // x was still not computed, "computing x" was not yet printed
x + x // x is required, x is going to be computed, a "computing x" message will be printed *once*
2. What is the difference between a trait
and an abstract class
?
The first difference is that a class
can only extend one other class
, but an unlimited number of traits
.
While traits
only support type parameters
, abstract classes
can have constructor parameters
.
Also, abstract classes
are interoperable with Java, while traits
are only interoperable with Java if they do not contain any implementation.
3. What is the difference between an object
and a class
?
An object
is a singleton instance of a class
. It does not need to be instantiated by the developer.
If an object
has the same name that a class
, the object
is called a companion object
(check Q7 for more details).
class MyClass(number: Int, text: String) {
def classMethod() = ???
}
object MyObject {
def objectMethod() = ???
}
new MyClass(3, "text").classMethod()
MyClass.classMethod() // won't compile
MyObject.objectMethod() // you don't need to create an instance to call the method
4. What is a case class
?
A case class
is syntactic sugar for a class that is immutable and decomposable through pattern matching (because they have an apply
and unapply
methods). Being decomposable means it is possible to extract its constructors parameters in the pattern matching.
Case classes contain a companion object which holds the apply
method. This fact makes possible to instantiate a case class without the new
keyword. They also come with some helper methods like the .copy
method, that eases the creation of a slightly changed copy from the original.
Finally, case classes are compared by structural equality instead of being compared by reference, i.e., they come with a method which compares two case classes by their values/fields, instead of comparing just the references.
Case classes are specially useful to be used as DTOs.
case class MyCaseClass(number: Int, text: String, others: List[Int])
val dto = MyCaseClass(3, "text", List.empty)
dto.copy(number = 5) // will produce an instance equal to the original, with number = 5 instead of 3
val dto2 = MyCaseClass(3, "text", List.empty)
dto == dto2 // will return true even if different references
class MyClass(number: Int, text: String, others: List[Int]) {}
val c1 = new MyClass(1, "txt", List.empty)
val c2 = new MyClass(1, "txt", List.empty)
c1 == c2 // will return false because they are different references
5. What is the difference between a Java future and a Scala future?
This one I had to google a little about it. I have never used Java futures, so it was impossible for me to answer.
Obviously, I was not the first to search for the differences between both futures. I found a really clean and simple answer on StackOverflow which highlights the fact that the Scala implementation is in fact asynchronous without blocking, while in Java you can’t get the future value without blocking.
Scala provides an API to manipulate the future as a monad or by attaching callbacks for completion. Unless you decide to use the Await
, you won’t block your program using Scala futures.
6. What is the difference between unapply
and apply
, when would you use them?
unapply
is a method that needs to be implemented by an object in order for it to be an extractor. Extractors are used in pattern matching to access an object constructor parameters. It’s the opposite of a constructor.
The apply
method is a special method that allows you to write someObject(params)
instead of someObject.apply(params)
. This usage is common in case classes
, which contain a companion object with the apply
method that allows the nice syntax to instantiate a new object without the new
keyword.
7. What is a companion object?
If an object
has the same name that a class
, the object is called a companion object
. A companion object has access to methods of private visibility of the class, and the class also has access to private methods of the object. Doing the comparison with Java, companion objects hold the “static methods” of a class.
Note that the companion object has to be defined in the same source file that the class.
class MyClass(number: Int, text: String) {
private val classSecret = 42
def x = MyClass.objectSecret + "?" // MyClass.objectSecret is accessible because it's inside the class. External classes/objects can't access it
}
object MyClass { // it's a companion object because it has the same name
private val objectSecret = "42"
def y(arg: MyClass) = arg.classSecret -1 // arg.classSecret is accessible because it's inside the companion object
}
MyClass.objectSecret // won't compile
MyClass.classSecret // won't compile
new MyClass(-1, "random").objectSecret // won't compile
new MyClass(-1, "random").classSecret // won't compile
8. What is the difference between the following terms and types in Scala: Nil
, Null
, None
, Nothing
?
The None
is the empty representation of the Option
monad (see answer #11).
Null
is a Scala trait, where null
is its only instance. The null
value comes from Java and it’s an instance of any object, i.e., it is a subtype of all reference types, but not of value types. It exists so that reference types can be assigned null
and value types (like Int
or Long
) can’t.
Nothing
is another Scala trait
. It’s a subtype of any other type, and it has no subtypes. It exists due to the complex type system Scala has. It has zero instances. It’s the return type of a method that never returns normally, for instance, a method that always throws an exception. The reason Scala has a bottom type is tied to its ability to express variance in type parameters..
Finally, Nil
represents an empty List of anything of size zero. Nil
is of type List[Nothing]
.
All these types can create a sense of emptiness right? Here’s a little help understanding emptiness in Scala.
9. What is Unit
?
Unit
is a type which represents the absence of value, just like Java void
. It is a subtype of scala.AnyVal
. There is only one value of type Unit, represented by ()
, and it is not represented by any object in the underlying runtime system.
10. What is the difference between a call-by-value
and call-by-name
parameter?
The difference between a call-by-value
and a call-by-name
parameter, is that the former is computed before calling the function, and the later is evaluated when accessed.
Example: If we declare the following functions
def func(): Int = {
println("computing stuff....")
42 // return something
}
def callByValue(x: Int) = {
println("1st x: " + x)
println("2nd x: " + x)
}
def callByName(x: => Int) = {
println("1st x: " + x)
println("2nd x: " + x)
}
and now call them:
scala> callByValue(func())
computing stuff....
1st x: 42
2nd x: 42
scala> callByName(func())
computing stuff....
1st x: 42
computing stuff....
2nd x: 42
As it may be seen, the call-by-name
example makes the computation only when needed, and every time it is called, while the call-by-value
only computes once, but it computes before invoking the function (callByName
).
11. Define uses for the Option
monad and good practices it provides.
The Option
monad is the Scala solution to the null
problem from Java. While in Java the absence of a value is modeled through the null
value, in Scala its usage is discouraged, in flavor of the Option
.
Using null
values one might try to call a method on a null instance, because the developer was not expecting that there could be no value, and get a NullPointerException
. Using the Option
, the developer always knows in which cases it may have to deal with the absence of value.
val person: Person = getPersonByIdOnDatabaseUnsafe(id = 4) // returns null if no person for provided id
println(s"This person age is ${person.age}") // if null it will throw an exception
val personOpt: Option[Person] = getPersonByIdOnDatabaseSafe(id = 4) // returns an empty Option if no person for provided id
personOpt match {
case Some(p) => println(s"This person age is ${p.age}")
case None => println("There is no person with that id")
}
12. How does yield
work?
yield
generates a value to be kept in each iteration of a loop. yield
is used in for comprehensions as to provide a syntactic alternative to the combined usage of map
/flatMap
and filter
operations on monads (see answer #14).
scala> for (i <- 1 to 5) yield i * 2
res0: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)
13. Explain the implicit parameter precedence.
Implicit parameters can lead to unexpected behavior if one is not aware of the precedence when looking up.
So, what’s the order the compiler will look up for implicits?
- implicits declared locally
- imported implicits
- outer scope (implicits declared in the class are considered outer scope in a class method for instance)
- inheritance
- package object
- implicit scope like companion objects
A nice set of examples can be found here.
14. What operations is a for comprehension
syntactic sugar for?
A for comprehension
is a alternative syntax for the composition of several operations on monads.
A for comprehension
can be replaced by foreach
operations (if no yield
keyword is being used), or by map
/flatMap
and filter
(actually, while confirming my words I found out about the withFilter
method).
for {
x <- c1
y <- c2
z <- c3 if z > 0
} yield {...}
is translated into
c1.flatMap(x => c2.flatMap(y => c3.withFilter(z => z > 0).map(z => {...})))
More examples by Loïc Descotte.
15. Streams: What consideration you need to have when you use Scala’s Streams
? What technique does the Scala’s Streams
use internally?
While Scala Streams
can be really useful due to its lazy nature, it may also come with some unexpected problems.
The biggest problem is that Scala Streams
can be infinite, but your memory isn’t. If used wrongly, streams can lead to memory consumption problems. One must be cautious when saving references to a stream
. One common guideline, is not to assign a stream (head) to a val
, but instead, make it a def
.
This is a consequence of the technique behind streams
: memoization
16. What is a value class
?
Have you ever had one of those nasty bugs where you were using an integer
thinking it would represent something but it actually represented a totally different thing ? For instance, an integer
representing an age, and another representing an height getting mixed (180 years old and 25 centimetres tall do look weird no?).
Because of that, it’s considered a good practice to wrap primitive types into more meaningful types.
Value classes allow a developer to increase the program type safety without incurring into penalties from allocating runtime objects.
There are some constraints and limitations, but the basic idea is that at compile time the object allocation is removed, by replacing the value classes instance by primitive types. More details can be found on its SIP.
17. Option vs Try vs Either
All of these 3 monads allow us to represent a computation that did not executed as expected.
An Option
, as explained on answer #11, represents the absence of value. It can be used when searching for something. For instance, database accesses often return Option
in lookup queries.
Try
is the monad approach to the Java try/catch
block. It wraps runtime exceptions.
If you need to provide a little more info about the reason the computation has failed, Either
may be very useful. With Either
you specify two possible return types: the expected/correct/successful and the error case which can be as simple as a String
message to be displayed to the user, or a full ADT.
def personAge(id: Int): Either[String, Int] = {
val personOpt: Option[Person] = DB.getPersonById(id)
personOpt match {
case None => Left(s"Could not get person with id: $id")
case Some(person) => Right(person.age)
}
}
18. What is function currying?
Currying is a technique of making a function that takes multiple arguments into a series of functions that take a part of the arguments.
def add(a: Int)(b: Int) = a + b
val add2 = add(2)(_)
scala> add2(3)
res0: Int = 5
Currying is useful in many different contexts, but most often when you have to deal with Higher order functions
.
19. What is Tail recursion?
In “normal” recursive methods, a method calls itself at some point. This technique is used in the naive implementation of the Fibonacci number, for instance. The problem with this approach is that at each recursive step, another chunk of information needs to be saved on the stack. In some cases, an huge number of recursive steps can occur, leading to stack overflow errors.
Tail recursion solves this problem. In tail recursive methods, all the computations are done before the recursive call, and the last statement is the recursive call. Compilers can then take advantage of this property to avoid stack overflow errors, since tail recursive calls can be optimized by not inserting info into the stack.
You can ask the compiler to enforce tail recursion in a method with @tailrec
def sum(n: Int): Int = { // computes the sum of the first n natural numbers
if(n == 0) {
n
} else {
n + sum(n - 1)
}
}
@tailrec // just to ensure at compile time
def tailSum(n: Int, acc: Int = 0): Int = {
if(n == 0) {
acc
} else {
tailSum(n - 1, acc + n)
}
}
if we now run both versions, what would happen?
> sum(5)
sum(5)
5 + sum(4) // computation on hold => needs to add info into the stack
5 + (4 + sum(3))
5 + (4 + (3 + sum(2)))
5 + (4 + (3 + (2 + sum(1))))
5 + (4 + (3 + (2 + 1)))
15
tailSum(5) // tailSum(5, 0) because the default value
tailSum(4, 5) // no computations on hold
tailSum(3, 9)
tailSum(2, 12)
tailSum(1, 14)
tailSum(0, 15)
15
20. What are High Order Functions?
High order functions are functions that can receive or return other functions. Common examples in Scala are the .filter
, .map
, .flatMap
functions, which receive other functions as arguments.
Conclusion
This was a fun and valuable exercise, both to deep my knowledge on some topics, and to learn about other topics I never had contact with until now, like the Java futures.
The answers I provide may offer some discussion points, but the goal was to provide a line of thought on those topics. There’s a lot more that can be said on each topic, depending on whom is asking them. Typical interviewers may ask you to go deeper on some of the topic to better understand your knowledge of the subject, or may ask you to relate two or more of the subjects.
If you are preparing yourself to an interview, make sure you are comfortable in those topics, and don’t simply memorize answers.