Skip to content

Instantly share code, notes, and snippets.

@izmailoff
Last active February 13, 2018 09:35
Show Gist options
  • Save izmailoff/aa8663de3b9bd787391b3c0adf1d1146 to your computer and use it in GitHub Desktop.
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.
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))
@izmailoff
Copy link
Author

izmailoff commented Sep 25, 2016

Running through REPL gives:


scala> scenarios.map(x => (x, purchaseItem(1, 2, x))).foreach(println)
(Failures(true,true,true),Left(Error(No such user: 1)))
(Failures(true,true,false),Left(Error(No such user: 1)))
(Failures(true,false,true),Left(Error(No such user: 1)))
(Failures(true,false,false),Left(Error(No such user: 1)))
(Failures(false,true,true),Left(Error(No such account: 3)))
(Failures(false,true,false),Left(Error(No such account: 3)))
(Failures(false,false,true),Left(Error(No such item: 2)))
(Failures(false,false,false),Right((Item(2,Book,25.99),Account(3,74.01))))

scala> scenarios.map(x => (x, purchaseItem_GetItemFirst(1, 2, x))).foreach(println)
(Failures(true,true,true),Left(Error(No such item: 2)))
(Failures(true,true,false),Left(Error(No such user: 1)))
(Failures(true,false,true),Left(Error(No such item: 2)))
(Failures(true,false,false),Left(Error(No such user: 1)))
(Failures(false,true,true),Left(Error(No such item: 2)))
(Failures(false,true,false),Left(Error(No such account: 3)))
(Failures(false,false,true),Left(Error(No such item: 2)))
(Failures(false,false,false),Right((Item(2,Book,25.99),Account(3,74.01))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment