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 {
case class Anonymous(cookie: String) extends User
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
.