How do I pull apart Case Classes filled with Options in Scala

1.6k Views Asked by At

I'm very new to Scala and I'm still trying to get used to the syntax and style, so this is probably a very simple question.

I'm working with a codebase where there are lots of case classes populated with Options like so:

case class Person(
  pants: Option[Pants]
)
case class Pants(
  pocket: Option[Pocket]
)
case class Pocket(
  cash: Option[Cash]
)
case class Cash(
  value: String = "zilch"
)

In the example above, how would you go about returning how much money is in a Person's Pants Pocket, if they are indeed wearing pants... with pockets, and if they have any money at all?

4

There are 4 best solutions below

1
On BEST ANSWER

A great time for for-comprehensions:

val someCash: Option[Cash] =
   for( pants  <- somePerson.pants;
        pocket <- pants.pocket;
        cash   <- pocket.cash ) yield cash

Equivalently you can write the following, for which the first code is syntactic sugar (ignoring some subtleties):

val someCash: Option[Cash] = 
   somePerson.pants.flatMap(_.pocket.flatMap(_.cash))

(I'm not totally sure if you can write the last expression using the _ wildcards, as I did).

0
On

ziggystar's answer is what I would use, but for completeness, pattern matching can also be used, eg,

val someCash: Option[Cash] = person match {
  case Person(Some(Pants(Some(Pocket(Some(cash)))))) => Some(cash)
  case _ => None
}
0
On

The question didn't mention modifying the data, but when you need to do this you quickly find the Scala library doesn't have the tools to make this easy (when the data is immutable). If you haven't experienced this yet, try writing a function which will replace, or modify, the value of the Cash held by a Person, using the types defined in the question.

As described in Tony Morris' Asymmetric Lenses in Scala, lenses are an appropriate solution to this problem.

Here's an example of how we can access and update the value of a person's Cash using the Lens and PLens (partial lens) implementations from the scalaz-seven branch of Scalaz.

First, some boilerplate: define the Lens instance for each field of the case classes. A @-@ B means the same as Lens[A, B].

val pants: Person @-@ Option[Pants] =
  lensG(_.pants, p => ps => p.copy(pants = ps))

val pocket: Pants @-@ Option[Pocket] =
  lensG(_.pocket, ps => p => ps.copy(pocket = p))

val cash: Pocket @-@ Option[Cash] =
  lensG(_.cash, p => c => p.copy(cash = c))

val value: Cash @-@ String =
  lensG(_.value, c => v => c.copy(value = v))

We can't compose all of these lenses, however, because most of the fields are wrapped in Option types.

Partial Lenses to the rescue: these allow us to access and update parts of a structure that may not exist, such as the Some value of an Option, or the head of a List.

We can use the somePLens function from Scalaz 7 to create a partial lens viewing each optional field. In order to compose a partial lens with one of our regular lenses, however, we need to access the equivalent partial lens instance for the regular lens, using the partial method that exists on every Lens.

// @-? is an infix type alias for PLens
val someCash: Pocket @-? Cash = cash.partial andThen somePLens

scala> someCash.get(Pocket(Some(Cash("zilch"))))
res1: Option[Cash] = Some(Cash(zilch))

In the same way, we can create our partial lens viewing the cash held by a Person by composing all our lenses' partial instances, and sandwiching instances of somePLens. Here, I've used the <=< operator, an alias for andThen (which is equivalent to compose with the operands switched).

val someCashValue: Person @-? String =
  pants.partial <=< somePLens <=<
  pocket.partial <=< somePLens <=<
  cash.partial <=< somePLens <=<
  value.partial

Creating a Person instance to play with:

val ben = Person(Some(Pants(Some(Pocket(Some(Cash("zilch")))))))

Using the partial lens to access the value of cash I have:

scala> someCashValue.get(ben)
res2: Option[String] = Some(zilch)

Using the partial lens to modify the value:

scala> someCashValue.mod(_ + ", zero, nada", ben)
res3: Person = Person(Some(Pants(Some(Pocket(Some(Cash(zilch, zero, nada)))))))

Now, if I'm not wearing any pants (!), we can see how an attempt to modify the value of my cash will have no effect:

scala> val ben = Person(None)
ben: Person = Person(None)

scala> someCashValue.mod(_ + ", zero, nada", ben)
res4: Person = Person(None)
4
On

Scalaz 7 has changed a little so here is another example:

  object PartialLensExample extends App {

  import scalaz._
  import Lens._
  import PLens._


  case class Bar(blub: Option[String])
  case class Foo(bar: Option[Bar])

  // normal lenses for getting and setting values
  val fooBarL: Foo @> Option[Bar] = lensg(foo ⇒ bar ⇒ foo.copy(bar = bar), _.bar)
  val barBlubL: Bar @> Option[String] = lensg(bar ⇒ blub ⇒ bar.copy(blub = blub), _.blub)

  // compose the above as 'Partial Lenses', >=> is just an alias for 'andThen'
  val fooBarBlubL: Foo @?> String = fooBarL.partial >=> somePLens >=> barBlubL.partial >=> somePLens

  // try it
  val foo = Foo(Some(Bar(Some("Hi"))))

  println(fooBarBlubL.get(foo)) // Some(Hi)

  println(fooBarBlubL.set(foo, "Bye")) //Foo(Some(Bar(Some(Bye))))

  // setting values
  val foo2 = Foo(None)
  println(fooBarL.set(foo2, Some(Bar(None)))) // Foo(Some(Bar(None)))

}