I think that isn’t actually the best encoding for the non-capability version because it doesn’t handle multiple uses of Eio_main.run
correctly. I think the best encoding is to use algebraic effects (using the old syntax for brevity):
effect Load : path -> string
effect Save : path * string -> unit
let main_run fn =
Env_main.run
(fun env ->
match fn () with
| () -> ()
| effect Load path, k ->
let data = Eio.Dir.load (Eio.Stdenv.fs env) path in
continue k data
| effect Save(path, data), k ->
Eio.Dir.save (Eio.Stdenv.fs env) path data ~create:(`Exclusive 0o666);
continue k ()
let load path = perform (Load path)
let save path data = perform (Save(path, data))
Given such an interface, you can also implement the capability-based version:
type env = { perf : 'a. 'a eff -> 'a }
type fs = env
let fs env = env
let load { perf } path = perf (Load path)
let save { perf } path data = perf (Save(path, data))
let main_run' fn =
let effect Perf : 'a eff -> 'a in
let effect Tunnel : 'a eff -> 'a in
let env = { perf = fun eff -> perform (Perf eff) } in
match
main_run (fun () ->
match fn env with
| () -> ()
| effect Perf eff, k -> reperform k eff
| effect (Load _| Save _) as eff, k -> reperform k (Tunnel eff))
with
| () -> ()
| Tunnel eff, k -> reperform k eff
The encoding is a bit awkward with the multicore algebraic effect handlers because they lack some more advanced facilities (e.g. shifting) for manipulating handlers.
I think that the “algebraic effect” version is the more natural design if you have an effect system to type things properly, since the types already give you more precise information about which facilities are being used than the capabilities do.
That’s not to say that the capability approach is a bad design though. On the contrary, I think that it might combine very nicely with some of the work we’ve been doing at Jane Street on local allocations, allowing us to have a safe effect handlers without/before having a full blown effect system in the language. I’m very interested in exploring this direction.
I just think that your reasoning that you can build the non-capability version on top of the capability version but not vice versa is incorrect: each version can in fact be implemented in terms of the other.
Eio uses objects internally, while mostly exposing a functional API
It still requires users to understand class syntax to read the API and to decode the more difficult type errors from object types. Taking a method call on every operation also seems like an unnecessary cost. Since all you are using them for is basic higher-order functions accepting products of functions, it would seem better to provide a version using records or first-class modules or just abstract product types, and then add a layer using classes in another module or library if someone actually wants the additional structural typing for some reason.
e.g.
val run_webserver :
www_root:Eio.Dir.t ->
certificates:Eio.Dir.t ->
Eio.Net.listening_socket ->
unit
gives much more information than:
val run_webserver :
unit -[filesystem,network]-> unit
FWIW, whilst earlier prototypes of effect typing for OCaml might look something like that, at this point I would expect any effect system that makes it into the language to have named effects like:
val run_webserver :
unit -[www_root: Eio.Dir.t;
certificates: Eio.Dir.t;
network: Eio.Net.listening_socket] -> unit
Although I’m not sure it would be quite as convenient as capabilities when used at this fine a granularity.