Adam Fraser
Adam Fraser

Reputation: 6635

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

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?

Upvotes: 7

Views: 1655

Answers (4)

Channing Walton
Channing Walton

Reputation: 4007

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)))

}

Upvotes: 12

ziggystar
ziggystar

Reputation: 28680

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

Upvotes: 8

Ben James
Ben James

Reputation: 125157

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)

Upvotes: 6

Kristian Domagala
Kristian Domagala

Reputation: 3696

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
}

Upvotes: 2

Related Questions