Published 2014-04-13.
Last modified 2014-06-12.
Time to read: 4 minutes.
This extended example of for-comprehensions brings together many concepts that we have learned and shows alternative ways to use for-comprehensions.
- Transactional code using
Option
- Transactional code with error logging and/or default values using
Option
andorElse
- Transactional code returning success or an error using
Try
/Success
/Failure
The “enrich my library” pattern is used to keep the model free of business logic.
The complete program is provided in
courseNotes/
.
The Model
This extended example models the purchase and usage of materials for a vegetarian backyard barbeque.
A somewhat realistic model defines the following domain objects: Money
, Wallet
,
InventoryItem
, CharcoalBag
, LighterFluid
and Tofu
.
All domain objects are defined using case classes, which is standard for Scala.
Money
is defined in terms of dollars, and supports the following operators:
+
, -
, <=
, >=
and ==
.
case class Money(dollars: Int) { def -(cost: Money): Money = Money(dollars - cost.dollars)
def +(extra: Money): Money = Money(dollars + extra.dollars)
def >=(other: Money): Boolean = dollars >= other.dollars
def <=(other: Money): Boolean = dollars <= other.dollars
def ==(other: Money): Boolean = dollars == other.dollars
override def hashCode = dollars.hashCode }
We need a Wallet
to hold Money
.
case class Wallet(money: Money) { def -(cost: Money): Wallet = Wallet(money - cost)
def +(extra: Money): Wallet = Wallet(money + extra) }
A generic InventoryItem
class is defined as being abstract so no instances are accidently defined.
Three subclasses are defined, and these can be purchased using Money
from the Wallet
.
sealed abstract class InventoryItem(val weight: Int, val price: Money)
case class CharcoalBag(override val weight: Int, override val price: Money) extends InventoryItem(weight, price)
case class LighterFluid(override val weight: Int, override val price: Money) extends InventoryItem(weight, price)
case class Tofu(override val weight: Int, override val price: Money) extends InventoryItem(weight, price)
An Inventory
class holds the store’s inventory and provides methods that allow customers to purchase
InventoryItems
subclasses if they have sufficient Money
in their Wallet
.
Subclasses will be enriched with versions of a buy
method that receives a customer’s Wallet
.
We do not define abstract methods because the return types will differ.
maybeBuy
will return an Option
of a tuple containing a modified Wallet
and the
InventoryItem
purchased, while tryBuy
will return a Try
of the same type of tuple.
maybeBuy
will silently return None
if the customer did not have sufficient funds.
sealed abstract class Inventory(val quantity: Int, val item: InventoryItem) { def isNotEmpty = quantity > 0 }
We can define Inventory
subclasses now (CharcoalBagInventory
,
LighterFluidInventory
and TofuInventory
).
case class CharcoalBagInventory(override val quantity: Int) extends Inventory(quantity, CharcoalBag(5, Money(10)))
case class LighterFluidInventory(override val quantity: Int) extends Inventory(quantity, LighterFluid(2, Money(7)))
case class TofuInventory(override val quantity: Int) extends Inventory(quantity, Tofu(1, Money(2)))
The BBQ
class tries to use the ingredients to light the barbeque and cook the food, if there is a sufficient quantity of ingredients.
Two versions of the light
method are provided in the business logic, described in the next section:
maybeLight
returns an Option[BBQ]
and tryLight
returns a Try[BBQ]
.
As before, the version that returns an Option
fails silently, and the version that returns a
Try
will return an Exception
containing information about the failure if it fails.
case class BBQ(charcoalBag: CharcoalBag, lighterFluid: LighterFluid) { def grill: String = "Yummy dinner!" }
Now we create an instance of Wallet
, put $100 into it, and define some inventory by defining instances of CharcoalBagInventory
, LighterFluidInventory
and TofuInventory
.
var wallet = Wallet(Money(100)) // change to 10 to cause pizza to be ordered due to insufficient funds var charcoalBagInventory = CharcoalBagInventory(100) var lighterFluidInventory = LighterFluidInventory(50) var tofuInventory = TofuInventory(12)
Just in case, we also define a method that handles cooking failure.
def orderPizza: String = "Not pizza again!"
Business Logic
The model has almost no business logic because we use the “enrich my library”
pattern to enhance the domain model with business logic.
We will look at two ways of working with Option
, then we will explore working with Try
.
Hopefully you will realize that Try
is more flexible and just as simple as Option
.
Try
was introduced in Scala 2.10, whereas Option
has been part of Scala for years,
so most libraries still use Option
; when designing APIs you should consider using Try
first in favor of Option
.
Transactional Code Using Option
Here are the enriching classes for the two versions that return Option
.
The RichInventory
class enriches the Inventory
class with the maybeBuy
method
and the RichBBQ
class enriches the BBQ
class with the maybeLight
method.
implicit class RichInventory(inventory: Inventory) { def maybeBuy(wallet: Wallet): Option[(Wallet, InventoryItem)] = if (inventory.isNotEmpty && wallet.money >= inventory.item.price) { val newWallet = wallet - inventory.item.price Some((newWallet, inventory.item)) } else None }
implicit class RichBBQ(bbq: BBQ) { def maybeLight: Option[BBQ] = if (bbq.charcoalBag.weight > 1 && bbq.lighterFluid.weight > 1) { Some(BBQ(bbq.charcoalBag.copy(weight = bbq.charcoalBag.weight - 1), bbq.lighterFluid.copy(weight = bbq.lighterFluid.weight - 1))) } else None }
Now we put it all together using a for-comprehension, then print the result.
If any of the for-comprehension generators returns None
, the remainder of that loop is quietly suppressed.
charcoalBag
and lighterFluid
are explicitly typed as CharcoalBag
and
LighterFluid
, which are both InventoryItem
subclasses.
Notice that bbq
is defined in the for-comprehension; this line is not a generator,
in other words it does not start an inner loop; instead, it is simply a scoped val
.
def maybeBBQ: Option[BBQ] = for {
(wallet2, charcoalBag: CharcoalBag) <- charcoalBagInventory.maybeBuy(wallet)
(wallet3, lighterFluid: LighterFluid) <- lighterFluidInventory.maybeBuy(wallet2)
(wallet4, tofu) <- tofuInventory.maybeBuy(wallet3)
_ <- Some{ wallet = wallet4 } // only reduce wallet contents if all purchases succeed
bbq = BBQ(charcoalBag, lighterFluid)
newBBQ <- bbq.maybeLight
} yield newBBQ
Notice that the discarded container idiom introduced in the previous lecture was employed to assign wallet4
to the variable wallet
defined in an outer scope.
If we had just written wallet = wallet4
then a shadow variable would have been created called
wallet
, and wallet
in the outer scope would not have been updated.
wallet
is only modified if the outer loops succeeded;
this means that wallet
is modified in a transactional style.
Because it is possible that you might purchase all the ingredients and still be unable to light the barbeque,
newBBQ
is defined as a generator returning an Option
;
if it fails then the newBBQ
value for the innermost loop is not added to maybeBBQ
.
We could write the following:
val result = maybeBBQ.map(_.grill).getOrElse(orderPizza) println(result)
The following idiom is considered a better style for accomplishing the same thing
(The fold
method is defined by
Iterable
).
val result = maybeBBQ.fold(orderPizza)(_.grill) println(result)
You can run this example by typing:
$ sbt "runMain ForFun3Option1" Yummy dinner!
If you modify the code such that only 10 dollars is stored into the wallet, instead of 100 dollars, then you will have pizza for dinner instead.
Transactional Code With Error Logging And/Or Default Values Using Option And orElse
This version of the preceding code uses the same RichBBQ
and RichInventory
classes,
and uses the orElse
combinator to add extra logic which handles default values and side-effect code, such as logging.
The first and last orElse
simply output a message before returning None
,
which causes the rest of the loop to be suppressed.
The second and third orElse
return default values instead of None
.
def maybeBBQ: Option[BBQ] = for { (wallet2, charcoalBag: CharcoalBag) <- charcoalBagInventory.maybeBuy(wallet).orElse { println("Too poor to buy charcoal and the next door neighbor has none.") None } (wallet3, lighterFluid: LighterFluid) <- lighterFluidInventory.maybeBuy(wallet2).orElse { println("Borrowed some lighter fluid from next door.") Some((wallet2, LighterFluid(3, Money(0)))) // return default value and pass back unmodified wallet } (wallet4, tofu) <- tofuInventory.maybeBuy(wallet3).orElse { println("Borrowed some tofu from next door.") Some((wallet3, Tofu(1, Money(0)))) // return default value and pass back unmodified wallet } _ <- Some{ wallet = wallet4 } // only spend money (reduce wallet contents) if all purchases succeed bbq = BBQ(charcoalBag, lighterFluid) newBBQ <- bbq.maybeLight.orElse { println("Not enough BBQ material to light it.") None } } yield newBBQ
val result = maybeBBQ.fold(orderPizza)(_.grill) println(result) }
You can run this example by typing:
$ sbt "runMain ForFun3Option2" Yummy dinner!
Again, if you modify the code such that only 10 dollars is stored into the wallet, instead of 100 dollars, then you will have pizza for dinner instead.
Transactional Code Returning Success Or An Error Using Try
Here is a version of the same code that uses Try
/Success
/Failure
.
tryBuy
returns an Exception
if the customer did not have sufficient funds,
which is helpful if you want to know what went wrong in the calling code.
The InsufficientFunds
object uses the efficient NoStackTrace
form of Exception
introduced in the
Try and try/catch/finally lecture in the
Introduction to Scala course.
We use different RichInventory
and RichBBQ
definitions,
because tryBuy
and tryLight
return Try
, not Option
.
We again define two objects for use by tryLight
that extend Exception with NoStackTrace
for efficiency.
import util.{Try, Failure, Success} import util.control.NoStackTrace
implicit class RichInventory(inventory: Inventory) { object InsufficientFunds extends Exception(s"Not enough money in the wallet to make a purchase") with NoStackTrace def tryBuy(wallet: Wallet): Try[(Wallet, InventoryItem)] = if (inventory.isNotEmpty && wallet.money >= inventory.item.price) { val newWallet = wallet - inventory.item.price Success((newWallet, inventory.item)) } else Failure(InsufficientFunds) }
implicit class RichBBQ(bbq: BBQ) { object BBQNoCharcoal extends Exception("Not enough charcoal left to light the BBQ") with NoStackTrace object BBQNoFluid extends Exception("Not enough lighter fluid left to light the BBQ") with NoStackTrace
def tryLight: Try[BBQ] = if (bbq.charcoalBag.weight <= 1) Failure(BBQNoCharcoal) else if (bbq.lighterFluid.weight <= 1) Failure(BBQNoFluid) else Success(BBQ(bbq.charcoalBag.copy(weight = bbq.charcoalBag.weight - 1), bbq.lighterFluid.copy(weight = bbq.lighterFluid.weight - 1))) }
def tryBBQ: Try[BBQ] = for { (wallet2, charcoalBag: CharcoalBag) <- charcoalBagInventory.tryBuy(wallet) (wallet3, lighterFluid: LighterFluid) <- lighterFluidInventory.tryBuy(wallet2) (wallet4, tofu) <- tofuInventory.tryBuy(wallet3) _ <- Try { wallet = wallet4 } // only reduce wallet contents if all purchases succeed bbq = BBQ(charcoalBag, lighterFluid) newBBQ <- bbq.tryLight } yield newBBQ
The following code looks similar to the Option1
and Option2
versions,
with the addition of the recover
block,
and the discarded container idiom uses a Try
container instead of a List
.
Inside that block we have some side effect code, and then the Exception
is passed along to the getOrElse
combinator.
This block is an example of a partial function.
val result = tryBBQ.map(_.grill).recover {
case x => // log the exception and pass it along
println(x)
x
}.getOrElse(orderPizza)
println(result)
You can run this example by typing:
$ sbt "runMain ForFun3Try" Yummy dinner!
Again, if you modify the code such that only 10 dollars is stored into the wallet, instead of 100 dollars, then you will have pizza for dinner instead.
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.