Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unsucking Error Handling with Futures

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Unsucking Error Handling with Futures

Avatar for Gary Coady

Gary Coady

October 29, 2015
Tweet

Other Decks in Programming

Transcript

  1. def  userForEmail(email:  String):  Option[User]  =  ???   def  shoeSizeForUser(user:  User):

     Option[Size]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Option[ShoeStyle]    =  ???   def  recommendedShoe(email:  String):  Option[ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  match  {      case  None  =>  println("No  shoe  recommendation")      case  Some(shoe)  =>  println(s"Shoe  recommendation:  $shoe")   }
  2. def  userForEmail(email:  String):  Future[User]  =  ???   def  shoeSizeForUser(user:  User):

     Future[Size]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[ShoeStyle]    =  ???   def  recommendedShoe(email:  String):  Future[ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(shoe)  =>  println(s"Shoe  recommendation:  $shoe")   }
  3. def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(userOpt:  Option[User]):

     Future[Option[Size]]  =  {      userOpt  match  {          case  None  =>  Future.successful(None)          case  Some(user)  =>  ???      }   }   def  recommendedShoeStyleForSize(sizeOpt:  Option[Size]):  Future[Option[ShoeStyle]]  =  {      sizeOpt  match  {          case  None  =>  Future.successful(None)          case  Some(size)  =>  ???      }   }   def  recommendedShoe(email:  String):  Future[Option[ShoeStyle]]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   }
  4. def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(userOpt:  Option[User]):

     Future[Option[Size]]  =  {      userOpt  match  {          case  None  =>  Future.successful(None)          case  Some(user)  =>  ???      }   }   def  recommendedShoeStyleForSize(sizeOpt:  Option[Size]):  Future[Option[ShoeStyle]]  =  {      sizeOpt  match  {          case  None  =>  Future.successful(None)          case  Some(size)  =>  ???      }   }   def  recommendedShoe(email:  String):  Future[Option[ShoeStyle]]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   }
  5. –Dave Thomas “Every piece of knowledge must have a single,

    unambiguous, authoritative representation within a system.”
  6. def  flatMap[B](f:  A  =>  F[B]):  F[B]   Given  an  F[A]:

    case  class  FutureOption[+A](future:  Future[Option[A]])  {      def  flatMap[B](f:  A  =>  FutureOption[B]):  FutureOption[B]  =  {          val  result  =  future  flatMap  {              case  None  =>  Future.successful(None)              case  Some(opt)  =>  f(opt).future          }          FutureOption(result)      }   }
  7. def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(user:  User):

     Future[Option[Size]]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???   def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))   }  yield  shoe recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   }
  8. def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(user:  User):

     Future[Option[Size]]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???   def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))   }  yield  shoe recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   }
  9. def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(user:  User):

     Future[Option[Size]]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???   def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))   }  yield  shoe recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   }
  10. OptionT[F[_], A] Wraps F[Option[A]] Works for any F[_] (as long

    as F[_] is a monad) Implements passing None through the F[_] effect e.g. OptionT[Future, A] is a wrapper for Future[Option[A]]
  11. import  scala.concurrent.Future   import  scala.concurrent.ExecutionContext.Implicits.global
 
 import  scalaz._   import

     Scalaz._   def  userForEmail(email:  String):  Future[Option[User]]  =  ???   def  shoeSizeForUser(user:  User):  Future[Option[Size]]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???   def  recommendedShoe(email:  String):  OptionT[Future,  ShoeStyle]  =  for  {      user  <-­‐  OptionT(userForEmail(email))      size  <-­‐  OptionT(shoeSizeForUser(user))      shoe  <-­‐  OptionT(recommendedShoeStyleForSize(size))   }  yield  shoe recommendedShoe("[email protected]").run  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")   } libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.4"
  12. Either[A, B]: not nearly as useful as it could be.

    In scalaz, use: A \/ B — same as \/[A, B] In cats, use: A Xor B — same as Xor[A, B]
  13. def  userForEmail(email:  String):  Future[String  \/  User]  =  ???   def

     shoeSizeForUser(userOpt:  String  \/  User):  Future[String  \/  Size]  =  {      userOpt.fold(          err  =>  Future.successful(err.left),          user  =>  ???      )   }   def  recommendedShoeStyleForSize(sizeOpt:  String  \/  Size):  Future[String  \/  ShoeStyle]  =  {      sizeOpt.fold(          err  =>  Future.successful(err.left),          size  =>  ???      )   }   def  recommendedShoe(email:  String):  Future[String  \/  ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )   }
  14. def  userForEmail(email:  String):  Future[String  \/  User]  =  ???   def

     shoeSizeForUser(userOpt:  String  \/  User):  Future[String  \/  Size]  =  {      userOpt.fold(          err  =>  Future.successful(err.left),          user  =>  ???      )   }   def  recommendedShoeStyleForSize(sizeOpt:  String  \/  Size):  Future[String  \/  ShoeStyle]  =  {      sizeOpt.fold(          err  =>  Future.successful(err.left),          size  =>  ???      )   }   def  recommendedShoe(email:  String):  Future[String  \/  ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)   }  yield  shoe recommendedShoe("[email protected]")  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )   }
  15. def  userForEmail(email:  String):  Future[String  \/  User]  =  ???   def

     shoeSizeForUser(user:  User):  Future[String  \/  Size]  =  ???   def  recommendedShoeStyleForSize(size:  Size):  Future[String  \/  ShoeStyle]  =  ???   def  recommendedShoe(email:  String):  EitherT[Future,  String,  ShoeStyle]  =  for  {      user  <-­‐  EitherT(userForEmail(email))      size  <-­‐  EitherT(shoeSizeForUser(user))      shoe  <-­‐  EitherT(recommendedShoeStyleForSize(size))   }  yield  shoe recommendedShoe("[email protected]").run  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )   }
  16. Some(3) \/> "no value" == 3.right None \/> "no value"

    == "no value".left Converting Option to \/ (Either)
  17. type  Response[A]  =  EitherT[Future,  Result,  A] Response[String] EitherT[Future,  Result,  String]

    Response[User] Response[UserAndAuthInfo] EitherT[Future,  Result,  User] EitherT[Future,  Result,  UserAndAuthInfo]
  18. type  Response[A]  =  EitherT[Future,  Result,  A] Response[String] EitherT[Future,  Result,  String]

    Response[User] Response[UserAndAuthInfo] Response[Result] EitherT[Future,  Result,  User] EitherT[Future,  Result,  UserAndAuthInfo] EitherT[Future,  Result,  Result]
  19. EitherT[Future,  Result,  Result] def  merge(implicit  ev:  A  =:=  B)  =

     fold(left  =>  ev(left),  right  =>  right) Future[Result]
  20. def  response(block:  =>  Response[Result]):  Action[AnyContent]  =      Action.async(block.merge)  

    def  response[A](bodyParser:  BodyParser[A])(block:  Request[A]  =>  Response[Result]):  Action[A]  =      Action.async(bodyParser)(req  =>  block(req).merge)   def  response(block:  Request[AnyContent]  =>  Response[Result]):  Action[AnyContent]  =      response(BodyParsers.parse.default)(block)  
  21. type  Response[A]  =  EitherT[Future,  Result,  A]   object  Response  {

       def  fromFuture[A](o:  Future[A]):  Response[A]  =          EitherT(o.map(_.right))      def  fromFutureOption[A](noValue:  =>  Result)(o:  Future[Option[A]]):  Response[A]  =          EitherT(o.map(_  \/>  noValue))      def  fromFutureEither[A,  B](err:  A  =>  Result)(o:  Future[A  \/  B]):  Response[B]  =          EitherT(o.map(_.leftMap(err)))      def  fromOption[A](noValue:  =>  Result)(o:  Option[A]):  Response[A]  =          EitherT(Future.successful(o  \/>  noValue))      def  fromEither[A,  B](e:  A  =>  Result)(o:  A  \/  B):  Response[B]  =          EitherT(Future.successful(o.leftMap(e)))      def  fromTry[A](t:  Throwable  =>  Result)(o:  Try[A]):  Response[A]  =  {          val  eitherResult  =  o  match  {              case  scala.util.Success(s)  =>  s.right              case  scala.util.Failure(f)  =>  t(f).left          }          EitherT(Future.successful(eitherResult))      }   }
  22. def  getUserByEmail(email:  String):  Future[String  \/  User]  =  ???   def

     getFavouriteProducts(user:  User):  Future[String  \/  Seq[String]]  =  ???  def  productsByEmail(email:  String)  =  response  {          for  {              user          <-­‐  getUserByEmail(email)            |>  fromFutureEither(s  =>  InternalServerError(s))              products  <-­‐  getFavouriteProducts(user)  |>  fromFuture          }  yield  Ok(views.html.productsByEmail(user,  products))      } type  Response[A]  =  EitherT[Future,  Result,  A]   object  Response  {    def  fromFuture[A](o:  Future[A]):  Response[A]  =          EitherT(o.map(_.right))    def  fromFutureEither[A,  B](err:  A  =>  Result)(o:  Future[A  \/  B]):  Response[B]  =          EitherT(o.map(_.leftMap(err)))   } f(a) == a |> f
  23. sealed  trait  UserServiceError   private[controllers]  case  class  AuthError(why:  String)  extends

     UserServiceError   private[controllers]  case  object  ConnectionError  extends  UserServiceError   object  UserServiceError  {      def  authError(why:  String):  UserServiceError  =  AuthError(why)      val  connectionError:  UserServiceError  =  ConnectionError      def  fold[A](authError:  String  =>  A,  connectionError:  =>  A)(u:  UserServiceError)  =  {          u  match  {              case  AuthError(why)  =>  authError(why)              case  ConnectionError  =>  connectionError          }      }   }
  24. sealed  trait  UserServiceError   private[controllers]  case  class  AuthError(why:  String)  extends

     UserServiceError   private[controllers]  case  object  ConnectionError  extends  UserServiceError def  getUserByEmail(email:  String):  Future[UserServiceError  \/  User]  =  ???   def  getFavouriteProducts(user:  User):  Future[String  \/  Seq[String]]  =  ???    val  userServiceErrorToResult  =  UserServiceError.fold(          authError  =  Forbidden(_),          connectionError  =  InternalServerError("foobar")      )  _      def  productsByEmail(email:  String)  =  response  {          for  {              user          <-­‐  getUserByEmail(email)            |>  fromFutureEither(userServiceErrorToResult)              products  <-­‐  getFavouriteProducts(user)  |>  fromFuture          }  yield  Ok(views.html.productsByEmail(user,  products))      }