Hi all!
I've been kicking around this idea for an addition to Scala's core syntax for about a month, and I've started work on a patch to the compiler to implement it. I'd very much like to see if anyone else besides me has any interest in/feedback on this idea, and hopefully to promote it to a SIP.
In a nutshell: I want to allow optional parameters (kind of like default arguments, but different—complementary to them, in a sense) to functions, that behave like Option the same way that repeated/vararg parameters behave like Seq. This arises from a problem (of the class ‘useful, but could be more useful’) I have with default arguments. Read on for an explanation of the problem and what I'm trying to do about it.
(Apologies if something like this came up and was rejected when default arguments were being proposed for 2.8. I'm pretty new to the Scala community, and while I read back through most of the scala-debate archives and didn't see any discussion of a feature like this, I'm well aware that I could have just missed it.)
1 Motivation
Suppose you have a service that looks like this:
class CookieBaker {
def bakeCookies(quantity: Int,
flavor: CookieFlavors.Value = CookieFlavors.ChocolateChip) =
sys.error("Not implemented yet...")
}
The fact that the default flavor is chocolate chip is an implementation detail of the baker. Callers don't need to know it, and the CookieBaker class could change the flavor at any time without having to change other code that truly is agnostic to the flavor of cookies produced. This is as it should be.
Now consider another class that uses CookieBaker:
class KitchenManager(cookieBaker: CookieBaker, cakeBaker: CakeBaker) {
def bakeForParty(partySize: Int,
cookieFlavor: CookieFlavors.Value = CookieFlavors.ChocolateChip) {
cookieBaker.bakeCookies(partySize*3, cookieFlavor)
cakeBaker.bakeCakes(partySize/4)
}
}
Now we are in trouble: we have redundantly specified that the default cookie flavor is chocolate chip in two separate classes. Maybe that's okay; it could be that in the domain of this problem, the KitchenManager and the CookieBaker separately have independent ideas about what the default cookie flavor should be (and at the moment, they happen to coincide). But let's assume in this case, if the KitchenManager is not told what cookie flavor to provide, it would prefer to defer to the CookieBaker.
So you can revise KitchenManager in a couple of different ways, none of which are great:
class KitchenManager(cookieBaker: CookieBaker, cakeBaker: CakeBaker) {
def bakeForParty(partySize: Int, cookieFlavor: Option[CookieFlavors.Value]) {
cookieFlavor match {
case Some(flavor) => cookieBaker.bakeCookies(partySize*3, flavor)
case None => cookieBaker.bakeCookies(partySize*3)
}
cakeBaker.bakeCakes(partySize/4)
}
}
class KitchenManager(cookieBaker: CookieBaker, cakeBaker: CakeBaker) {
def bakeForParty(partySize: Int, cookieFlavor: CookieFlavors.Value) {
cookieBaker.bakeCookies(partySize*3, cookieFlavor)
cakeBaker.bakeCakes(partySize/4)
}
def bakeForParty(partySize: Int) {
cookieBaker.bakeCookies(partySize*3)
cakeBaker.bakeCakes(partySize/4)
}
}
(If I'm missing a very clean way to do this in existing Scala, please let me know!)
The first solution is a little ugly to call (callers have to use Some or None, which is both extra clutter and not consistent with the more lightweight calling pattern for CookieBaker), and a little ugly to implement (you have to call cookieBaker.bakeCookies in both cases of the match, with very similar arguments—and about the only thing you can factor out of this would be a def quantity = partySize*3, which certainly doesn't make the method much easier to read). You could solve the first of these problems with implicits and either some new Optional trait which is a dumb wrapper for an Option (sad) or implicit conversions to/from Option itself (potentially a source of bugs/confusion elsewhere!); but I think the second problem is basically intractable (and grows exponentially with the number of default arguments under consideration!).
The second solution is ideal to call, but has the same redundancy/exponential code growth problems as the first, and what's worse, the growth affects actual method overloads rather than case clauses isolated inside a single method. Plus, it feels plain dirty to use default arguments at one layer of the software, and overloads at another, when talking about the same thing threaded between the two layers.
There are other hacks: you could use (shudder) null as the default argument to KitchenManager, and trust that null doesn't have some special significance to some other part of the software. You could, if you can edit the CookieFlavors enum, give it a Default value, but then everything that ever takes a CookieFlavors.Value would have to define what to do if given a Default. None of these solutions feel good and Scala-ish to me.
Here's the code I'd like to write (the bold red bits are new syntax that this language proposal would enable):
class KitchenManager(cookieBaker: CookieBaker, cakeBaker: CakeBaker) {
def bakeForParty(partySize: Int, cookieFlavor: CookieFlavors.Value?) {
cookieBaker.bakeCookies(partySize*3, cookieFlavor:_?)
cakeBaker.bakeCakes(partySize/4)
}
}
myKitchenManager.bakeForParty(5)
myKitchenManager.bakeForParty(10, CookieFlavors.Sugar)
val optFlavor: Option[CookieFlavors.Value] = getCookieFlavorPreference
myKitchenManager.bakeForParty(15, optFlavor:_?)
If it isn't obvious what the new ? syntax is intended to mean, consider it analogous to the * character in repeated parameters:
‘*’ is to scala.Seq as ‘?’ is to scala.Option
Just like * lets callers provide zero or more arguments for a parameter, which get wrapped up in a Seq, ? is intended to let callers provide zero or one arguments for a parameter, which gets wrapped up in an Option. The remainder of this document hammers out in pedantic detail what this means technically, but hopefully that analogy tells you enough to be able to understand the code and form an opinion on whether Scala would be better off with this feature.
Incidentally, I think of this improvement as two mostly independent features: optional parameters (the ability to declare a parameter with type T? as sugar for Option[T], with the feature that such parameters expect an argument of type T which may be missing) and option arguments (the ability to call a method expecting arguments of type T using arguments of type Option[T], if the language knows what to do if that argument is missing). The next two sections describe each of these separately, though their utility and the spirit of the */? analogy is I think most evident when both are considered together.
2 Optional Parameters
This proposed feature provides for the declaration/definition of functions with optional parameters with a syntax analogous to repeated parameters. The proposal builds on the work done with default arguments for Scala 2.8.
Parameter types can be suffixed with ‘?’ to indicate that a parameter is optional. Just as the type of a repeated parameter (annotated with T*) in the definition of the function is scala.Seq[T], the type of an optional parameter (annotated with T?) in the definition of the function is scala.Option[T]. When applying a function with one or more optional parameters, overload resolution, implicit conversions, and parameter binding proceed exactly as if each optional parameter a: T? were actually declared with a default argument as a: T = undefined, where undefined is a fictitious term of type T. After binding has succeeded, if optional parameter a is bound to an expression expr, that expression is lifted to scala.Some(expr); if a is not bound (where, for an ordinary parameter with a default argument, the compiler would insert a call to the default), the binding a = scala.None: Option[T] is used.
A function declaration/definition with optional parameters is valid if and only if the equivalent function with all optional parameters a: T? replaced with a: T = undefined would be valid. In particular, it is allowed to declare any number of optional or non-optional parameters in any order in a parameter section, but it is not allowed to have a parameter section with both optional parameters and a repeated parameter.
2.1 Syntax
The production of ParamType in SLS 2.9 chapter 4 is extended with the following clauses:
ParamType ::= Type ‘?’
| ‘=>’ Type ‘?’
2.2 Integration with other features
By-Name Parameters An optional parameter declared with the by-name arrow ‘=>’ works such that every usage of the parameter within the body of the function re-evaluates the lifted argument expression (scala.Some(expr) or scala.None, depending).
Default Arguments Works exactly as if the optional parameter a: T? were declared as a: T = undefined. An optional parameter may not be declared with a default argument itself, but within a single parameter section, optional parameters and parameters with default arguments may coexist.
Implicit Parameters If an optional parameter a: T? is implicit, an implicit value matching T (not scala.Option[T]) is searched for. If an implicit value v matching T is found, the value of a inside the function is scala.Some(v); if none is found, the value of a inside the function is scala.None.
Overloading For the purpose of determining whether a member definition matches another (per definition 5.1.4 in SLS 2.9), an optional parameter a: T? is considered to have type Option[T]. In addition, at most one overloaded alternative of a method is allowed to have default arguments or optional parameters (or both).
Overloading Resolution As with default arguments, when multiple overloaded alternatives are applicable, the alternative which uses optional parameters is never selected.
Overriding If a member M matches a non-private member M′ of a base class, then in addition to the existing restrictions on M and M′ defined in section 5.1.4 of SLS 2.9, this additional restriction applies to M: a parameter of M must be optional if and only if it corresponds to an optional parameter of M′. (In other words, even though the definition of matching is amended to define T? to be equivalent to Option[T], a T? parameter cannot be overridden with an Option[T] parameter or vice versa.)
Repeated Parameters As with default arguments, repeated parameters and optional parameters may not coexist in a parameter section.
2.3 Example
The following code is a trivial use of optional parameters:
def show(i: Int?) = i match {
case Some(value) => println("i = " + value)
case None => println("i is unspecified")
}
show(5) // prints "i = 5"
show() // prints "i is unspecified"
It is semantically equivalent to the following Scala 2.9 code:
def show(i: Option[Int]) = i match {
case Some(value) => println("i = " + value)
case None => println("i is unspecified")
}
show(Some(5))
show(None)
3 Option Arguments
This proposed feature provides for using a scala.Option[T] to determine at runtime whether to ‘take the default’ when calling a function with a parameter of type T that is either optional or has a default argument.
Analogously to the existing sequence argument feature, an expression of type scala.Option[T] can be marked to be an option argument with a _? type annotation. For the purposes of resolving overloads and parameter binding, option arguments are taken to have type T, except that an option argument can only be considered compatible with optional parameters or parameters with default arguments; all other rules apply as usual. If the option argument expr: Option[T] is successfully bound to a parameter a: T = default, then the actual value of a in the function is expr.getOrElse(default). If the option argument is successfully bound to an optional parameter a: T?, then the actual value of a in the function is expr (skipping the usual lifting performed for optional parameters as described in section 2 of this proposal).
3.1 Syntax
The production of ArgumentExprs as defined in chapter 6 of SLS 2.9 is amended to the following:
ArgumentExprs ::= ‘(’ [Exprs1] ‘)’
| ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’
| [nl] BlockExpr
Exprs1 ::= Expr [‘:’ ‘_’ ‘?’] {‘,’ Expr [‘:’ ‘_’ ‘?’] }
Note that, for instance, someFunc(a:_?, b:_*) is syntactically invalid (and could never match a real function anyway, as repeated parameters and default argument/optional parameters can't coexist in a parameter section).
3.2 Integration with other features
By-Name Parameters An option argument to a function with a by-name parameter works such that every usage of the parameter within the body of the function re-evaluates the unlifted argument expression or the entire expr.getOrElse(default) expression, depending on whether the parameter is optional or has a default argument.
Overloading Resolution Because only one overload alternative can use default arguments or optional parameters, when applying an overloaded function with option arguments, only that alternative is considered for applicability. Option arguments do not allow for runtime switching between different overload alternatives.
3.3 Example
Continuing the example from 2.3:
val three = Some(3)
show(three:_?) // same as show(3)
show(None:_?) // same as show()
----------------------------------------
This message is intended exclusively for the individual(s) or entity to
which it is addressed. It may contain information that is proprietary,
privileged or confidential or otherwise legally exempt from disclosure.
If you are not the named addressee, you are not authorized to read,
print, retain, copy or disseminate this message or any part of it.
If you have received this message in error, please notify the sender
immediately by e-mail and delete all copies of the message.