Newtype

In the previous post, we briefly covered how a typeclass can be implemented and ended up asking ourselves how multiple implementations of a typeclass could be done for the same data type while not compromising the implicit mechanism (in other words, type class coherency).

This technique has been deeply covered in a Spartan session presented by John De Goes, and this is a small teaser of what one can learn while attending his course:

trait Associative[A] {
  def combine(a0: A, a1: A): A
}
object Associative {
  def apply[A: Associative]: Associative[A] = implicitly[Associative[A]]

  implicit val intAssociative: Associative[Int] = _ + _
  /* which is syntactic sugar for: 
  implicit val intAssociative: Associative[Int] = (a, b) => a + b
  */
  // ...
}

The Associative typeclass describes how two values of the same type can be combined using an associative combinator. In order for a structure to be associative, the combinator must obey the following law:

a + (b + c) === (a + b) + c

This is an aspect of typeclasses we have not mentioned in the previous post. Each typeclass has specific laws that must be satisfied by its implementations (commonly called instances in the functional programming jargon). There is no such thing as a lawless typeclass (otherwise it cannot be referred to as one).

Back to our problem, when it comes to Int, there are two possible implementations to satisfy this associative law:

implicit val intSumAssociative    : Associative[Int] = _ + _
implicit val intProductAssociative: Associative[Int] = _ * _

As we explained it in the previous post, the compiler throws an error whenever it cannot figure out which one of two or more implicits definitions should be selected to satisfy an implicit requirement. Therefore, it may seem at first impossible to create two implicit implementations of a typeclass, in the same implicit scope, for the same data type (eg. Int).

Value Type

One approach to get around this, would consist in creating a value type:

case class Mult(value: Int) extends AnyVal

object Associative {
  implicit val sum    : Associative[Int] = _ + _
  implicit val product: Associative[Mult] = 
    (a0, a1) => Mult(a0.value * a1.value)
}

This is not bad, but very inefficient (in terms of memory usage and ergonomics) even using AnyVal. AnyVal would prevent some extra allocations for sure but won’t get us too far:

def reduce[A: Associative](zero: A, as: List[A]): A = 
  as.fold(zero)(Associative[A].combine)

We would lose the benefits of AnyVal as soon as a List[Mult] is passed to reduce as memory allocation is required when a value value class is used as a type argument:

reduce(Mult(0), List(Mult(1), Mult(2))) // Mult(3)

What we really need is a way to create a new type that can be used in place of another, while being considered different during the implicit lookup, so that an Associative[Mult] is different than an Associative[Int].

Naive approach

Haskell provides a feature called newtype which exactly does that. Let’s try to implement a similar feature in Scala and take a step-by-step approach:

sealed trait Newtype[A] {
  type WrappedType = A
  def wrap(a: A): WrappedType = a
}

Newtype[A] defines a type that can be used in place of an A. It provides a wrap function which given an A returns a Newtype[A].WrappedType. If you look closely, you will notice that WrappedType is a type alias for A:

object Mult extends Newtype[Int]
type Mult = Mult.WrappedType

val m1: Mult = Mult.wrap(1)
def add1(i: Int): Int = i + 1

add1(m1) // 2 

First, an object Mult is defined to represent the newtype. A type alias Mult is then created on Mult.WrappedType which points to A. This works fine, but this does not give us much compared to a simple type alias:

type Mult = Int

val m1: Mult = 1
def add1(i: Int): Int = i + 1

add1(m1) // 2 

And when it comes to implicits, Scala does not make any difference between an Associative[Int] and an Associative[Mult]. The problem here is that the compiler still knows that Mult stands for Int. This connection needs to be broken somehow so they are not considered the same. We could do this using an additional interface:

trait Foo {
  type Bar = Int
  def bar: Bar = 42
}

val foo: Foo = new Foo {}
val i  : Int = foo.bar // compiles

In the above example, Scala knows that Bar is a type alias for Int. Therefore, a Foo#Bar can be used in place of an Int, and these two are considered the same. However, this connection breaks whenever an additional abstraction layer is introduced:

trait Foo {
  type Bar
  def b(): Bar
}

class FooImpl extends Foo { 
  type Bar = Int
  def b(): Bar = 42
}

val foo: Foo = new FooImpl

val i: foo.Bar = foo.b() // compiles
val i: Int     = foo.b() // does not compiles
/*                   ^
error: type mismatch;
 found   : foo.Bar
 required: Int
*/

As shown by this example, Bar and Int become distinct types even though they are the same. This is because foo is defined as a Foo and not a FooImpl. When referring to Foo, there is no way to prove that Foo#Bar points to Int. So foo.b() could technically return anything.

The real newtype

Using what we’ve just learnt, let’s use a similar approach with Newtype[A]:

sealed trait Newtype[A] {
  type WrappedType
  def wrap(a: A): WrappedType
}

val Mult: Newtype[Int] = new Newtype[Int] {
  type WrappedType = Int
  def wrap(a: Int): WrappedType = a
}
type Mult = Mult.WrappedType

val m0: Mult = Mult.wrap(1) // compiles
val m1: Int  = Mult.wrap(1) // does not compile

The connection between Mult and Int is now broken, and they are not considered as the same type by Scala anymore. However, we lost the ability to use a Mult in place of an Int. In some cases that’s exactly what we would like to achieve but not always. For this reason, we could make the distinction between Newtype which is unrelated to the initial type (this is how Haskell implements it), and Subtype which “extends” it:

sealed trait Newtype[A] {
  type WrappedType
  // ...
}

sealed trait Subtype[A] {
  type WrappedType <: A
  // ...
}
// ...
val Mult: Subtype[Int] = new Subtype[Int] {
  type WrappedType = Int
  def wrap(a: Int): WrappedType = a
}
type Mult = Mult.WrappedType

val m0: Mult = Mult.wrap(1) // compiles
val m1: Int  = Mult.wrap(1) // compiles

Note: We won’t go through the Subtype facilities implementation to avoid going too much through similar code. You may refer to the complete code here.

So far, in terms of memory allocation the only cost is the one implied by creating Mult. Wrapping a type to a WrappedType only consists of a function call with no extra allocation. This approach is therefore more contraining than a type alias but at least as efficient memory wise than using a value class.

What about implicit resolution now? Can we declare two implementations of the Associative typeclass for, technically speaking, the same data type? To achieve this, we would need a bit more than just wrap:

sealed trait Newtype[A] {
  // ...
  def unwrap(wt: WrappedType): A
}

val Mult: Newtype[Int] = new Newtype[Int] {
  // ...
  def unwrap(wt: WrappedType): Int = wt
}

implicit val sum    : Associative[Int] = _ + _
implicit val product: Associative[Mult] = 
  (a0, a1) => Mult.wrap(
    Mult.unwrap(a0) * Mult.unwrap(a1)
  )

Let’s see if we can use this with reduce without getting a compilation error:

reduce(1, List(2, 3)) // 2 + 3 = 5

reduce(
  Mult.wrap(1), 
  List(Mult.wrap(2), Mult.wrap(3))
) // 2 * 3 = 6

Great! we’ve just managed to create a type that is more constraining that an type alias, and more efficient memory wise than using a value class. Before going forward, let’s look at how the code looks like so far:

sealed trait Newtype[A] {
  type WrappedType
  def wrap(a: Int): WrappedType
  def unwrap(wt: WrappedType): A
}

val Mult: Newtype[Int] = new Newtype[Int] {
  type WrappedType = Int
  def wrap(a: Int): WrappedType    = a
  def unwrap(wt: WrappedType): Int = wt
}

implicit val sum    : Associative[Int] = _ + _
implicit val product: Associative[Mult] = 
    (a0, a1) => Mult.wrap(
      Mult.unwrap(a0) * Mult.unwrap(a1)
    )

reduce(1, List(2, 3)) // 5
reduce(Mult.wrap(1), List(Mult.wrap(2), Mult.wrap(3))) // 6

Ergonomics

This is great but there is a lot of boilerplate. Declaring a newtype should be at most two lines long. If we think about it, no matter the newtype, the implementation of Newtype[A] is always the same. We could therefore provide an additional module dedicated for that:

object NewtypeModule {
  def newtype[A]: Newtype[A] = new Newtype[A] {
    type WrappedType = A
    def wrap(a: A): WrappedType = a
    def unwrap(wt: WrappedType): A = wt
  }
}

val Mult: Newtype[Int] = NewtypeModule.newtype[Int]

We could also improve the wrapping operation by simply renaming the wrap method to apply:

sealed trait Newtype[A] {
  def apply(a: A): WrappedType
  // ...
}

The API looks already better:

val Mult: Newtype[Int] = NewtypeModule.newtype[Int]
type Mult = Mult.WrappedType

val m0: Mult = Mult(1)

Pattern Matching

There is one last thing we cannot do with a newtype which is pattern matching. Mult is currently a value which prevents writing anything such as:

Mult(42) match {
  case Mult(meaningOfLife) => ???
}

To achieve this, Mult has to be an object implementing a unapply method:

object Mult extends ???
type Mult = Mult.WrappedType

However, Mult can’t extend Newtype[Int] otherwise Scala will establish a link between Mult.WrappedType and Int bringing us back to square one. Newtype[Int] should therefore be provided as a dependent type:

object Mult extends module.Newtype[Int]
type Mult = Mult.WrappedType

module is a value providing a dependent type Newtype[A] and which could be implemented like this:

trait NewtypeModule {
  sealed trait Newtype[A] {
    type WrappedType
    def apply(a: A): WrappedType
    def unwrap(wt: WrappedType): A
  }
}

val module: NewtypeModule = new NewtypeModule {}

Mult can now extends module.Newtype[Int] as described earlier:

object Mult extends module.Newtype[Int] {
  type WrappedType  = Int
  def apply(a: Int): WrappedType = a
  def unwrap(wt: WrappedType): Int = wt
}

Reducing Boilerplate

So far so good, except that we ended up with even more boilerplate :) If we think about it, we don’t really need multiple instances of NewtypeModule. We just need one:

object NewtypeModule {
  val instance: NewtypeModule = new NewtypeModule {}
}

object Mult extends NewtypeModule.instance.Newtype[Int] {
 // ...
}

Secondly, we could factor the implementation of a Newtype[A], as it is the same for every newtype:

trait NewtypeModule {
  def newtype[A]: Newtype[A]
  // ...
}

object NewtypeModule {
  val instance: NewtypeModule = new NewtypeModule {
    def newtype[A]: Newtype[A] = new Newtype[A] {
      type WrappedType  = A
      def apply(a: A): WrappedType = a
      def unwrap(wt: WrappedType): A = wt
    }
  }
}

Finally the API could be improved by creating an abstract class Newtype[A] which delegates the work to NewtypeModule.instance:

trait NewtypeModuleExports {
  import NewtypeModule._
  
  abstract class Newtype[A] extends instance.Newtype[A] {
    val newtype: instance.Newtype[A] = instance.newtype[A]
    type WrappedType = newtype.WrappedType

    def apply(a: A): WrappedType   = newtype(a)
    def unwrap(wt: WrappedType): A = newtype.unwrap(wt)
  }
}

NewtypeModuleExports could be then mixed in a package object to prevent any additional import from the user:

package io.github.francistoth

// if the package is called `newtype`
package object newtype extends NewtypeModuleExports

This makes the declaration of a newtype really easy:

object Mult extends Newtype[Int]
type Mult = Mult.WrappedType

The last piece consists of adding an unapply method to enable pattern matching capabilities for any newtype:

trait NewtypeModule {
  sealed trait Newtype[A] {
    type WrappedType
    def apply(a: A): WrappedType
    def unwrap(wt: WrappedType): A
    def unapply(wt: WrappedType): Option[A] =
      Some(unwrap(wt))
  }
}
// ...
Mult(42) match {
  case Mult(i) => i
} // 42

There we go. The code is available here.

Additional thoughts

In the past, Calvin and myself have been pushing hard to use strong typing everywhere in our code. We used to rely on value types, macros and libraries such as scala-newtype. Knowing the technique we just covered is one more tool under our belt, and is probably the one we’ll use in the future, especially since it will be part of the incoming zio-prelude library.

Credits

Thanks to Calvin, Phil, and John for helping me to write this post and for their mentoring.