Bartłomiej Szałach
Bartłomiej Szałach

Reputation: 2453

Convert case class with Options to case class without Options

Having case class with nested Options how do I convert it to its twin case class with no Options by extracting Some’s and replacing None’s by default values.

Let's say we have:

case class UserPreferencesOpt(animals: Option[AnimalPreferencesOpt])
case class AnimalPreferencesOpt(dogs: Option[String], cats: Option[String])

case class UserPreferences(animals: AnimalPreferences)
object UserPreferences {
    def getDefault: UserPreferences(AnimalPreferences("bark", "meow")
}
case class AnimalPreferences(dogs: String, cats: String)

So basically, I’d like to convert:

UserPreferencesOpt(Some(AnimalPreferencesOpt(Some("Dogs are cool!"), None)))

into:

UserPreferences(AnimalPreferences("Dogs are cool!", "meow")

by extracting Options when present and replacing absent with default value.

It can be done by pattern matching and checking value by value, but this is just an example. Obviously, in many cases, there will be much more nested preferences in domain logic and I wonder if it’s possible to convert it without adding extra matches and cases when new preferences are added (eg. WeatherPreferences or even something inside UserPreferences).

Having heard of some advanced Scala stuff like Shapeless or macros I think somebody might recommend it, but is there a way to achieve it in idiomatic, functional, Scala-ish way?

Upvotes: 4

Views: 1521

Answers (4)

vicont
vicont

Reputation: 628

There is a library supports this feature, like this:

import cats.data.Validated
import cats.implicits._
import henkan.optional.all._

case class Message(a: Option[String], b: Option[Int])
case class Domain(a: String, b: Int)

validate(Message(Some("a"), Some(2))).to[Domain]
// res0: henkan.optional.ValidateFromOptional.Result[Domain] = Valid(Domain(a,2))

validate(Message(Some("a"), None)).to[Domain]
// res1: henkan.optional.ValidateFromOptional.Result[Domain] = Invalid(NonEmptyList(RequiredFieldMissing(b)))

Upvotes: 1

Ra Ka
Ra Ka

Reputation: 3055

Another scala-ish way to solve this problem is using implicit conversions. You have to define implicit methods that convert toConvert class to required class e.g. UserPreferenceOpt to UserPreference in their respective companion classes.

  case class UserPreferencesOpt(animals: Option[AnimalPreferencesOpt])
  case class AnimalPreferencesOpt(dogs: Option[String], cats: Option[String])

  case class UserPreferences(animals: AnimalPreferences)
  case class AnimalPreferences(dogs: String, cats: String)

  //Note that, I have used `null` value in case of `None` found in Opt class. Instead of `null`, you can provide default value with some logic here.
  object UserPreferencesOpt {
    implicit def optToUserPref(userPref: UserPreferencesOpt): UserPreferences = UserPreferences(userPref.animals.getOrElse(null))
  }
  object AnimalPreferencesOpt {
    implicit def optToAnimalPref(animalPref: AnimalPreferencesOpt): AnimalPreferences = AnimalPreferences(animalPref.dogs.getOrElse(null), animalPref.cats.getOrElse(null))
  }
  val userPrefOpt:UserPreferencesOpt = UserPreferencesOpt(Some(AnimalPreferencesOpt(Some("Dogs are cool!"), None)))
  val userPref: UserPreferences = userPrefOpt

Upvotes: 1

Juh_
Juh_

Reputation: 15539

If the default are static, then the simpler solution is to fill it at the instance construction, and not having class with Options.

For your example, that would be:

case class UserPreferences(animals: AnimalPreferences)
case class AnimalPreferences(dogs: String, cats: String)

object UserPreferences {
  def apply(dogOpt: Option[String], catOpt: Option[String]): UserPreferences = {
    val dog = dogOpt.getOrElse("bark")
    val cat = catOpt.getOrElse("meow")
    UserPreferences(AnimalPreferences(dog, cat))
  }
}

// then you can create, eg:
UserPreferences(None, None)
UserPreferences("Grrr", None)
UserPreferences("Dogs are cool!", "meow")

Upvotes: 1

Evgeny
Evgeny

Reputation: 1770

You can specify defaults in apply in companion object for target case classes. E.g.:

case class UserPreferencesOpt(animals: Option[AnimalPreferencesOpt] = None)
case class AnimalPreferencesOpt(dogs: Option[String] = None, cats: Option[String] = None)

case class UserPreferences(animals: AnimalPreferences)
object UserPreferences {
  def apply(o: UserPreferencesOpt): UserPreferences = {
    UserPreferences(
      AnimalPreferences(
        o.animals.getOrElse(AnimalPreferencesOpt())
      ))
  }
}
case class AnimalPreferences(dogs: String, cats: String)

object AnimalPreferences {
  def apply(opt: AnimalPreferencesOpt): AnimalPreferences = {
    AnimalPreferences(
      opt.dogs.getOrElse("bark"),
      opt.cats.getOrElse("meow")
    )
  }
}

But I agree with @pedrorijo91 that model looks weird..

Upvotes: 1

Related Questions