Scala Patterns To Avoid: Implicit Arguments With Default Values

Page content

There is a tendency for the Scala projects to prefer more explicit programming style. The biggest aspect of that is in my opinion the type system of the Scala language, programmers often start writing their functions by defining types of the arguments and type of the result, only to write the body of the function as last step. That’s also because we have the Scala compiler to help us.

I recently stumbled on a snippet that contradicts this rule and can be a source of hard to spot bugs for the person unfamiliar with the code.

The problem happens when you try to mix both features of the Scala language that make sense if used on their own (although I’ll argue with that a little in a moment), but when used together they create a very serious problem.

The features (as you probably guessed) are:

  • Implicit arguments

  • Default function arguments values

Implicit arguments

Implicit arguments when used in separation make a lot of sense, they allow for context-like arguments to be passed through the whole call stack. Furthermore they are the major building block for more advanced features like typeclasses.

Many major libraries or projects would not exist without it, but because this is a very powerful feature, it’s use should be limited.

Default function arguments

In my option this feature should be avoided almost everywhere - they make it very hard to use functions as arguments and pass function around.

It’s better to use either currying (multiple parameter lists) or just define function few times taking different set of parameters in each case.

I think it makes sense to use default arguments in 2 cases:

  • Backwards compatibility - you have an existing case class and want do add another attribute to it without changing your code or underlying database structures

  • Complicated, builder-like constructors - you have constructor that takes a ton of arguments, and users would like to configure only a limited set each time, using defaults for everything else

The problematic code

Here’s a snippet of the code I stumbled on:

case class User(name: String)

object User { val Default = User(name = unknown) }

def updateCampaign(c: Campaign)(implicit user: User = User.Default) { // save updated campaign // add audit log entry indicating user as a person performing action }

Here's what I think is wrong here:

* Caller is not aware that `updateCampaign` actually takes any argument unless he/she reads the source or looks up documentation (if there's any).  

You can perfectly well write code that looks like this:

val user = User(test) val campaign = Campaign(…) updateCampaign(campaign)

And you will not see anything wrong - the compiler won't complain. Unless the caller is aware there's another parameter expected, there's no straightforward way to learn it.

* If someone overrides `updateCampaign` function this information will be lost forever, all calls to the overridden version will use the default argument.

The above snippet contradicts the explicitness rule, also Scala compiler will not help you spotting a bug.

Of course the code will still run - the only problem is that every update to the campaign will be attributed to `User.Default` which is not what we are expecting - but there's no way to express that intend when using default arguments.

# Fixing the code

To fix it, simply remove the default value for the `user` argument:

def updateCampaign(c: Campaign)(implicit user: User) { // save updated campaign // add audit log entry indicating user as a person performing action }

After that change you will get the compilation error forcing you to fix the issue.

Scala compiler is now able to detect this issue right away, you don't have to remember which functions take which arguments because you can leverage the compiler.

# Summary

The outlined scenario is one of many examples where a programmer can use Scala compiler to his/her benefit. A simple change to the code will result in a compiler pointing out the problem right away, forcing you to address it before code is released.

It's also one of the many logic-related mistakes that are very hard to spot in testing - the "audit logging" in this case is a side effect to the regular responsibility of the code.