Mike Slinn

Extended Example of For-Comprehensions

— Draft —

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.

  1. Transactional code using Option
  2. Transactional code with error logging and/or default values using Option and orElse
  3. 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/src/main/scala/ForFun3.scala.

The final structure of the code is not presented here, instead we will only walk through the code section by section. I suggest you examine the complete program to see how it is structured.

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 ==.

Scala code
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.

Scala code
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.

Scala code
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.

Scala code
sealed abstract class Inventory(val quantity: Int, val item: InventoryItem) {
  def isNotEmpty = quantity > 0
}

We can define Inventory subclasses now (CharcoalBagInventory, LighterFluidInventory and TofuInventory).

Scala code
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.

Scala code
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.

Scala code
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.

Scala code
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.

When designing APIs you should consider using Try instead 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.

Scala code
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 Inventory­Item 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.

Scala code
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:

Scala code
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).

Scala code
val result = maybeBBQ.fold(orderPizza)(_.grill)
println(result)

You can run this example by typing:

Shell
$ 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.

Scala code
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:

Shell
$ 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.

Scala code
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.

Scala code
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:

Shell
$ 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.


* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.