scalafunctional-programmingnewtypeopaque-types

How to automatically apply modifications to all/some fields of a case class in Scala?


I'm currently challenging myself to skill up in Scala and FP. And today:


Let's say I have the following case class in scala 3:

type EmailAddress = String // I defined them like that to show I'm interested in
type PhoneNumber = String // ... attributes via their names, not via their types.
case class Person(name: String, emails: List[EmailAddress], phones: List[PhoneNumber])

I would like to have a method that automatically transform (almost) all fields. For example, I would like to order emails with the default given instance of Ordering[String] and phones with a specified one. Ideally I should be able to exclude name field.

So I would get something like:

/* Below, I represented the kind of parametrization I would like to be able to do 
 * as parameters of the method orderValues,
 * but it could be annotations or meta-programming instead.
 * 
 * An `orderedPerson` can be directly an instance of Person
 * or something else like an OderedEntity[Person], I don't care so far.
 */
val orderedPerson =
  person.orderValues(
    excluded = Set("name"),
    explicitRules = Map(
      // Phones would have a special ordering (reverse is just a dummy value)
      "phones" -> Ordering.String.reverse
    )
  )

// -----

// So we would get:
Person(
  name = "Xiao",
  emails = List("a@a.a", "a@a.b", "a@b.a"),
  phones = List("+86 100 9000 1000", "+86 100 2000 1000")
)

I haven't used Reflection for a long time and I'm not yet familiar with Meta-Programming, but I'm open to any solution that can help me to achieve that. It's a good opportunity for learning !


[Edit]

My intentional intent was to have a library that can be use to easily anonymize any data.


Solution

  • The type keyword in Scala is just a type alias. You should use a newtype library like https://github.com/estatico/scala-newtype (or opaque type in Scala 3) and derive implicit instances of Ordering from String

    Example with estatico/scala-newtype:

    import io.estatico.newtype.macros.newtype
    import io.estatico.newtype.ops._
    
    @newtype case class Email(string: String)
    object Email {
      implicit val ordering: Ordering[Email] = deriving
    }
    @newtype case class PhoneNumber(string: String)
    object PhoneNumber {
      implicit val ordering: Ordering[PhoneNumber] = deriving[Ordering].reverse
    }
    case class Person(name: String, emails: List[Email], phones: List[PhoneNumber]) {
      lazy val orderValues: Person = this.copy(emails = emails.sorted, phones = phones.sorted)
    }
    
    Person(
      "Xiao",
      List(Email("a@a.a"), Email("a@a.b"), Email("a@b.a")),
      List(PhoneNumber("+86 100 9000 1000"), PhoneNumber("+86 100 2000 1000"))
    ).orderValues