I’ve been using Scala for the better part of a year, and it’s mostly been an enjoyable experience. Scala fits in a comfortable position in the programming latent space somewhere in between Java, Python, JavaScript, and Rust. However, Scala is definitely a a “big” language – it has lots of language features, supports many programming paradigms, and has a large enough surface area that the likelihood of encountering one (of many) footguns is high.
One pitfall that I’ve seen fairly regularly is in the (mis)use of parameterless methods. In Scala, likely due to its functional programming influence, functions without parameters do not need parenthesis. So, the following call patterns function identically and are both valid:
class Dog {
def bark() = { println("bark") }
}
val dog = new Dog()
dog.bark() // With parens
dog.bark // Without parens
By itself, this isn’t a harmful feature. Invoking method functions without parenthesis, in my opinion, introduces a little bit of cognitive load – since you don’t always know if what’s being evaluated is a function, or just a member variable.
What can get you in trouble though is that if one swaps out def
for val
in
the above example, the behavior changes significantly, with just one keyword
change.
Take the following example, creating a very basic counter object:
class Counter {
private var count = 0
def increment() = {
count += 1
}
val value: Int = {
count
}
}
c = new Counter()
c.increment
c.increment
println(c.value) // Prints 0!
In this case, it’s pretty clear what’s going wrong – the val
block of value
is only evaluated once, when the count
is still 0. However, from the call
site, this isn’t clear, since c.value
could be a member var
or val
, or
could be a parameterless function call (def
). In languages that don’t allow
function calls without parenthesis, the distinction would be more clear:
value()
indicates a function call, value
indicates a member access.
In a less trivial example, this is more-or-less something that I’ve seen happen multiple times in my current code base. For this example, assume we’re pulling some configuration data from a cache – for instance, whether or not to enable a feature or not. This could be to implement a dynamic kill switch for a particular set of features.
class CacheClient {
def getValue(key: String) = { ... }
}
class MyConfiguration {
val cache = new CacheClient()
def isFooEnabled: Boolean = {
cache.getValue("foo")
}
// Oops!
val isBarEnabled: Boolean = {
cache.getValue("bar")
}
}
class BusinessLogic {
val conf = new MyConfiguration()
def handleRequest() {
if (conf.isFooEnabled) {
// Some code path
} else {
// Another code path
}
if (conf.isBarEnabled) {
// Some code path
} else {
// Another code path
}
}
}
The issue above, with isBarEnabled
, is that it looks dynamic, in the same
way that isFooEnabled
actually is dynamic, but it’s not! If the underlying
value in the cache changes, isFooEnabled
will correctly (dynamically) fetch
the value from the cache each time, but isBarEnabled
won’t. It just saves the
value of cache.getValue("bar")
that was returned at initialization, and never
re-queries the cache.
From the BusinessLogic
code though, you’d never know this, unless you dug in
to the implementation of MyConfiguration
.
My team owns something that looks pretty similar to the MyConfiguration
object, as part of a common library that gets used in most of the services
running at my company. Since these methods are, by definition, only used on
configuration objects, they aren’t typically unit tested. Additionally,
integration tests often fail to catch this sort of issue, since they don’t
usually test the value of the configuration changing at runtime.
To solve this particular issue, my team added a wrapper around the dynamic
component, and provide a getCurrentValue
method to get the value dynamically.
This makes it a bit harder to misuse:
class ConfigurationFetcher[T](key: String, default: T) {
def getCurrentValue(): T = { ... }
}
class MyConfiguration {
val fooEnabled: ConfigurationFetcher[Boolean] = ConfigurationFetcher("foo", false)
}
class BusinessLogic {
val conf = new MyConfiguration()
def handleRequest() {
if(conf.fooEnabled.getCurrentValue()) {
// Some code path
}
...
}
}
We also added static compile-time checks to prevent a common misuse:
class MyConfiguration {
// Disallowed: ConfigurationFetcher is not a class property!
val fooEnabled = {
val fetcher = ConfigurationFetcher[Boolean]("foo", false)
fetcher.getCurrentValue()
}
}
Of course, there are still ways of getting around the static checks, such as
wrapping the output of getCurrentValue
itself in a non-changing val
:
class MyConfiguration {
val fooEnabled: ConfigurationFetcher[Boolean] = ConfigurationFetcher("foo", false)
val barEnabled: ConfigurationFetcher[Boolean] = ConfigurationFetcher("bar", false)
// Not caught by static analysis, but won't work as expected!
val fooOrBarEnabled = fooEnabled.getCurrentValue() || barEnabled.getCurrentValue()
}
This isn’t really a problem with Scala, rather just one outcome of its
flexible nature and terse syntax. Your eye eventually becomes trained to see
def
and val
as importantly different symbols, but even so some of these
issues are tricky to spot.