Do you know why they replace object by polymorphic variant? In the documentation of the Resource module, it is stated:
I could plead guilty to not liking objects (because they’re rarely the kind of type I need), but I find that the use of polymorphic variant is far more confusing for this use case.
I think I finally find what I was searching in the documentation but, honestly, I did the bad experience to feel like Asterix facing “The Place that Sends you Mad” in his twelves tasks. Was it design by french administration?
My main concern was: how is written the policy access language on resources? It appears that it is indeed the module type language. But to find the policy access rules, you first have to understand it is hidden under a Pi
submodule. For instance, in the Eio.Flow.Pi
you find this module type signature:
module type SINK = struct
type t
val single_write : t -> Cstruct.t list -> int
val copy : t -> src: _ source -> unit
end
And, as far as I understand, it is the method that should satisfied a _ sink
. But it would have been clearer if the row-type variable of sink
was a row-type variable of an object class and not one of a polymorphic variant. We could do so by maintaining the signature language to describe policy access.
Now, suppose you want to define your own sink
resource, and not only use the ones built in Eio, you first have to implements the above SINK
signature. Then you could use the Eio.Flow.Pi.sink
function:
val sink : (module SINK with type t = 't) -> ('t, sink_ty) Resource.handler
and finally, equipped with this handler, all you have to do is package it correctly with a resource using the Resource.T
constructor:
type -'tags t = T : ('t * ('t, 'tags) handler) -> 'tags t
I do not have difficulties to answer your four questions (but I’m not a newcomer, I’m used with type trickery and I know subtyping and row polymorphism), but I still wonder why they used a polymorphic variant 'tags
type (which should have been named 'traits
, since it seemed to be used to implement a kind of traits system in OCaml).
First of all, the [> ...]
despite its appearance is in fact a polymorphic type (hence the error message). Let stay with the _ sink
resources. A sink provide two traits : write and flow, and that’s what states the ty
suffixed type (documentation):
type sink_ty = [` W | `Flow]
and then you have the definition of the row-polymorphic sink
type:
type 'a sink = ([> sink_ty] as 'a) Std.r
The [> ..]
indeed defines a family of types (hence the polymorphism): it is the family of all supertypes of [sink_ty]
, and when you get one such supertype you give it the type variable name 'a
. It may be clearer with this naming scheme:
type 'kind sink = ([> sink_ty] as 'kind) Std.r
where 'kind
is the type of the kind of sink you got, for instance a two_way_ty
one. Basically, a _ sink
value claims implementing the sink_ ty
traits and maybe more (the maybe more is the source of the row type variable). It is the equivalent of the <sink; ..>
object type that states that you must implements the methods of the sink
object and maybe more (hence the ellipsis ..
). If you use OCaml classes, the type #my_class
is an abbreviation for the row-polymorphic <my_class; .. >
object type. And it seems that’s what used Eio before.
Now, you may wonder why using row-polymorphism?[1] It is for subtyping (a core feature of capabilities system: the principle is to delegate some of the rights you own on a resource, but not necessarily all your rights, hence you must have subtyping) and convenience: without row-polymorphism you have to explicitly upcast your values.
Suppose you have a two_way (a supertype of sink) value v : _ two_way
then you can use copy_string
directly:
copy_string "OCaml" v
without such polymorphism you will have to explicitly cast your value:
copy_string "OCaml" (v :> sink)
There is still something that I can’t explain to myself: why using polymorphic variant and not object? With object we already have a type hierarchy (objects understand subtyping) and here we have to mimic this hierarchy by creating a mirror of it (contravariantly) in the polymorphic variant world. The Resource
module implements its own dynamic method dispatch with these two functions:
val get : ('t, 'tags) handler -> ('t, 'impl, 'tags) pi -> 'impl
val get_opt : ('t, _) handler -> ('t, 'impl, _) pi -> 'impl option
So why doing by hand what we have for free with objects? What are the benefits of all the type trickery implemented in the Resource
module? I’m pretty sure that no newcomers (even most OCaml programmers not used with advanced uses of type system) can understand it, and yet it’s the heart of the whole capabilities system. In fact, a Std.r
(that is in fact a Resource.t
) is an existential GADT type equivalent to an object type. Usually I don’t like objects because I then lost access to the packed value (the one of type 't
in the GADT) but here, it’s a feature of a capabilities system to hide the resource in order to avoid privilege escalation.
[1]: The use of first-class module to avoid row-polymorphism seems difficult to practice since methods of resources may consume others resources, resource may contain resources and so on.