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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With