Newtype vs. newtype in Scala

October 03, 2015

I recently needed to use newtype in a Scala project, and I discovered that in Scalaz it has changed in a significant way since the last time I used it.

The old way

In Scalaz, a type T tagged with Tag used to look like this:

type Tagged[T] = {type Tag = T}
type @@[+T, Tag] = T with Tagged[Tag]

In other words, A @@ T <: A.

Using a utility class Tagger and function tag:

class Tagger[U] {
  def apply[T](t : T) : T @@ U =
    t.asInstanceOf[T @@ U]

def tag[U] = new Tagger[U]

We can define a type Email that is a special tagged version of String:

trait EmailTag
type Email = String @@ EmailTag

Now we can enforce that String instances are Email instances:

def send(to: Email, message: String): Unit = ...

// does not compile, because String != Email
send("foo@bar.baz", "Hello, world!")

// compiles
val email: Email = tag[EmailTag]("foo@bar.baz")
send(email, "Hello, world!")

But we can still use Email instances as String instances:

def show(x: String): Unit = ...

// compiles
val email: Email = tag[EmailTag]("foo@bar.baz")

The new way

Tagging was changed in this patch, evidently due to bugs involving implicit resolution.

Now, a type T tagged with Tag looks like this:

type Tagged[A, T] = {type Tag = T; type Self = A}
type @@[T, Tag] = Tagged[T, Tag]

In other words, A @@ T is no longer a subtype of A.

Our show(x: String) function will no longer accept an Email instance, unless we manually unwrap it first:

// does not compile, because Email is not <: String

def unwrap[A, T](a: A @@ T): A = a.asInstanceOf[A]

// compiles


Both approaches offer the advantages of fast (i.e. no (un)boxing of case classes) newtype functionality, but the latter approach forces the developer to unbox the value whenever they need it to behave as its original type.

I find old-style type inheritance to be a significant benefit of unboxed tagged types, because it lets me treat Emails as Strings without a fuss, so I tend to prefer it for my projects.