Precision on OxCaml Capsule "Key" API

Hi everyone,

While learning about OxCaml Capsule API, I wonder how to get access to the capsule counter after the fork/join, since the key is already consumed in the first anonymous function passed to Parallel.fork_join2?

open Basement

let increment ~access counter =
  let c = Capsule.Data.unwrap ~access counter in
  incr c
;;

let f par =
  let (P key) = Capsule.create () in
  let counter = Capsule.Data.create (fun () -> ref 0) in
  let #(_, _) =
    Parallel.fork_join2
      par
      (fun _ -> Capsule.Key.access key ~f:(fun access -> increment ~access counter))
      (fun _ -> ())
  in
  (* How to have access to the capsule content here? *)
  ()
;;
4 Likes

Hey @Tim-ats-d, thanks for the question. The idea behind what to do here is to thread through a unique reference to the key. In an ideal world, here’s what I’d suggest you to write:

open Basement

let increment ~access counter =
  let c = Capsule.Data.unwrap ~access counter in
  incr c
;;

let f par =
  let (P key) = Capsule.create () in
  let counter = Capsule.Data.create (fun () -> ref 0) in
  let #(key, _) =
    Parallel.fork_join2
      par
      (fun _ ->
        let #(_, key) =
          Capsule.Key.access key ~f:(fun access -> increment ~access counter)
        in
        key)
      (fun _ -> ())
  in
  let _ : _ = Capsule.Key.access key ~f:(fun access -> increment ~access counter) in
  ()
;;

In the future, we expect you’ll be able to write this. But because we don’t yet have layout polymorphism or mode polymorphism (these are features we’re currently working on), you’ll run into some errors. This is the sort of thing that we currently use ppx_template for. But alas, when we wrote the Parallel interface, we didn’t template the functions in it. So we’ll need to do some annoying workarounds.

The first issue is that key has layout void, but Parallel.fork_join2 expects the value you return to have layout value. we can get around this by defining a type with layout value that contains a key within it, like so:

type 'k key_container = K of 'k Capsule.Key.t

The second issue is that Parallel.fork_join2’s return value is always aliased, so we’re essentially forgetting that the key it returns is unique. One way to work around this is to use the module Once from the library unique (interface here). This allows us to perform a dynamic check that we have a unique reference to some value whose mode is aliased.

Altogether, this looks like:

open Basement

let increment ~access counter =
  let c = Capsule.Data.unwrap ~access counter in
  incr c
;;

type 'k key_container = K of 'k Capsule.Key.t

let f par =
  let (P key) = Capsule.create () in
  let counter = Capsule.Data.create (fun () -> ref 0) in
  let #(key, _) =
    Parallel.fork_join2
      par
      (fun _ ->
        let #(_, key) =
          Capsule.Key.access key ~f:(fun access -> increment ~access counter)
        in
        Unique.Once.make (K key))
      (fun _ -> ())
  in
  (* This will raise an exception if it's not unique *)
  let (K key) = Unique.Once.get_exn key in
  let _ : _ = Capsule.Key.access key ~f:(fun access -> increment ~access counter) in
  ()
;;

I’d also like to point out that there’s a higher-level capsule API defined in the library portable (interface here), which is more approriate for most use cases. In particular, have a look at Capsule.Isolated, which is convenient when you have a unique reference to a capsule that contains some data.

6 Likes

Thank you for your detailed response! I suppose you can infer that from the Key.Access signature that returns a pair containing a key with the type equality on 'k preserved. Unfortunately, at this time, there is no explanation of how to reuse a key in the Oxcaml tutorial.

we can get around this by defining a type with layout value that contains a key within it

Fwiw, someone has pointed out to me that this type already exists and is called Capsule.Key.boxed, so key_container is a bit unnecessary.

1 Like