igx
igx

Reputation: 4231

Understand how to use apply and unapply

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

Answers (4)

Abhijit Sarkar
Abhijit Sarkar

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:

  • Boolean match
  • Product match

Or U conforms to the type R:

type R = {
  def isEmpty: Boolean
  def get: S
}

Boolean Match

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

Product Match

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

Single Match

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

MJeremy
MJeremy

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

amnn
amnn

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

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

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

Lanny Ripple
Lanny Ripple

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

Related Questions