2
votes

We have a Scala/Play application in which we have several implicit classes that create Try objects from the Request, e.g.

implicit class RequestUtils[+T](req: Request[T]) {
  def user: Try[User] = // pull the User from the Session, or throw an UnauthorizedException
  def paging: Try[Paging] = // create a Paging object, or throw an IllegalArgumentException
}

We then access the wrapped objects via flatMaps

def route(pathParam: String) = BasicAction {
  request => 
    request.user.flatMap(user => 
      request.paging.flatMap(paging => 
        Try{ ... }
))}

And finally, an ActionBuilder generates a SimpleResult from the Try

case class BasicRequest[A](request: Request[A]) extends WrappedRequest(request)

class BasicActionBuilder extends ActionBuilder[BasicRequest] {
  def invokeBlock[A](request: Request[A], block: (BasicRequest[A]) => Future[SimpleResult]) = {
    block(BasicRequest(request))
  }
}

def BasicAction[T](block: BasicRequest[AnyContent] => Try[T]) = {
  val f: BasicRequest[AnyContent] => SimpleResult = (req: BasicRequest[AnyContent]) =>
    block(req) match {
      case Success(s) => Ok(convertToJson(s))
      case Failure(e: UnauthorizedException) => Unauthorized(e.getMessage)
      case Failure(e: Exception) => BadRequest(e.getMessage)
      case Failure(t: Throwable) => InternalServerError(e.getMessage)
    }

  val ab = new BasicActionBuilder
  ab.apply(f)
}

We're trying to find a way to essentially compose multiple Try objects together (or something along those lines - we're not wedded to using Trys) - the flatMaps are working fine for one or two Trys, but nesting them more than that hampers program readability. We can manually compose the objects together, e.g.

case class UserAndPaging(user: User, paging: Paging)

implicit class UserAndPagingUtils[+T](req: Request[T]) {
  def userAndPaging: Try[UserAndPaging] = req.user.flatMap(user => req.paging.flatMap(paging => UserAndPaging(user, paging))
}

but that's going to result in an explosion of case class + implicit class def combinations. Ideally I'd like to be able to compose multiple Try objects together in an ad hoc fashion, e.g.

def route(pathParam: String) = BasicAction {
  request => compose(request.user, request.paging).flatMap(userWithPaging => ...)
}

and have a Try[User with Paging] magically composed for me, but I have no idea how I'd go about doing this - I've been wrestling with the type system to try to assign a meaningful type to "compose" without any success.

How can I compose multiple Try objects together, or something equivalent using another language construct?

1

1 Answers

6
votes

Trys can be used in for-comprehensions, since they have a flatMap function:

def route(pathParam: String) = BasicAction { request =>
  val userWithPaging =
    for {
      user <- request.user
      paging <- request.paging
    } yield {
      doSomethingWith(user, paging)
    } 
}