Reputation: 4231
I'm trying to get a better understanding of the correct usage of apply
and unapply
methods.
Considering an object that we want to serialize and deserialize, is this a correct usage (i.e. the Scala way) of using apply
and unapply
?
case class Foo
object Foo {
apply(json: JValue): Foo = json.extract[Foo]
unapply(f: Foo): JValue = //process to json
}
Upvotes: 38
Views: 35207
Reputation: 24593
Note that in Scala 3, unapply
is no longer required to return an Option
. For "Fixed-Arity Extractors", i.e. when the number of elements to be matched are known at compile-time, the signature of unapply
is:
def unapply(x: T): U
The type U
conforms to one of the following matches:
Or U
conforms to the type R
:
type R = {
def isEmpty: Boolean
def get: S
}
object Even:
def unapply(s: String): Boolean = s.size % 2 == 0
"even" match
case s @ Even() => println(s"$s has an even number of characters")
case s => println(s"$s has an odd number of characters")
// even has an even number of characters
class FirstChars(s: String) extends Product:
def _1 = s.charAt(0)
def _2 = s.charAt(1)
// Not used by pattern matching: Product is only used as a marker trait.
def canEqual(that: Any): Boolean = ???
def productArity: Int = ???
def productElement(n: Int): Any = ???
object FirstChars:
def unapply(s: String): FirstChars = new FirstChars(s)
"Hi!" match
case FirstChars(char1, char2) =>
println(s"First: $char1; Second: $char2")
// First: H; Second: i
class Nat(val x: Int):
def get: Int = x
def isEmpty = x < 0
object Nat:
def unapply(x: Int): Nat = new Nat(x)
5 match
case Nat(n) => println(s"$n is a natural number")
case _ => ()
// 5 is a natural number
Source: https://docs.scala-lang.org/scala3/reference/changed-features/pattern-matching.html
Upvotes: 0
Reputation: 1250
The apply
method is like a constructor which takes arguments and creates an object, whereas the unapply
takes an object and tries to give back the arguments.
A simple example:
object Foo {
def apply(name: String, suffix: String) = name + "." + suffix
def unapply(name: String): Option[(String, String)] = {
//simple argument extractor
val parts = name.split("\\.")
if (parts.length == 2) Some(parts(0), parts(1)) else None
}
}
when you call
val file = Foo("test", "txt")
It actually calls Foo.apply("test", "txt")
and returns test.txt
If you want to deconstruct, call
val Foo(name) = file
This essentially invokes val name = Foo.unapply(file).get
and returns (test, txt)
(normally use pattern matching instead)
You can also directly unpack the tuple with 2 variables, i.e.
scala> val Foo(name, suffix) = file
val name: String = test
val suffix: String = txt
BTW, the return type of unapply
is Option
by convention.
Upvotes: 11
Reputation: 3716
Firstly, apply
and unapply
are not necessarily opposites of each other. Indeed, if you define one on a class/object, you don't have to define the other.
apply
is probably the easier to explain. Essentially, when you treat your object like a function, apply is the method that is called, so, Scala turns:
obj(a, b, c)
to obj.apply(a, b, c)
.
unapply
is a bit more complicated. It is used in Scala's pattern matching mechanism and its most common use I've seen is in Extractor Objects.
For example, here's a toy extractor object:
object Foo {
def unapply(x : Int) : Option[String] =
if(x == 0) Some("Hello, World") else None
}
So now, if you use this is in a pattern match like so:
myInt match {
case Foo(str) => println(str)
}
Let's suppose myInt = 0
. Then what happens? In this case Foo.unapply(0)
gets called, and as you can see, will return Some("Hello, World")
. The contents of the Option
will get assigned to str
so in the end, the above pattern match will print out "Hello, world".
But what if myInt = 1
? Then Foo.unapply(1)
returns None
so the corresponding expression for that pattern does not get called.
In the case of assignments, like val Foo(str) = x
this is syntactic sugar for:
val str : String = Foo.unapply(x) match {
case Some(s) => s
case None => throw new scala.MatchError(x)
}
Upvotes: 86
Reputation: 419
So apply and unapply are just defs that have extra syntax support.
Apply takes arguments and by convention will return a value related to the object's name. If we take Scala's case classes as "correct" usage then the object Foo's apply will construct a Foo instance without needing to add "new". You are free of course to make apply do whatever you wish (key to value in Map, set contains value in Set, and indexing in Seq come to mind).
Unapply, if returning an Option or Boolean can be used in match{} and pattern matching. Like apply it's just a def so can do whatever you dream up but the common usage is to extract value(s) from instances of the object's companion class.
From the libraries I've worked with serialization/deserialization defs tend to get named explicitly. E.g., write/read, show/read, toX/fromX, etc.
If you want to use apply/unapply for this purpose the only thing I'd suggest is changing to
def unapply(f: Foo): Option[JValue]
Then you could do something like:
val myFoo = Foo("""{name: "Whiskers", age: 7}""".asJson)
// use myFoo
val Foo(jval) = myFoo
// use jval
Upvotes: 5