Simple and community accepted answer
In general, the community has a consensus that all the proposals or alternatives listed bellow are not worth it for their trade-offs.
As such, the recommended solution is to just use the Option datatype and manually / explicitly wrapping the value in a Some
def test(required: Int, optional: Option[String] = None): String =
optional.map(_ * required).getOrElse("")
test(required = 100) // ""
test(required = 3, optional = Some("Foo")) // "FooFooFoo"
However, the obvious disadvantage of this approach is the necessary boilerplate on-call site.
But, it can be argued that it makes the code easier to read and understand, and thus to maintain.
Nevertheless, sometimes you can provide a better API using other techniques like default arguments, or overloading (discussed below).
Alternatives and proposals
Implicit conversions
Due to the boilerplate of the previous solution, a common alternative of using implicit conversions is mentioned over and over again; for example:
implicit def a2opt[A](a: A): Option[A] = Some(a)
So that the previous function could be called like this:
test(required = 3, optional = "Foo")
The downside of this is that the implicit conversion hides the fact that optional was an optional argument (well, of course, if it would be named different) and that such conversion can be applied in many other (unintended) parts of the code; which is the reason why implicit conversions, in general, are discouraged.
A sub-alternative would be to use extension methods instead of implicit conversions, something like optional = "foo".opt. However, the fact that the extension method is even more code to add and that site call still have some boilerplate makes this one like a mediocre middle point.
(disclaimer, if you are using cats you already have such an extension method in scope .some so you may want to use that).
Default arguments
The language provides support for giving default values to arguments of a function, such that if not passed the compiler will insert the default value.
One may think that this should be the best way to model optional arguments; however, they had three problems.
You do not always have a default value, sometimes you only want to know if the value was passed or not. For example, a flag.
If it is on its own parameter group, you still need to add empty parenthesis which may look ugly (this is, of course, a subjective opinion).
def transact[A](config: Config = Config.default)(f: Transaction => A): A
transact()(tx => ???)
- You can only have one overload with default arguments.
object Functions {
def run[A](query: Query[A], config: Config = Config.default): A = ???
def run[A](query: String, config: Config = Config.default): A = ???
}
error: in object Functions, multiple overloaded alternatives of method run define default arguments.
Overloading
Another common workaround is to provide an overload version of the method; for example:
def test(required: Int, optional: String): String =
optional * required
def test(required: Int): String =
test(required, optional = "")
The advantage of this one is that it encapsulates the boilerplate on-definition site instead of on-call site; also makes the code easier to read and is well supported by tooling.
Nevertheless, the biggest disadvantage is that this doesn't scale well if you have more than one optional argument; for example, for three arguments you need seven (7) overloads.
But, if you have many optional parameters, maybe it could be better to rather ask for just one single Config / Context argument and use a Builder.
Builder pattern.
def foo(data: Dar, config: Config = Config.default)
// It probably would be better not to use a case class for binary compatibility.
// And rather define your own private copy method or something.
// But that is outside of the scope of this question / answer.
final case class Config(
flag1: Option[Int] = None,
flag2: Option[Int] = None,
flag3: Option[Int] = None
) {
def withFlag1(flag: Int): Config =
this.copy(flag1 = Some(flag))
def withFlag2(flag: Int): Config =
this.copy(flag2 = Some(flag))
def withFlag3(flag: Int): Config =
this.copy(flag3 = Some(flag))
}
object Config {
def default: Config = new Config()
}
Request for native support
On the contributors' discourse, it has been proposed to add language-level or stdlib-level support for this use case. However, all of them have been discarded for the same reasons mentioned above.
Examples of such proposals:
Conlusion
As always, choose which technique(s) to use depending on your specific case and the API you want to provide.
Scala 3
Maybe the introduction of union types could open the possibility of a simpler way to encode optional arguments?