Last active
February 13, 2018 09:35
-
-
Save izmailoff/aa8663de3b9bd787391b3c0adf1d1146 to your computer and use it in GitHub Desktop.
Illustrates the use of the new implementation of Either that supports map, flatMap, etc. It's available since 2.12.0-RC1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class Error(message: String) | |
case class User(id: Int, name: String, accountId: Int) | |
case class Account(id: Int, balance: Double) | |
case class Item(id: Int, title: String, price: Double) | |
case class Failures(isUserFailure: Boolean, isAccountFailure: Boolean, isItemFailure: Boolean) | |
def getUser(id: Int, fail: Boolean = false): Either[Error, User] = | |
if(fail) Left(Error(s"No such user: $id")) else Right(User(id, "Bob", 3)) | |
def getAccount(id: Int, fail: Boolean = false): Either[Error, Account] = | |
if(fail) Left(Error(s"No such account: $id")) else Right(Account(id, 100)) | |
def getItem(id: Int, fail: Boolean = false): Either[Error, Item] = | |
if(fail) Left(Error(s"No such item: $id")) else Right(Item(id, "Book", 25.99)) | |
def purchaseItem(userId: Int, itemId: Int, failureMode: Failures): Either[Error, (Item, Account)] = { | |
import failureMode._ | |
for { | |
user <- getUser(userId, isUserFailure) | |
account <- getAccount(user.accountId, isAccountFailure) | |
item <- getItem(itemId, isItemFailure) | |
} yield (item, account.copy(balance = account.balance - item.price)) | |
} | |
// can also be written as: | |
def purchaseItem(userId: Int, itemId: Int): Either[Error, (Item, Account)] = | |
getUser(userId).flatMap(user => | |
getAccount(user.accountId).flatMap(account => | |
getItem(itemId).map(item => | |
(item, account.copy(balance = account.balance - item.price))))) | |
/// | |
def scenarios: List[Failures] = | |
for { | |
userFlag <- List(true, false) | |
accountFlag <- List(true, false) | |
itemFlag <- List(true, false) | |
} yield Failures(userFlag, accountFlag, itemFlag) | |
scenarios.map(x => (x, purchaseItem(1, 2, x))).foreach(println) | |
def purchaseItem_GetItemFirst(userId: Int, itemId: Int, failureMode: Failures): Either[Error, (Item, Account)] = { | |
import failureMode._ | |
for { | |
item <- getItem(itemId, isItemFailure) | |
user <- getUser(userId, isUserFailure) | |
account <- getAccount(user.accountId, isAccountFailure) | |
} yield (item, account.copy(balance = account.balance - item.price)) | |
} | |
scenarios.map(x => (x, purchaseItem_GetItemFirst(1, 2, x))).foreach(println) | |
////// ADDITIONAL ////// | |
// COMPARE THESE 2 APPROACHES: | |
def purchaseItem(userId: Int, itemId: Int): (Item, Account) = { | |
val user = getUser(userId) | |
if(user.isRight) { // isRight or == null or similar test | |
val account = getAccount(user.value.accountId) | |
if(account.isRight) { | |
val item = getItem(itemId) | |
if(item.isRight) | |
(item, account.copy(balance = account.balance - item.price)) // FINALLY WE CAN RETURN | |
else | |
throw ... | |
} else | |
throw ??? // return null / return Either | |
} else | |
throw new IllegalArgumentException($"No such user: $userId") // or return null or best return: Either.left | |
} | |
// we can flatten this with early `return`: if(==null) return x; but it still leaves lots of boilerplate: | |
def convertLeft[A, B, C](either: Either[A, B]): Either[A, C] = | |
either match { | |
case Left(value) => Left[A, C](value) | |
case _ => ??? | |
} | |
def purchaseItem(userId: Int, itemId: Int): Either[Error, (Item, Account)] = { | |
val user = getUser(userId) | |
if(user.isLeft) | |
return convertLeft(user) | |
val account = getAccount(user.right.get.accountId) | |
if(account.isLeft) | |
return convertLeft(account) | |
val item = getItem(itemId) | |
if(item.isLeft) | |
return convertLeft(item) | |
val it = item.right.get | |
val acnt = account.right.get | |
return Right((it, acnt.copy(balance = acnt.balance - it.price))) | |
} | |
// or hypothetical Java-ish / Python like implementation: | |
def purchaseItem(userId: Int, itemId: Int): (Error, Item, Account) = { | |
val user = getUser(userId) | |
if(user == null) | |
return (Error("no such user"), null, null) // or throw an exception, or just: return null | |
val account = getAccount(user.accountId) | |
if(account == null) | |
return (Error("no such account"), null, null) | |
val item = getItem(itemId) | |
if(item == null) | |
return (Error("no such item"), null, null) | |
return (item, account.copy(balance = acnt.balance - it.price)) | |
} | |
// VS | |
def purchaseItem_GetItemFirst(userId: Int, itemId: Int): Either[Error, (Item, Account)] = | |
for { | |
item <- getItem(itemId) | |
user <- getUser(userId) | |
account <- getAccount(user.accountId) | |
} yield (item, account.copy(balance = account.balance - item.price)) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Running through REPL gives: