On August 8th and 9th I had the pleasure of attending the Scala by the Bay conference as a speaker. In my talk, “Reasoning With Types,” I discussed how we, as developers, can approach types not as something “just for the compiler” but rather as a tool for us to reason about code.

A good chunk of Box’s codebase is written in Scala—a programming language exceptional in its ability to exhibit a rather expressive type system. With it, we are able to write code so that the types give insight into what the code can or cannot do. We can also use those types to encode stronger guarantees about the behavior of our programs.

Polymorphism and Correctness

As a simple example, consider the following function:

• def foo(xs: List[Int]): List[Int]

Looking at the type signature alone, we cannot infer much about what the function may or may not do. Since the return type is List[Int], and we know how to create arbitrary values of type List[Int], the function could do many things: it could return the list it was passed in, or it might return some random list, or maybe it takes the size of the input list and creates a list of one element. It’s hard to say. Let’s try again.

• def foo[A](xs: List[A]): List[A]

We’ve now removed the information we had on the return type by making the function polymorphic. We no longer know we return a List[Int]; we only know we return a List of some A, depending on what the caller passes in. Because we know nothing about A, we can no longer create arbitrary values of type A, and, hence, we deduce that any A that appears in the output list came from the input list.

Using Stronger Types

How else can we use types to reason about our code? Consider the following function:

• def groupBy[A, K](as: List[A])(f: A => K): Map[K, List[A]]

groupBy takes a List[A] and a function on which to group the elements of the list. Therefore, for each key in the resulting map, we know there is at least one element in the list (the value associated with the key). However, the type of the value is a List, a collection whose semantics covers empty and non-empty lists. While the caller believes that each value of the map is a non-empty list, the type does not imply it. Should the caller wish to begin extracting elements out of that list or otherwise manipulating it, dealing with the possibility of empty semantics of the List can become quite tiresome. Can we do better?

• case class NonEmptyList[A](head: A, tail: List[A])

We can create a data type whose semantics capture strictly a non-empty list; its constructor demands at least one value of type A to create an instance of it. We can then rewrite the above function as:

• def groupBy[A, K](as: List[A])(f: A => K): Map[K, NonEmptyList[A]]

This is much better—the return type now captures the guarantee that every value of the map is non-empty. The statement has switched from “I know” to “the compiler has guaranteed it to be so.” And why not have the compiler work for you? You have to go through it anyway, and you have better things to do than write tests for edge cases!

Increasing Productivity

At the end of the day, we want to write our code and move on with our lives, not spending days on end figuring out which edge case we failed to take into account. By removing extraneous type information from our functions, we make them not only more generic, but we also hint at what the function can or can not do with the type signature alone. Through using more precise types, we are able to statically assert properties of our programs and eliminate any irrelevant edge cases that may have otherwise occurred.

For more detailed information, see the slides from my talk below.

