1

In Scala, I have seen examples like the following in which a function is called if the previous function fails. But how does it work? Eg, below, if find is successful, map is called but if find fails, recover is called. From syntax perspective, it looks that map and recover, both will be called.

userService.find(id).map { user =>
      Ok(success(user.toJson))
    }.recover { case e =>
      errors.toResult(e) // this returns the appropriate result
    }
Manu Chadha
  • 15,555
  • 19
  • 91
  • 184
  • Correct me if i am wrong, i think find(id) is returning you Future[Option[user]]? or the find method is user defined? which return Future[user] or exception! – Raman Mishra Oct 07 '18 at 07:52
  • I just picked the example from SO. But in general you could see such function compositions everywhere in Scala, eg. map and recover in future where map is called if Future returns value and recover is called if Future throws an exception. How one creates such function? – Manu Chadha Oct 07 '18 at 08:08
  • Ok so by the code i can infer that find(id) will return Future[User] or Future[Exception] now you will have to do .map or onComplete or any call back which will be called after the future is completed then you have case Success or Failure in this code we are using recover in case of Failure. – Raman Mishra Oct 07 '18 at 08:20

3 Answers3

2

It's not that functions are selectively called but rather those functions are always called but with their responsibility being same and transparent all the time.

Let's say I have my own Functor called MyFunctor with map and recover.

  • I will write map: A => MyFunctor[B] in a such a way that if the current result is good (A), then apply the function if not, return the alternative result
  • recover would be Throwable => Myfunctor[B]

Example,

  class MyFunctor[+A] {

    import MyFunctor._

    def map[B](fn: A => B): MyFunctor[B] = {
      this match {
        case Good(a) => Good(fn(a))
        case Bad(b) => Bad(b)
      }
    }

    def recover[B >: A](fn: PartialFunction[Throwable, B]): MyFunctor[B] = {
      this match {
        case Bad(b) if fn.isDefinedAt(b) => Good(fn(b))
        case _ => this
      }
    }
  }

  object MyFunctor {

    case class Good[A](data: A) extends MyFunctor[A]

    case class Bad[A](data: Throwable) extends MyFunctor[A]

  }

Now you can chain on map and recover. Both map, and recover are called but they do what they should.

val good = Good(1000).map(_ * 2).recover { case a: Throwable => 11 }
println(good) //Good(2000)

val bad = MyFunctor.Bad[Int](new Exception("error")).map(a => a * 2).recover {
  case e: Throwable => 8
}

println(bad) // Good(8)
prayagupa
  • 30,204
  • 14
  • 155
  • 192
0

The thing is that find returns not a User, but a Future[User].

Future[User] can be either DefaultPromise[User] or never.

Future's map(..) and recover(..) redirect to Try's map(..) and recover(..) via Future's transform(..). And the transform(..) is defined differently for both cases.

trait Future[+T] extends Awaitable[T] {
  def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S] = transform(_ map f)
  def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] = 
    transform { _ recover pf }
  def transform[S](f: Try[T] => Try[S])(implicit executor: ExecutionContext): Future[S]
  //...
}

private[concurrent] trait Promise[T] extends scala.concurrent.Promise[T] with scala.concurrent.Future[T] {
  override def transform[S](f: Try[T] => Try[S])(implicit executor: ExecutionContext): Future[S] = {
    val p = new DefaultPromise[S]()
    onComplete { result => p.complete(try f(result) catch { case NonFatal(t) => Failure(t) }) }
    p.future
  }
  //...
}

class DefaultPromise[T] extends AtomicReference[AnyRef](Nil) with Promise[T] { /*...*/ }

final object never extends Future[Nothing] {
  override def transform[S](f: Try[Nothing] => Try[S])(implicit executor: ExecutionContext): Future[S] = this
  //...
}

https://github.com/scala/scala/blob/2.13.x/src/library/scala/concurrent/Future.scala

Try[User] can be either Failure[User] or Success[User]. And map(..) and recover(..) are defined there differenty for both cases.

sealed abstract class Try[+T] extends Product with Serializable {
  def map[U](f: T => U): Try[U]
  def recover[U >: T](@deprecatedName('f) pf: PartialFunction[Throwable, U]): Try[U]
  //...
}

final case class Failure[+T](exception: Throwable) extends Try[T] {
  override def map[U](f: T => U): Try[U] = this.asInstanceOf[Try[U]]
  override def recover[U >: T](@deprecatedName('rescueException) pf: PartialFunction[Throwable, U]): Try[U] =
    try { if (pf isDefinedAt exception) Success(pf(exception)) else this } catch { case NonFatal(e) => Failure(e) }
  //...
}

final case class Success[+T](value: T) extends Try[T] {
  override def map[U](f: T => U): Try[U] = Try[U](f(value))
  override def recover[U >: T](@deprecatedName('rescueException) pf: PartialFunction[Throwable, U]): Try[U] = this
  //...
}

https://github.com/scala/scala/blob/2.13.x/src/library/scala/util/Try.scala

So basically all cases are dealt with:

  • when 1st call is success (then 2nd call is trivial, you can see above that implementation of recover(..) for Success is trivially just this)
  • when 1st call is failure but 2nd is success
  • when both calls are failures

Maybe you should read more about Try, Future and other monads, monadic chaining of calculations etc.

Error Handling With Try

Welcome to the Future

Promises and Futures in Practice

Futures and Promises

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
0

The important thing to remember is that functions are objects just like any other data. So you can pass a function to another function without actually calling it. A function that takes a function as an argument is known as a higher-order function, sometimes abbreviated to HOF.

In this case both map and recover are higher-order functions; they take a single argument which is a function. Both map and recover are indeed executed, but they don't immediately execute the function that you give them. In normal usage the function passed to map will be called if the find succeeds and the function passed to recover will be called if the find fails. The exact behaviour depends on which object is returned by that find call.

Tim
  • 26,753
  • 2
  • 16
  • 29