See monadic datatypes. Using monadic data types is much more expressive and understandable than throwing exceptions and it would be the preferred way to declaratively handle all cases without unclear side effects. http://en.wikipedia.org/wiki/Monad_(functional_programming)
The advantage of using failure with success and using a map and flatMap to express a βhappy journeyβ is that exceptions / failures become apparent in the chain.
If you can tell if you call doSomethingMayThrow there may be a side effect (like throwing an exception), it is very clear if you are using a monadic data type as a result.
This will help to look at the real world. I will use this as this is what I have been working on recently:
Consider the monadic future in a caching scenario - if the cache returns a result, you can process the result. If the cache does not return a result, we can go to the actual service from which we are trying to cache the results, and we can express it very explicitly without any unclear implied side effects, such as an exception:
Here recoverWith is similar to flatMap in case of an error (instead of a failure) (return the Future [Model]). recovery is like a map in case of an error (return the model to the future instead of failure). Then the card takes any received model and processes it - all cases are clearly defined in the code, so there is clarity of all cases and conditions in one expression.
(userCacheActor ? GetUserModel(id="abc")) .recoverWith(userServiceActor ? GetUserModel(id="abc")) .recover(new ErrorModel(errorCode=100, body="service error") .map(x: Response => createHttpResponseFromModel(x)) def createHttpResponseFromModel(x: Model) => x match { case model: ErrorModel => ??? case model: UserModel => ??? }
Again, everything is clearly indicated - what to do with the cache failure, what to do if the service cannot respond in this scenario, what to do with the result model at the end of all processing in any case.
Often flatMap talk about the "plumbing" of the monad or the "plumber" of a happy journey. flatMap allows you to take another Monad and return it. This way, you can βtryβ a few scripts and write the code for the happy journey, collecting all the errors at the end.
scala> Option("Something").flatMap(x => Option( x + " SomethingElse")) .flatMap(x => None).getOrElse("encountered None somewhere") res1: String = encountered None somewhere scala> scala.util.Try("tried") .flatMap(x => scala.util.Try( x + " triedSomethingElse")) .flatMap(x => scala.util.Try{throw new Exception("Oops")}) .getOrElse("encountered exception") res2: String = encountered exception