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

无人久伴 提交于 2020-02-01 03:48:45

问题


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

  • Future - to represent async computation
  • Either - to communicate known errors
  • Option - to communicate that the value may not be present

I am a little bit scared when looking at this. Is it a good practice to use such type combination?


回答1:


Let's have a 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

This looks very expressive, but things get ugly fast when you try to compose such nested structure with other calls (see Converting blocking code to using scala futures) for an example of composingFuture[Option[T]])

So following the principle of the least power, we ask ourselves: Are there less complex alternatives that preserve the semantics? One could argue that Future[User] could be sufficient if we make use of the full potential of exception (and exception hierarchies).

Let's 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 the users of the API will need to be aware of the Exceptions, which is not self-documented in the interface. The upper-hand is that having an API based on Futures will allow expressive monadic compositions with other Future-based APIs.




回答2:


Generally, there is nothing wrong with the proposed API. It gives you exactly the flexibility you need, but requires you to either write a decent amount of boilerplate to deal with the return type, or use scalaz/cats and monadic transformations to extract everything.

But, Let me try and propose an additional API.

Let's 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 modeling the non-existence of a user with an Option[A], we use our algebra to define our domain.

Now, we can expose a 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:


Things like Future[Either[AppError, Option[User]]] return type can be ok at the time of prototyping things but once you are done with prototyping you should think of options which offer better readability and expressibility.

Lets take this Future[Either[AppError, Option[User]]] as an example. Lets say there is a method which has this return type.

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

Now, you can either choose to create 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) => ....
  })    

The other option will be to use monadic composition and transformation utilities from scalaz or cats.

Raúl Raja Martínez has addressed similar problems and few ways to counter these in one of his presentations - A team's journey over Scala's FP emerging patterns - Run Wild Run Free



来源:https://stackoverflow.com/questions/40283042/futureeitherapperror-optionuser-in-scala

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!