Smartypants: Simple smart constructors for Scala.

Written by Dave Gurnell

Smartypants is a tiny library for defining boilerplate-free smart constructors for Scala ADTs.


Algebraic Data Types are the cornerstone of functional programming in Scala. They provide a simple, robust way to represent data that works well with tools like cats and shapeless that scaffold code from static type information.

ADTs are families of sum types and product types. Sum types are disjunctions of other types, represented in Scala by sealed traits and sealed abstract classes. Product types are conjunctions of other types, represented in Scala as case classes. Here’s a simple example of an ADT that uses both of these relationships:

sealed abstract class User extends Product with Serializable {
  def cookie: String
}

object User {
  case class Anonymous(cookie: String) extends User
  case class LoggedIn(email: String, cookie: String) extends User
}

A User is either Anonymous or LoggedIn. It is always one of these types, and the composition of each type is completely known by the compiler. This allows the compiler to check any code we write that uses User:

// Constructor function to produce an arbitrary user:
def aUser: User = ???

// The compiler checks the exhaustivity of our match,
// along with the types and uses of each field:
aUser match {
  case Anonymous(cookie)       => // do something
  case LoggedIn(email, cookie) => // do something else
}

So where does smartypants come in? Sometimes, when using sum types, the compiler can work itself into a corner by inferring types that are more specific than it needs. Here’s an example:

implicit val userOrdering: Ordering[User] =
  Ordering.by(_.cookie)

val users = List(User.Anonymous("cookie2"), User.Anonymous("cookie1"))
// users: List[User.Anonymous] = List(Anonymous(cookie2), Anonymous(cookie1))

val sorted = users.sorted
// <console>:17: error: No implicit Ordering defined for User.Anonymous.
//        users.sorted
//              ^

The problem here is that users is of type List[User.Anonymous], not List[User] as our ordering requires. We can fix this issue by inserting type annotations:

(users : List[User]).sorted

but Smartypants lets us bypass the problem altogether. The library provides a macro annotation called @smart that defines a “smart constructor” for the subtypes of our sum type:

import smartypants._

sealed abstract class User extends Product with Serializable {
  def cookie: String
}

object User {
  @smart case class Anonymous(cookie: String) extends User
  @smart case class LoggedIn(email: String, cookie: String) extends User
}

In this example, the @smart annotations define two constructor methods called anonymous and loggedIn. Each method takes the same number of parameters as its respective class and returns an instance of the class typed as a User:

object User {
  case class Anonymous(cookie: String) extends User

  def anonymous(cookie: String): User =
    new Anonymous(cookie)

  case class LoggedIn(email: String, cookie: String) extends User

  def loggedIn(email: String, cookie: String): User =
    new LoggedIn(email, cookie)
}

Here’s an example of the difference between the regular and smart constructors for Anonymous. Note the types of a and b:

val a = User.Anonymous("aCookie")
// a: User.Anonymous = Anonymous(aCookie)

val b = User.anonymous("aCookie")
// b: User = Anonymous(aCookie)

By using these smart constructors instead of the regular case class constructors, we can bypass a lot of potentially erroneous type inference:

val users1 = List(User.anonymous("cookie2"), User.anonymous("cookie1"))
// users1: List[User] = List(Anonymous(cookie2), Anonymous(cookie1))

val sorted1 = users1.sorted
// sorted1: List[User] = List(Anonymous(cookie1), Anonymous(cookie2))

You can use @smart to annotate any inner class or object. There are options for configuring the name and return type of the method it generates.

Smart constructors aren’t a new concept. The term, which (like so many other good ideas) we have adopted from Haskell community, generally refers to a constructor function that enforces some kind of correct usage. Smartypants provides a very simple piece of syntax to create a very basic type of constructor, without the boilerplate of a bunch of redundant defs.