Skip to content

Commit bb47407

Browse files
authored
Merge pull request #553 from talex5/variants
Replace objects with variants
2 parents 47f4d20 + 95c91c0 commit bb47407

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2212
-1426
lines changed

README.md

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,19 +1524,26 @@ See Eio's own tests for examples, e.g., [tests/switch.md](tests/switch.md).
15241524
## Provider Interfaces
15251525

15261526
Eio applications use resources by calling functions (such as `Eio.Flow.write`).
1527-
These functions are actually wrappers that call methods on the resources.
1527+
These functions are actually wrappers that look up the implementing module and call
1528+
the appropriate function on that.
15281529
This allows you to define your own resources.
15291530

15301531
Here's a flow that produces an endless stream of zeros (like "/dev/zero"):
15311532

15321533
```ocaml
1533-
let zero = object
1534-
inherit Eio.Flow.source
1534+
module Zero = struct
1535+
type t = unit
15351536
1536-
method read_into buf =
1537+
let single_read () buf =
15371538
Cstruct.memset buf 0;
15381539
Cstruct.length buf
1540+
1541+
let read_methods = [] (* Optional optimisations *)
15391542
end
1543+
1544+
let ops = Eio.Flow.Pi.source (module Zero)
1545+
1546+
let zero = Eio.Resource.T ((), ops)
15401547
```
15411548

15421549
It can then be used like any other Eio flow:
@@ -1549,34 +1556,6 @@ It can then be used like any other Eio flow:
15491556
- : unit = ()
15501557
```
15511558

1552-
The `Flow.source` interface has some extra methods that can be used for optimisations
1553-
(for example, instead of filling a buffer with zeros it could be more efficient to share
1554-
a pre-allocated block of zeros).
1555-
Using `inherit` provides default implementations of these methods that say no optimisations are available.
1556-
It also protects you somewhat from API changes in future, as defaults can be provided for any new methods that get added.
1557-
1558-
Although it is possible to *use* an object by calling its methods directly,
1559-
it is recommended that you use the functions instead.
1560-
The functions provide type information to the compiler, leading to clearer error messages,
1561-
and may provide extra features or sanity checks.
1562-
1563-
For example `Eio.Flow.single_read` is defined as:
1564-
1565-
```ocaml
1566-
let single_read (t : #Eio.Flow.source) buf =
1567-
let got = t#read_into buf in
1568-
assert (got > 0 && got <= Cstruct.length buf);
1569-
got
1570-
```
1571-
1572-
As an exception to this rule, it is fine to use the methods of `env` directly
1573-
(e.g. using `main env#stdin` instead of `main (Eio.Stdenv.stdin env)`.
1574-
Here, the compiler already has the type from the `Eio_main.run` call immediately above it,
1575-
and `env` is acting as a simple record.
1576-
We avoid doing that in this guide only to avoid alarming OCaml users unfamiliar with object syntax.
1577-
1578-
See [Dynamic Dispatch](doc/rationale.md#dynamic-dispatch) for more discussion about the use of objects here.
1579-
15801559
## Example Applications
15811560

15821561
- [gemini-eio][] is a simple Gemini browser. It shows how to integrate Eio with `ocaml-tls` and `notty`.
@@ -1729,9 +1708,8 @@ Of course, you could use `with_open_in` in this case to simplify it further.
17291708

17301709
### Casting
17311710

1732-
Unlike many languages, OCaml does not automatically cast objects (polymorphic records) to super-types as needed.
1711+
Unlike many languages, OCaml does not automatically cast to super-types as needed.
17331712
Remember to keep the type polymorphic in your interface so users don't need to do this manually.
1734-
This is similar to the case with polymorphic variants (where APIs should use `[< ...]` or `[> ...]`).
17351713

17361714
For example, if you need an `Eio.Flow.source` then users should be able to use a `Flow.two_way`
17371715
without having to cast it first:
@@ -1741,13 +1719,13 @@ without having to cast it first:
17411719
(* BAD - user must cast to use function: *)
17421720
module Message : sig
17431721
type t
1744-
val read : Eio.Flow.source -> t
1722+
val read : Eio.Flow.source_ty r -> t
17451723
end
17461724
17471725
(* GOOD - a Flow.two_way can be used without casting: *)
17481726
module Message : sig
17491727
type t
1750-
val read : #Eio.Flow.source -> t
1728+
val read : _ Eio.Flow.source -> t
17511729
end
17521730
```
17531731

@@ -1756,20 +1734,18 @@ If you want to store the argument, this may require you to cast internally:
17561734
```ocaml
17571735
module Foo : sig
17581736
type t
1759-
val of_source : #Eio.Flow.source -> t
1737+
val of_source : _ Eio.Flow.source -> t
17601738
end = struct
17611739
type t = {
1762-
src : Eio.Flow.source;
1740+
src : Eio.Flow.source_ty r;
17631741
}
17641742
17651743
let of_source x = {
1766-
src = (x :> Eio.Flow.source);
1744+
src = (x :> Eio.Flow.source_ty r);
17671745
}
17681746
end
17691747
```
17701748

1771-
Note: the `#type` syntax only works on types defined by classes, whereas the slightly more verbose `<type; ..>` works on all object types.
1772-
17731749
### Passing env
17741750

17751751
The `env` value you get from `Eio_main.run` is a powerful capability,

doc/prelude.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ module Eio_main = struct
4343
end
4444
end
4545

46-
let parse_config (flow : #Eio.Flow.source) = ignore
46+
let parse_config (flow : _ Eio.Flow.source) = ignore

doc/rationale.md

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,32 +125,27 @@ For dynamic dispatch with subtyping, objects seem to be the best choice:
125125
An object uses a single block to store the object's fields and a pointer to the shared method table.
126126

127127
- First-class modules and GADTs are an advanced feature of the language.
128-
The new users we hope to attract to OCaml 5.00 are likely to be familiar with objects already.
128+
The new users we hope to attract to OCaml 5.0 are likely to be familiar with objects already.
129129

130130
- It is possible to provide base classes with default implementations of some methods.
131131
This can allow adding new operations to the API in future without breaking existing providers.
132132

133133
In general, simulating objects using other features of the language leads to worse performance
134134
and worse ergonomics than using the language's built-in support.
135135

136-
In Eio, we split the provider and consumer APIs:
137-
138-
- To *provide* a flow, you implement an object type.
139-
- To *use* a flow, you call a function (e.g. `Flow.close`).
140-
141-
The functions mostly just call the corresponding method on the object.
142-
If you call object methods directly in OCaml then you tend to get poor compiler error messages.
143-
This is because OCaml can only refer to the object types by listing the methods you seem to want to use.
144-
Using functions avoids this, because the function signature specifies the type of its argument,
145-
allowing type inference to work as for non-object code.
146-
In this way, users of Eio can be largely unaware that objects are being used at all.
147-
148-
The function wrappers can also provide extra checks that the API is being followed correctly,
149-
such as asserting that a read does not return 0 bytes,
150-
or add extra convenience functions without forcing every implementor to add them too.
151-
152-
Note that the use of objects in Eio is not motivated by the use of the "Object Capabilities" security model.
153-
Despite the name, that is not specific to objects at all.
136+
However, in order for Eio to be widely accepted in the OCaml community,
137+
we no longer use of objects and instead use a pair of a value and a function for looking up interfaces.
138+
There is a problem here, because each interface has a different type,
139+
so the function's return type depends on its input (the interface ID).
140+
This requires using a GADT. However, GADT's don't support sub-typing.
141+
To get around this, we use an extensible GADT to get the correct typing
142+
(but which will raise an exception if the interface isn't supported),
143+
and then wrap this with a polymorphic variant phantom type to help ensure
144+
it is used correctly.
145+
146+
This system gives the same performance as using objects and without requiring allocation.
147+
However, care is needed when defining new interfaces,
148+
since the compiler can't check that the resource really implements all the interfaces its phantom type suggests.
154149

155150
## Results vs Exceptions
156151

fuzz/fuzz_buf_read.ml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,30 @@ exception Buffer_limit_exceeded = Buf_read.Buffer_limit_exceeded
2626
let initial_size = 10
2727
let max_size = 100
2828

29-
let mock_flow next = object (self)
30-
inherit Eio.Flow.source
29+
module Mock_flow = struct
30+
type t = string list ref
3131

32-
val mutable next = next
33-
34-
method read_into buf =
35-
match next with
32+
let rec single_read t buf =
33+
match !t with
3634
| [] ->
3735
raise End_of_file
3836
| "" :: xs ->
39-
next <- xs;
40-
self#read_into buf
37+
t := xs;
38+
single_read t buf
4139
| x :: xs ->
4240
let len = min (Cstruct.length buf) (String.length x) in
4341
Cstruct.blit_from_string x 0 buf 0 len;
4442
let x' = String.drop x len in
45-
next <- (if x' = "" then xs else x' :: xs);
43+
t := (if x' = "" then xs else x' :: xs);
4644
len
45+
46+
let read_methods = []
4747
end
4848

49+
let mock_flow =
50+
let ops = Eio.Flow.Pi.source (module Mock_flow) in
51+
fun chunks -> Eio.Resource.T (ref chunks, ops)
52+
4953
module Model = struct
5054
type t = string ref
5155

lib_eio/buf_read.ml

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
exception Buffer_limit_exceeded
22

3+
open Std
4+
35
type t = {
46
mutable buf : Cstruct.buffer;
57
mutable pos : int;
68
mutable len : int;
7-
mutable flow : Flow.source option; (* None if we've seen eof *)
8-
mutable consumed : int; (* Total bytes consumed so far *)
9+
mutable flow : Flow.source_ty r option; (* None if we've seen eof *)
10+
mutable consumed : int; (* Total bytes consumed so far *)
911
max_size : int;
1012
}
1113

@@ -45,7 +47,7 @@ open Syntax
4547
let capacity t = Bigarray.Array1.dim t.buf
4648

4749
let of_flow ?initial_size ~max_size flow =
48-
let flow = (flow :> Flow.source) in
50+
let flow = (flow :> Flow.source_ty r) in
4951
if max_size <= 0 then Fmt.invalid_arg "Max size %d should be positive!" max_size;
5052
let initial_size = Option.value initial_size ~default:(min 4096 max_size) in
5153
let buf = Bigarray.(Array1.create char c_layout initial_size) in
@@ -128,17 +130,22 @@ let ensure_slow_path t n =
128130
let ensure t n =
129131
if t.len < n then ensure_slow_path t n
130132

131-
let as_flow t =
132-
object
133-
inherit Flow.source
133+
module F = struct
134+
type nonrec t = t
134135

135-
method read_into dst =
136-
ensure t 1;
137-
let len = min (buffered_bytes t) (Cstruct.length dst) in
138-
Cstruct.blit (peek t) 0 dst 0 len;
139-
consume t len;
140-
len
141-
end
136+
let single_read t dst =
137+
ensure t 1;
138+
let len = min (buffered_bytes t) (Cstruct.length dst) in
139+
Cstruct.blit (peek t) 0 dst 0 len;
140+
consume t len;
141+
len
142+
143+
let read_methods = []
144+
end
145+
146+
let as_flow =
147+
let ops = Flow.Pi.source (module F) in
148+
fun t -> Resource.T (t, ops)
142149

143150
let get t i =
144151
Bigarray.Array1.get t.buf (t.pos + i)

lib_eio/buf_read.mli

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
]}
1010
*)
1111

12+
open Std
13+
1214
type t
1315
(** An input buffer. *)
1416

@@ -21,7 +23,7 @@ type 'a parser = t -> 'a
2123
@raise End_of_file The flow ended without enough data to parse an ['a].
2224
@raise Buffer_limit_exceeded Parsing the value would exceed the configured size limit. *)
2325

24-
val parse : ?initial_size:int -> max_size:int -> 'a parser -> #Flow.source -> ('a, [> `Msg of string]) result
26+
val parse : ?initial_size:int -> max_size:int -> 'a parser -> _ Flow.source -> ('a, [> `Msg of string]) result
2527
(** [parse p flow ~max_size] uses [p] to parse everything in [flow].
2628
2729
It is a convenience function that does
@@ -32,7 +34,7 @@ val parse : ?initial_size:int -> max_size:int -> 'a parser -> #Flow.source -> ('
3234
3335
@param initial_size see {!of_flow}. *)
3436

35-
val parse_exn : ?initial_size:int -> max_size:int -> 'a parser -> #Flow.source -> 'a
37+
val parse_exn : ?initial_size:int -> max_size:int -> 'a parser -> _ Flow.source -> 'a
3638
(** [parse_exn] wraps {!parse}, but raises [Failure msg] if that returns [Error (`Msg msg)].
3739
3840
Catching exceptions with [parse] and then raising them might seem pointless,
@@ -46,7 +48,7 @@ val parse_string : 'a parser -> string -> ('a, [> `Msg of string]) result
4648
val parse_string_exn : 'a parser -> string -> 'a
4749
(** [parse_string_exn] is like {!parse_string}, but handles errors like {!parse_exn}. *)
4850

49-
val of_flow : ?initial_size:int -> max_size:int -> #Flow.source -> t
51+
val of_flow : ?initial_size:int -> max_size:int -> _ Flow.source -> t
5052
(** [of_flow ~max_size flow] is a buffered reader backed by [flow].
5153
5254
@param initial_size The initial amount of memory to allocate for the buffer.
@@ -68,7 +70,7 @@ val of_buffer : Cstruct.buffer -> t
6870
val of_string : string -> t
6971
(** [of_string s] is a reader that reads from [s]. *)
7072

71-
val as_flow : t -> Flow.source
73+
val as_flow : t -> Flow.source_ty r
7274
(** [as_flow t] is a buffered flow.
7375
7476
Reading from it will return data from the buffer,

lib_eio/buf_write.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ exception Flush_aborted
8585

8686
(** {2 Running} *)
8787

88-
val with_flow : ?initial_size:int -> #Flow.sink -> (t -> 'a) -> 'a
88+
val with_flow : ?initial_size:int -> _ Flow.sink -> (t -> 'a) -> 'a
8989
(** [with_flow flow fn] runs [fn writer], where [writer] is a buffer that flushes to [flow].
9090
9191
Concurrently with [fn], it also runs a fiber that copies from [writer] to [flow].

lib_eio/eio.ml

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@ include Eio__core
33
module Debug = Private.Debug
44
let traceln = Debug.traceln
55

6-
module Std = struct
7-
module Promise = Promise
8-
module Fiber = Fiber
9-
module Switch = Switch
10-
let traceln = Debug.traceln
11-
end
12-
6+
module Std = Std
137
module Semaphore = Semaphore
148
module Mutex = Eio_mutex
159
module Condition = Condition
1610
module Stream = Stream
1711
module Exn = Exn
18-
module Generic = Generic
12+
module Resource = Resource
1913
module Flow = Flow
2014
module Buf_read = Buf_read
2115
module Buf_write = Buf_write
@@ -28,17 +22,17 @@ module Fs = Fs
2822
module Path = Path
2923

3024
module Stdenv = struct
31-
let stdin (t : <stdin : #Flow.source; ..>) = t#stdin
32-
let stdout (t : <stdout : #Flow.sink; ..>) = t#stdout
33-
let stderr (t : <stderr : #Flow.sink; ..>) = t#stderr
34-
let net (t : <net : #Net.t; ..>) = t#net
25+
let stdin (t : <stdin : _ Flow.source; ..>) = t#stdin
26+
let stdout (t : <stdout : _ Flow.sink; ..>) = t#stdout
27+
let stderr (t : <stderr : _ Flow.sink; ..>) = t#stderr
28+
let net (t : <net : _ Net.t; ..>) = t#net
3529
let process_mgr (t : <process_mgr : #Process.mgr; ..>) = t#process_mgr
3630
let domain_mgr (t : <domain_mgr : #Domain_manager.t; ..>) = t#domain_mgr
3731
let clock (t : <clock : #Time.clock; ..>) = t#clock
3832
let mono_clock (t : <mono_clock : #Time.Mono.t; ..>) = t#mono_clock
39-
let secure_random (t: <secure_random : #Flow.source; ..>) = t#secure_random
40-
let fs (t : <fs : #Fs.dir Path.t; ..>) = t#fs
41-
let cwd (t : <cwd : #Fs.dir Path.t; ..>) = t#cwd
33+
let secure_random (t: <secure_random : _ Flow.source; ..>) = t#secure_random
34+
let fs (t : <fs : _ Path.t; ..>) = t#fs
35+
let cwd (t : <cwd : _ Path.t; ..>) = t#cwd
4236
let debug (t : <debug : 'a; ..>) = t#debug
4337
let backend_id (t: <backend_id : string; ..>) = t#backend_id
4438
end

0 commit comments

Comments
 (0)