io7m | archive (zip, signature)

Type Tricks 2 - Interfaces as capabilities

Interface types in Java are typically used to allow programmers to write functions that generalize over a set of types. As an example, the programmer can write a function that takes a List<Comparable<T>>. The programmer doesn't care if the actual implementation of the list is an array, a linked list, or a binary tree - only that it looks and behaves like a list. The programmer also doesn't care about the type of the elements of the list - only that the elements of the list can be compared. The use of interface types makes a statement about the parametricity of the function - "this function works correctly with anything that looks and acts like a list of comparable objects".

However, it's also possible to use interface types to make statements about what the function will not do. For example, if some type A implements interfaces J and K, and values of type A can only be modified via the functions contained in K, then the programmer has a high degree of confidence that a value of type A passed to a function that takes a J will not modify A!

Essentially, when used in this manner, interface types become capabilities (in the sense of capabilities in information security). If a piece of code has access to a value of type T (where T may be an interface type), then it has the capability to perform all operations defined for type T. As a given type may implement any number of interfaces, it becomes possible to separate a type into interfaces and then write code that only depends on a subset of those interfaces. This means that code can be written in a "least-privilege" style, where individual functions only have access to the bare minimum number of operations that they need in order to work.


An example, taken from the jtensors package, is of vectors with "readable" interfaces. The VectorM4D type provides a mutable four-element vector of double precision real numbers. Many functions written using vectors do not need to modify the vectors in question and therefore the VectorM4D type implements the VectorReadable4DType interface. The programmer can pass values of type VectorM4D to functions that take values of type VectorReadable4DType, confident in the knowledge that the functions cannot modify the given vectors. The use of this interface also acts as documentation on the usage of a function: Clearly, if a function f takes a VectorReadable4DType, then it's obviously expecting that vector to contain meaningful values of some kind because all it can do is read from the vector. If f simply took a VectorM4D, there would be no way to know from the type alone whether or not the programmer was supposed to write anything to the vector beforehand.

Another example, taken from the jvvfs package, is of a virtual filesystem implementation separated into interfaces representing capabilities. As an example, code that needs to read from the filesystem can be written to take values of type FSCapabilityReadType, whereas code that needs to be able to mount archives into the filesystem can be written to take values of type FSCapabilityMountDirectoryType. As stated, this allows programmers to restrict what particular parts of the code are allowed to do when passed references to filesystems.

Combining Interfaces with Generics

It's also possible to write functions that take ad-hoc combinations of interfaces using an apparently little-known feature of Java generics. Essentially, the & symbol is used to declare the union of multiple interfaces given as type parameters to a function or class. Using jvvfs as an example, the following function unambiguouly declares that it both reads from the filesystem and creates directories in the given filesystem:

<T extends FSCapabilityReadType & FSCapabilityCreateDirectoryType>
readAndCreate(T filesystem)

This technique is used heavily in io7m packages to give reasonably strong specifications for functions.


Programmers are encouraged to break up their classes into multiple interfaces in order to control, statically, how different parts of their systems can communicate. It's recommended that most classes initially be broken into "readable" interfaces (that allow reading from values but not modifying) and "writable" interfaces (that allow writing to values but not reading). These interfaces can then be split further depending on requirements.