The Future [Either [AppError, Option [User]]] in Scala

As mentioned in the title, does it make sense to use such a data structure? Let me explain one by one:

  • The future is for introducing asynchronous computing
  • Or - to report known errors
  • Option - inform that the value may be absent.

I'm a little scared when I look at this. Is it good to use such a combination of types?

+7
scala functional-programming
source share
3 answers

Let's look at the solution space:

Success(Right(Some(user))) => Everythig OK, got an user Success(Right(None)) => Everything OK, no user Success(Left(AppError)) => Something went wrong at app level Failure(Exception) => Something went wrong 

It looks very expressive, but everything becomes terribly fast when you try to create such a nested structure using other calls (see Converting the lock code to use scala futures ) for an example of compiling Future[Option[T]] )

So, following the principle of least power , we ask ourselves: are there less complex alternatives that preserve semantics? It can be argued that Future[User] may be sufficient if we take advantage of the full potential of exclusion (and the hierarchy of exceptions).

Check:

 Everythig OK, got an user => Success(user) Everything OK, no user => Failure(UserNotFoundException) (Application level exception) Something went wrong at app level => Failure(AppException) (Application level exception) Something went wrong => Failure(Exception) (System-level exception) 

The only limitation of this approach is that API users will need to be aware of exceptions that are not self-documenting in the interface. The surface is that having Future based APIs will allow expressive monadic compositions with other Future -based APIs.

+5
source share

As a rule, there is nothing wrong with the proposed API. This gives you the flexibility you need, but you either need to write a decent amount of templates to handle the return type, or use skazazh / cats and monadichesky transformations to extract everything.

But let me try and suggest an additional API.

Define our algebra (or abstract data types):

 // parten me for the terrible name sealed trait DomainEntity case class User(id: UserId) extends DomainEntity case object EmptyUser extends DomainEntity case class UserId(id: String) 

Instead of simulating user non-existence with Option[A] , we use our algebra to define our area.

Now we can set Future[Try[DomainEntity]] , which we can later match for different combinations generated by the API:

 findUserById(UserId("id")).map { case Success(user: User) => // Do stuff with user case Success(EmptyUser) => // We have no user, do something else case Failure(e) => // Log exception? } 
+3
source share

Things like the Future[Either[AppError, Option[User]]] return type may be fine when prototyping, but once you have finished prototyping, you should consider options that provide better readability and expressiveness.

Let's take this Future[Either[AppError, Option[User]]] as an example. Suppose there is a method that has this type of return.

 def fetchUser(userId: UUID): Future[Either[AppError, Option[User]]] 

Now you can either choose a more expressive type hierarchy ... for example,

 // Disclamer : // this is just for pointing you out towards a direction and // I am sure many can propose a better design hierarchy trait Model case class User(id: UUID,....) extends Model // Fetch Result protocol sealed trait FetchModelResult case class FetchModelSuccess(model: Model) extends FetchModelResult sealed trait FetchModelFailure extends FetchModelResult case class ModelNotFound extends FetchModelFailure ... case class FetchModelGenericFailure(ex: Exception) extends FetchModelFailure // App Result protocol sealed trait AppResult case class AppResultSuccess[T](result: T) extends AppResult sealed trait AppResultFailure extends AppResult case class AppResultGenericFailure(ex: Exception) extends AppResultFailure // fetch user problem def fetchUser(userId: UUID): Future[FetchModelResult] = ??? // Notice that we are not using the generic AppError here // This is called segregation of problems // the current problem is fetching the user // so our design is just to represent what can happen while fetching // Now whichever method is using this can come-up with an AppError // or AppResult based on what is gets from here. def fetchUserApiHandler(userId: UUID): Future[AppResult] = fetchUser(userId).map({ case FetchModelSuccess(model) => ..... case FetchModelFailure(ex) => .... }) 

Another option is to use monadic compositions and conversion utilities from scalaz or cats .

Raul Raja Martinez examined similar problems and several ways to confront them in one of his presentations - Team trip through Scala FP emerging models - Run Wild Launch for free

+2
source share

All Articles