OxCaml logo Jane Street logo

Some Pitfalls of Uniqueness

This document outlines some common pitfalls that may come up when trying out uniqueness, as well as some suggested workarounds. Over time, this list may grow (as experience discovers new things that go wrong) or shrink (as new compiler versions ameliorate some issues).

If you want an introduction to uniqueness, see the introduction.

Annotating unique return values

When the compiler infers uniqueness, it will only mark values as unique if they are actually used as unique. For example, given a smart constructor:

let mk () = { x = 1 }

The result is not unique by default. This helps the compiler identify static allocations: as long as mk () returns an aliased value, the compiler can lift out the allocation so that mk () returns the same pointer on each invocation. However, you might want to annotate this allocation as unique:

let mk () = ({ x = 1 } : _ @ unique)

Unfortunately, this will not work: while the allocation is now indeed unique, the compiler immediately casts it to aliased so that mk still returns an aliased value. The only way to reliably change the return mode of mk to be unique is through a type signature:

let mk : unit -> t @ unique = fun () -> { x = 1 }

Most times, it will not be necessary to manually annotate a return value as unique. While the compiler defaults the return mode to aliased, it will make the return mode unique if there is any unique use of the result of mk ():

let use () = free (mk ())

This code typechecks even if mk is not annotated, because, now that a need has arisen, the compiler has inferred the return mode of mk to be unique. However, this analysis happens on a per module basis. If we abstract mk into its own module we will get a mode error:

module Mk = struct
  let mk () = { x = 1 }
end

let use () = free (Mk.mk ())
                  ^^^^^^^^^^
Error: This value is "aliased" but expected to be "unique".

When abstracting functions that return unique values into a module, we need to provide an explicit mode annotation in the signature.

Threading unique values

While unique values allow safe mutation, they require a coding style that is much closer to purely functional code than mutating code. For example, you might imagine a function to set values in unique arrays:

val set : 'a array @ unique -> int -> 'a -> unit

which you would use like this:

let set_all_zero arr =
  for i = 0 to Unique_array.size arr do
    Unique_array.set arr i 0
  done

But this does not work! Similar to how closures closing over unique values can only be invoked once, a for-loop may not close over unique values at all:

3 |     Unique_array.set arr i 0
                         ^^^
Error: This value is "aliased" but expected to be "unique".
  Hint: This identifier cannot be used uniquely,
  because it was defined outside of the for-loop.

Indeed, we can not use the same array arr more than once uniquely. Instead, the set function needs to return the new array:

val set : 'a array @ unique -> int -> 'a -> 'a array @ unique

This is a crucial change: since set consumes a unique argument, this removes the only reference we have to the old array. To be able to use the array afterwards, you need set to return another reference. Then we can write:

let set_all_zero arr =
  let rec loop idx arr =
    if idx == 0 then arr
    else loop (idx - 1) (Unique_array.set arr idx 0)
  in
  let size, arr = Unique_array.size arr in
  loop size arr

We are planning a new feature called exclusive mutable that will allow you to use unique values in a more imperative code style similar to the first example.