Uniqueness Types

Uniqueness Types are an experimental feature available from Idris 0.9.15. A value with a unique type is guaranteed to have at most one reference to it at run-time, which means that it can safely be updated in-place, reducing the need for memory allocation and garbage collection. The motivation is that we would like to be able to write reactive systems, programs which run in limited memory environments, device drivers, and any other system with hard real-time requirements, ideally while giving up as little high level conveniences as possible.

They are inspired by linear types, Uniqueness Types in the Clean programming language, and ownership types and borrowed pointers in the Rust programming language.

Some things we hope to be able to do eventually with uniqueness types include:

  • Safe, pure, in-place update of arrays, lists, etc
  • Provide guarantees of correct resource usage, state transitions, etc
  • Provide guarantees that critical program fragments will never allocate

Using Uniqueness

If x : T and T : UniqueType, then there is at most one reference to x at any time during run-time execution. For example, we can declare the type of unique lists as follows:

data UList : Type -> UniqueType where
     Nil   : UList a
     (::)  : a -> UList a -> UList a

If we have a value xs : UList a, then there is at most one reference to xs at run-time. The type checker preserves this guarantee by ensuring that there is at most one reference to any value of a unique type in a pattern clause. For example, the following function definition would be valid:

umap : (a -> b) -> UList a -> UList b
umap f [] = []
umap f (x :: xs) = f x :: umap f xs

In the second clause, xs is a value of a unique type, and only appears once on the right hand side, so this clause is valid. Not only that, since we know there can be no other reference to the UList a argument, we can reuse its space for building the result! The compiler is aware of this, and compiles this definition to an in-place update of the list.

The following function definition would not be valid (even assuming an implementation of ++), however, since xs appears twice:

dupList : UList a -> UList a
dupList xs = xs ++ xs

This would result in a shared pointer to xs, so the typechecker reports:

unique.idr:12:5:Unique name xs is used more than once

If we explicitly copy, however, the typechecker is happy:

dup : UList a -> UList a
dup [] = []
dup (x :: xs) = x :: x :: dup xs

Note that it’s fine to use x twice, because a is a Type, rather than a UniqueType.

There are some other restrictions on where a UniqueType can appear, so that the uniqueness property is preserved. In particular, the type of the function type, (x : a) -> b depends on the type of a or b - if either is a UniqueType, then the function type is also a UniqueType. Then, in a data declaration, if the type constructor builds a Type, then no constructor can have a UniqueType. For example, the following definition is invalid, since it would embed a unique value in a possible non-unique value:

data BadList : UniqueType -> Type where
     Nil   : {a : UniqueType} -> BadList a
     (::)  : {a : UniqueType} -> a -> BadList a -> BadList a

Finally, types may be polymorphic in their uniqueness, to a limited extent. Since Type and UniqueType are different types, we are limited in how much we can use polymorphic functions on unique types. For example, if we have function composition defined as follows:

(.) : {a, b, c : Type} -> (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

And we have some functions over unique types:

foo : UList a -> UList b
bar : UList b -> UList c

Then we cannot compose foo and bar as bar . foo, because UList does not compute a Type! Instead, we can define composition as follows:

(.) : {a, b, c : Type*} -> (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

The Type* type stands for either unique or non-unique types. Since such a function may be passed a UniqueType, any value of type Type* must also satisfy the requirement that it appears at most once on the right hand side.

Borrowed Types

It quickly becomes obvious when working with uniqueness types that having only one reference at a time can be painful. For example, what if we want to display a list before updating it?

showU : Show a => UList a -> String
showU xs = "[" ++ showU' xs ++ "]" where
  showU' : UList a -> String
  showU' [] = ""
  showU' [x] = show x
  showU' (x :: xs) = show x ++ ", " ++ showU' xs

This is a valid definition of showU, but unfortunately it consumes the list! So the following function would be invalid:

printAndUpdate : UList Int -> IO ()
printAndUpdate xs = do putStrLn (showU xs)
                       let xs' = umap (*2) xs -- xs no longer available!
                       putStrLn (showU xs')

Still, one would hope to be able to display a unique list without problem, since it merely inspects the list; there are no updates. We can achieve this, using the notion of borrowing. A Borrowed type is a Unique type which can be inspected at the top level (by pattern matching, or by lending to another function) but no further. This ensures that the internals (i.e. the arguments to top level patterns) will not be passed to any function which will update them.

Borrowed converts a UniqueType to a BorrowedType. It is defined as follows (along with some additional rules in the typechecker):

data Borrowed : UniqueType -> BorrowedType where
     Read : {a : UniqueType} -> a -> Borrowed a

lend : {a : UniqueType} -> a -> Borrowed a
lend x = Read x

A value can be “lent” to another function using lend. Arguments to lend are not counted by the type checker as a reference to a unique value, therefore a value can be lent as many times as desired. Using this, we can write showU as follows:

showU : Show a => Borrowed (UList a) -> String
showU xs = "[" ++ showU' xs ++ "]" where
  showU' : Borrowed (UList a) -> String
  showU' [] = ""
  showU' [x] = show x
  showU' (Read (x :: xs)) = show x ++ ", " ++ showU' (lend xs)

Unlike a unique value, a borrowed value may be referred to as many times as desired. However, there is a restriction on how a borrowed value can be used. After all, much like a library book or your neighbour’s lawnmower, if a function borrows a value it is expected to return it in exactly the condition in which it was received!

The restriction is that when a Borrowed type is matched, any pattern variables under the Read which have a unique type may not be referred to at all on the right hand side (unless they are themselves lent to another function).

Uniqueness information is stored in the type, and in particular in function types. Once we’re in a unique context, any new function which is constructed will be required to have unique type, which prevents the following sort of bad program being implemented:

foo : UList Int -> IO ()
foo xs = do let f = \x : Int => showU xs
            putStrLn $ free xs
            putStrLn $ f 42
            pure ()

Since lend is implicit, in practice for functions to lend and borrow values merely requires the argument to be marked as Borrowed. We can therefore write showU as follows:

showU : Show a => Borrowed (UList a) -> String
showU xs = "[" ++ showU' xs ++ "]" where
  showU' : Borrowed (UList a) -> String
  showU' [] = ""
  showU' [x] = show x
  showU' (x :: xs) = show x ++ ", " ++ showU' xs

Problems/Disadvantages/Still to do...

This is a work in progress, there is lots to do. The most obvious problem is the loss of abstraction. On the one hand, we have more precise control over memory usage with UniqueType and BorrowedType, but they are not in general compatible with functions polymorphic over Type. In the short term, we can start to write reactive and low memory systems with this, but longer term it would be nice to support more abstraction.

We also haven’t checked any of the metatheory, so this could all be fatally flawed! The implementation is based to a large extent on Uniqueness Typing Simplified, by de Vries et al, so there is reason to believe things should be fine, but we still have to do the work.

Much as there are with linear types, there are some annoyances when trying to prove properties of functions with unique types (for example, what counts as a use of a value). Since we require at most one use of a value, rather than exactly one, this seems to be less of an issue in practice, but still needs thought.