Safer refactoring with newtype

January 27, 2017

One of my favorite advantages of static type systems is how safe they make the process of refactoring.

When I change a function's type signature, the compiler lets me know, before I ever try to run my code, everywhere I need to update my program to work with the change.

Consider the following refactoring; I want setName, which currently takes arguments for first and last names, to take just a single argument for a full name.

def setName(first: String, last: String): Unit

                    ||
                    ||
                    \/

def setName(fullName: String): Unit

This change will ripple compile errors through my program, showing me each place I need to update my calls to it.

Unfortunately, refactoring code does not always imply a type signature change, and API errors can sneak in to my code under the compiler's radar.

Consider the following refactoring; I want to swap the order of two arguments of the same type.

def search(haystack: String, needle: String): Unit

                    ||
                    ||
                    \/

def search(needle: String, haystack: String): Unit

This breaks any uses I have of the search function, but the compiler isn't able to let me know about it because it can't distinguish needle from haystack; they're both Strings.

The problem with this implementaiton of search is that it is "stringly typed".

One way around this is to make discrete record types for needle and haystack, so that this refactoring causes a change in the type signature of search.

Unfortunately, this single-value "boxing" of data leads to an ever-growing pile of new data types, each of which imposes additional CPU and memory overhead.

What I want is a way to distinguish between these values at compile time, but avoid extra computational burden at runtime. It turns out that several languages support this idea, commonly known as "newtypes".

Examples

Go

Go supports type identities via the type keyword.

search.go:

package main

import "fmt"
import "strings"

type Needle string
type Haystack string

func search(n Needle, h Haystack) bool {
  return strings.Contains(string(h), string(n))
}

func main() {

  const n Needle = "needle"
  const h Haystack = "This haystack is nothing but needles!"

  fmt.Println(search(n, h))
}
$ go run search.go
true

Haskell

Haskell supports newtypes via the newtype keyword.

search.hs:

import Data.List (isSubsequenceOf)

newtype Needle = Needle String
newtype Haystack = Haystack String

search :: Needle -> Haystack -> Bool
search (Needle n) (Haystack h) = isSubsequenceOf n h

main :: IO ()
main = do
  let n = Needle "needle"
  let h = Haystack "This haystack is nothing but needles!"
  print $ search n h
$ runhaskell search.hs
True

Scala

Scala supports unboxed tagged types via traits, available through libraries such as Scalaz.

build.sbt:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.8"

search.scala:

import scalaz._
import Tag._

trait Needle
trait Haystack

object Search {
  def apply(n: String @@ Needle, h: String @@ Haystack): Boolean =
    unwrap(h) contains unwrap(n)
}

object Main extends App {

  val n = Tag[String,Needle]("needle")
  val h = Tag[String,Haystack]("This haystack is nothing but needles!")

  println(Search(n, h))
}
$ sbt run
true