Why and how to migrate case class to extractor in Scala

Viewed 128

In the Programming in Scala discussion on when to use case classes and when extractors, they say

Extractors break [the] link between data representations and patterns ... This property is called representation independence ...

If your component had defined and exported a set of case classes, you'd be stuck with them because client code could already contain pattern matches against these classes ...

Fortunately, you need not decide right away. You could always start with case classes and then, if the need arises, change to extractors. Because patterns over extractors and patterns over case classes look exactly the same in Scala, pattern matches in your clients will continue to work.

I'm trying to figure out what this looks like. Suppose, to use their example, that I define emails using

case class Email(user: String, domain: String)

and client Bob and I both and match against it like

def send(addr: Email) = addr match {
  case Email(user, domain) => ...
}

send(Email("joel", "gmail.com"))

Then suppose I decide to switch to extractors to gain representation independence as

// case class Email(user: String, domain: String)

object Email {
  def apply(user: String, domain: String) = user + "@" + domain

  def unapply(addr: String) = addr split "@" match {
    case Array(user: String, domain: String) => Some(user, domain)
    case _ => None
  }
}

this all works except for a compilation failure in send: "not found: type Email". I can fix this with

type Email = String

but is this the right way? And I don't quite see how this has helped me gain representation independence. Sure, Bob can now match against "joel@gmail.com" or Email("joel", "gmail.com") (which are now equivalent), but I have to assume they're using Emails.

1 Answers

I am assuming by representation independence meant that you can store things inside Email whichever way you wanted, but you still need to have this separate type Email... if you want to return Email from apply and extract from it in unapply:

class Email(private val internal: String)

object Email {
  def apply(user: String, domain: String): Email = new Email(user + "@" + domain)

  def unapply(email: Email): Option[(String, String)] =
    email.internal split "@" match {
      case Array(user: String, domain: String) => Some((user, domain))
      case _                                   => None
    }
}

This way you can write:

val email: Email = Email("my", "email.com")

email match {
  case Email(user, domain) => ...
}

while being able to change internals of Email whichever you like. But still, there has to be some Email type, and whether it is trait, class and what is inside should be of no concern to your API's user. You can also write type Email = String but type alias is not a "newtype", so you can put any String there and compiler will not comply. Whether you want it or not is for you to decide.

Related