Passing true optional arguments to functions

I have a question regarding consensus of passing true optional arguments to functions. By “true” I mean use cases where in Java you would pass null to a function. My understanding is that I can make argument to such function to be of type Option[...] but then additional ceremony is required to actually pass arguments to such function. There is a work around that which involves use of implicit as in the example below

implicit def anyToOption[A](a: A): Option[A] = Option(a)

def age(years: Option[Int] = None): String = years match {
  case Some(value) => s"You are ${value} old"
  case None => "You are a timeless being"
}

def main(args: Array[String]): Unit = {
  println(age())
  println(age(10)) // <- Using implicit conversion
  println(age(Some(10))) // <- Without implicit conversion
}

Is that an acceptable practice? Are there any drawbacks that I should be aware of?

The main drawback is simply that that anyToOption is pretty unlimited – it’s going to apply in the cases you intend, but also potentially in many where you don’t. This sort of wide-ranging implicit can be pretty dangerous, since it weakens type safety in hard-to-predict ways. In general, broadly-applicable implicit conversions like this tend to be discouraged nowadays.

That is, there’s nothing specifically wrong with it – this sort of thing just tends to bite you on the butt when you least expect it.

I don’t know of a perfect solution to this particular problem, but there are a couple of alternatives I sometimes use. On the one hand, there are overloaded methods:

def age(years: Option[Int]): String = ...
def age(): String = age(None)
def age(years: Int): String = age(Some(years))

Or occasionally, I will abuse varargs to achieve this, although the signature winds up a bit misleading:

def age(yearsIn: Int*): String = {
  val years = yearsIn.headOption
}
1 Like

Yes that is the consensus and apart for having to write extra 6 characters, I do not see nothing wrong with it.

It may be worth to note that it have been proposed a couple of times adding syntactic sugar for allowing the same as that implicit conversion but as a rule of the compiler.
Such proposals have always been rejected by the argument that it makes the compiler / language even more complex and that it makes the code more obscure.

Making clear I was one of the people to make one of such proposals in the past (I even made that same implicit conversion that you have on one of my very first projects), I now agree it is better to be explicit about the option part.

BTW, as a bonus, I am pretty sure everyone will believe that implicit conversion is really a bad idead. It can get out of hands pretty easily (as any implicit conversion actually).

I would prefer overloading:

def age(years: Int): String = "you are $years old"
def age: String = "you are a timeless being"

In less trivial scenarios, you may want some sort of configuration object

class Person(age: Option[Int], name: Option[String]) {
  def aged(a: Int) = new Person(Some(a), name)
  def named(n: String) = new Person(age, Some(n))
}

object Person {
  def apply(age: Int, name: String) = new Person(Some(age), Some(name))
  def unknown = new Person(None, None)
}

def age(person: Person): String = person.age.map(y => s"you are $y years old").getOrElse("You are a timeless being")

age(Person.unknown.aged(10))
4 Likes

Agreed – I tend not to bother with this unknown, and instead just use the empty constructor as the initializer (so I would make it Person().aged(10).named("Joe")), but this sort of builder pattern works well when you’re constructing a complex structure, only a few of whose parameters might be specified at at given time.

1 Like

Thank you all for the answers. Normally I would not bother with use of implicit conversion, however, this pattern is for functions that are to be used for ad-hoc scratch like work similar to what you see in Jupiter notebooks and therefore need to have the lowest ceremony. I like idea of using overloading though where it is does not lead to excessive code bloat.

By “ true ” I mean use cases where in Java you would pass null to a function

additional ceremony is required to actually pass arguments to such function

Just to play devil’s advocate, in Java you need either the ceremony of reading the documentation of all the functions you call to see if they actually handle null being passed in as a value, or the ceremony of try { } catch (NullPointerException) everywhere. The ceremony is actually quite low.

But this solution avoids implicits and overhead and does everything you want:

object A {
    def age(years :Option[Int] = None) :String = years.map { y => s"You are ${y} old" }.getOrElse("You are a timeless being")
    def age(years :Int) :String = age(Some(years))
}

println(A.age())
println(A.age(10))
println(A.age(Some(10)))

It is considered bad practice to catch NullPointerException, because it may be thrown from a place different than what you were expecting.

I think it is actually not that common in Java that a method allows multiple arguments to be null, but it is far more common to massively overload a method to omit arguments, or use some sort of builder.

An option is to create a helper class only for such arguments to avoid the problem which @jducoeur explains:

class OptArg[A](val asOption: Option[A])

object OptArg {
  def NoArg[A] = new OptArg[A](None)
  implicit def fromValue[A](x: A): OptArg[A] = new OptArg(Some(x))
  // optional
  implicit def toOption[A](arg: OptArg[A]) = arg.asOption
}

// use
def age(years: OptArg[String] = NoArg) = years.asOption match {
   ...
}

Here asOption call is necessary, but thanks to the toOption implicit conversion you could e.g. call Option methods on years directly.

Sure, overloading can work better in simple cases like this, but not so much if you have many optional arguments (or even many non-optional arguments which will have to be duplicated).

I am using this technique in a codebase at work and liking it: Scala Option symbolic syntax

It allows things like:

// Using type ?[A] instead of Option[A]
def age(years: ?[Int] = None): String = ...

// Using expr.? instead of Some(expr)
println(age(10.?))
1 Like