An original programming pattern in Mezzo
- March 11, 2014
Lately, it seems all I've been doing is writing. There's been that paper about the implementation of Mezzo, which I'm supposed to push out at some point. There's that ICFP submission (blog post pending) which we've been writing with some other cool kids from the lab. That's not counting the various presentations, research statements, and other misc. documents that I've been busy with...
Last week, I was (again!) writing, this time working on my part for a journal version of our ICFP paper, and I was trying to write a small tutorial with longer examples and a better introduction to Mezzo. After explaining tail-recursive concatenation for the n-th time, I decided to try out explaining a new example. Here's the result.
The example is about an interesting programming pattern in Mezzo, which I dub "control-flow inversion". The idea is not new: we've been using it for iterators, but is was somehow buried under the details of how iterators work. The present blog post tries to present it in a more self-contained way. It somehow showcases both the strengths and weaknesses of Mezzo as it stands right now. Hopefully we'll improve the shortcomings soon.
An ownership problem
Imagine you want to find one element among a list that satisfies a given predicate. In OCaml, this can be easily done:
let rec find0 (pred: 'a -> bool) (l: 'a list): 'a option =
match l with
| [] -> None
| hd::tl ->
if pred hd then
Some hd
else
find0 pred tl
However, there's an ownership problem here. Assuming an element is found, then doing the following breaks the ownership discipline.
let l = ... in
match find0 ... l with
| Some elt -> (* Ownership problem here! *)
| None -> ...
Indeed, the element elt
is reachable through two different, seemingly distinct paths: the variable elt
itself, and the list l
where the elements remains. In a language that tracks ownership, such as, but not limited to, Mezzo, this is a problem.
Indeed, if you want your find
function to work for any list of a
, then your function cannot make any assumptions on a
, and has to assume that elements of type a
have a unique owner. The find0
function above will thus be rejected by Mezzo.
An alternative solution would be to add an extra constraint on a caller of the function, and require that the type a
be duplicable. There is a mechanism for that in Mezzo. Here, however, we wish to attain a more general solution.
The classic solution
The classic solution is to reverse the control-flow: instead of asking find
to provide us with the element it found, we are going to provide find
with whatever we want to do with the element. That is, we're going to pass a first-class function to find
.
The signature of find
thus becomes, in Mezzo:
val find1: [a] (l: list a, pred: a -> bool, f: a -> ()) -> ()
(We could have f
return an element of type b
and then have find1
return b
as well. We keep it simple for now.)
Remember that in Mezzo, unless marked with consumes
, arguments are preserved (i.e. their ownership is taken and returned). The function above thus preserves a list l
; it takes a predicate and a function f
. The function f
is allowed to operate on an element but must leave it untouched.
The function f
may want to perform side-effects: we can provide a better signature, as follows.
val find2: [a, s: perm] (l: list a, pred: a -> bool, f: (a | s) -> ()) -> ()
One may use find2
as follows.
let l = ... in
(* l @ list (ref int) *)
let found = ref None in
find2 (l,
(fun (x: ref int): bool = !x > 2),
fun (x: ref int | found @ ref (int option)): () =
found := Some (!x)
);
match !found with
| Some x ->
(* Found an integer greater than 2, it's x *)
...
| None ->
...
end
This is awkward for a variety of reasons. First, the control-flow feels very unnatural. We're actually losing control. Second, writing first-class functions requires a lot of annotations. Finally, find2
is actually no more useful than a regular iter
function.
The better solution
Turns out (ta-da!) we can actually do better. We can write this function in "direct style" (i.e. have the soon-to-be find3
return the element to its caller) while still maintaining ownership guarantees. Without any runtime test!
The key insight is to have a restore
function that consumes the ownership of the element in order to regain the ownership of the original list. This, of course, will rely a little bit on dependent types.
alias restore_t (x: term) a (l: term) =
{ p: perm } (
(| consumes (p * x @ a)) -> (| l @ list a) | p
)
An element of type restore_t x a l
is a package of a function along with an existentially-quantified permission p
. The function, in order to be called, requires x @ a * p
, which it consumes. In exchange, one gains l @ list a
.
Phrased differently, this is a one-shot function (p
is existentially-quantified, so the function can only be called once), which consumes x @ a
to obtain l @ list a
.
Since the permission p
is existentially-quantified, we must treat it as affine. Because calling f
requires p
, after f
has been called once, p
is gone, meaning that f
can no longer be called: this is effectively a one-shot function. The iterator blog post refers to this as a magic wand from x @ a
to l @ list a
.
As you may have guessed by now, the signature of find3
is as follows.
val find3: [a] (consumes l: list a, pred: a -> bool) ->
either (| l @ list a) (x: a, restore_t x a l)
The function takes, just like find0
, a list l
and a predicate function pred
. The return type is a sum. In the Left
case, no element has been found, so the unit value is returned, along with full ownership of the list. In the Right
case, the caller does not regain ownership of the list l
. What it gains instead is an element x
(the element that was found) along with the restore
function we just saw.
Here's some sample client code for find3
which, as you can see, is written in the same, natural style as the OCaml code for find0
, except for the call to restore
(but well, our language tracks ownership, so the user will have to talk about ownership too...).
val _ =
(* Create a sample list. *)
let l = cons (newref 1, cons (newref 2, nil)) in
(* Try to find an element greater than 1 *)
match find (l, fun (x: ref int): _ = !x > 1) with
| Left ->
(* No such element has been found *)
()
| Right { contents = (elt, restore) } ->
(* The element [elt] has been found. *)
print elt;
(* Calling the (ghost) [restore] function allows one to give up ownership
* of [elt] and recover ownership of [l] instead. *)
restore ()
end;
(* In any case, we can use [l] afterwards. *)
assert l @ list (ref int)
In can hear the impatient readers eager to see the implementation of find3
. Behold! Here it is.
val rec find3 [a] (consumes l: list a, pred: a -> bool):
either (| l @ list a) (x: a, restore_t x a l)
=
match l with
| Nil ->
(* We found no suitable element. *)
left ()
| Cons { head; tail } ->
if pred head then
(* We found the element we're looking for! *)
right (
head,
fun (| consumes (head @ a * tail @ list a)): (| l @ list a) =
(* Here's permission p: ^^^^^^^^^^^^^ *)
())
else
(* The element we're looking for may be further away. *)
match find3 (tail, pred) with
| Left ->
left ()
| Right { contents = (elt, restore) } ->
let flex s: perm in
right (
elt,
fun (| consumes (elt @ a * head @ a * s)): (| l @ list a) =
(* Here's permission p: ^^^^^^^^^^^^ *)
restore ())
end
end
It's a 20-liner, so don't be afraid, and let's break it down.
Boring case
In the case that we've reached the end of the list, we return the left
injection with the unit type. The ownership of the list is properly returned to the caller: it's the empty list.
Interesting case
Let us now turn to the first interesting case: the element we've hit is precisely the one we were looking for.
We return head
: this corresponds to the x
we wrote in the function signature.
Let us now examine the definition of restore
. It requires the permission for head
: this is line with the definition of restore_t
. The definition of restore_t
also mentions a certain permission p
: in our case, this is tail @ list a
. The restore
function also captures the duplicable permission l @ Cons { head = head; tail = tail }
: duplicable permissions can be captured in Mezzo. Combining these three permissions, without performing any run-time operation, restore
returns full ownership for the list l
.
In Mezzo lingo: head @ a * tail @ list a * l @ Cons { head = head; tail = tail } ≤ l @ list a
.
There an existential-packing involved: the type-checker performs this automatically and packs tail @ list a
as p
(this is real, unedited Mezzo code that I'm showing).
One can glance briefly further down, to see that in the last case, the actual permission p
is a different one. Without the existential quantification, the types would fail to match. The actual value of p
("existential witness") depends on the case.
Actually not-boring case
In the case that the element failed to be found further down the list, the recursive call to find3
returns Left
. Even though the code is terse, let us break down what happens.
In the Left
branch, we learn that we regain full ownership of whatever we called find3
with: in our case, we gain tail @ list a
. The type-checker knows that we still possess head @ a * l @ Cons { head = head; tail = tail }
: it thus recombines the three permissions to show that left ()
actually produces Left { contents = () } | l @ list a
, per the return type of find3
.
There's a lot of under-the-hood machinery that is completely transparent: I'm going to be bold and declare that I find this pretty cool.
Very interesting case
The last case is, naturally, the most interesting one. The element we're looking for has actually been found further down the list. The recursive call provided us with: the element elt
, along with elt @ a
; a function restore
that consumes elt @ a
and returns us tail @ a
; a certain permission that restore
needs in order to be called. It's the existentially-quantified one. Let's name it s
.
We return the element we've found, elt
, along with a function that consumes elt @ a
and returns full ownership of the list. How do we write that function?
That function will consume elt @ a
, which is sort of expected. It also needs other permissions: it needs the "certain permission" s
, in order to call the inner restore
function. We also need head @ a
if we are to piece back everything together to recreate ownership of l
.
Entering the new restore
function, we have elt @ a * s * head @ a
. Calling restore
consumes elt @ a * s
, and gives tail @ a
. The rest of the story is familiar: all is well and ownership of l
is returned.
Wrapping it up
In order for the story to have a happy ending, we must somehow get rid of the run-time operations for restore
which is, in essence, a no-op. Turns out the story's not that good: we don't have ghost functions in Mezzo. That's a pretty big deal actually: it's a major feature to add into the language. One needs to think carefully about termination criteria. In our example, since our data types are inductive, and never co-inductive, one can use the length of the list to show that restore
only ever calls itself with a smaller parameter. But imagine we have integers: should we use an external solver to prove that f(x) < g(x)
? Should we write our own? What about other termination criteria?
So it's a little bit frustrating that the run-time operations are still there. Hopefully that's something that we'll fix at some point. It seems there's still some valuable research to be done about Mezzo...