Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Failing in a companion object's apply mehod

Tags:

scala

It's common for Scala classes to have apply and unapply methods in their companion object.

The behaviour of unapply is unambiguous: if its parameter is or can be transformed into a valid instance of the class, return a Some of it. Otherwise, return None.

To take a concrete example, let's imagine a Url case class:

object Url {
  def apply(str: String): Url = ??? 
  def unapply(str: String): Option[Url] = ???
}

case class Url(protocol: String, host: String, path: String)

If str is a valid URL, then unapply will return a Some[Url], otherwise None.

apply is a bit less clear to me, however: how should it react to str not being a valid URL?

Coming from the Java world, my first instinct is to throw an IllegalArgumentException, which would allow us to implement the companion object as:

object Url {
  def apply(str: String): Url = ... // some function that parses a URI and throws if it fails.

  def unapply(str: String): Option[Url] = Try(apply(str)).toOption
}

I understand that this is not considered terribly good practice in the functional world (as explained, for example, in this answer).

The alternative would be to have apply return an Option[Url], in which case it'd be a simple clone of unapply and better left unimplemented.

Is that the correct conclusion to reach? Should this type of potentially failing apply methods simply not be implemented? Is throwing, in this case, considered to be ok? Is there a third option that I'm not seeing?

like image 261
Nicolas Rinaudo Avatar asked Dec 20 '22 14:12

Nicolas Rinaudo


1 Answers

This is a bit subjective, but I don't think you should do either.

Suppose you allow apply to fail, i.e. throw an exception or return an empty option. Then doing val url = Url(someString) might fail, despite looking awfully like a constructor. That's the whole problem: the apply method of a companion object should reliably construct new instances for you, and you simply cannot reliably construct Url instances from arbitrary Strings. So don't do that.

unapply should generally be used to take a valid Url object and return another representation with which you could create a Url again. As an example of this, look at the generated unapply method for case classes, which simply returns a tuple containing the arguments with which it was constructed. So the signature should actually be def unapply(url: Url): String.

So my conclusion is that neither should be used for the construction of a Url. I think it would be the most idiomatic to have a method def parse(str: String): Option[Url] to make explicit what you're actually doing (parsing the string) and that it might fail. You can then do Url.parse(someString).map(url => ...) to use your Url instance.

like image 66
DCKing Avatar answered Jan 07 '23 01:01

DCKing