From: Ivan Gotovchits <ivg@ieee.org>
To: Oleg <oleg@okmij.org>, camllist <camllist@inria.fr>
Subject: Re: [Camllist] Implicits for the masses
Date: Wed, 4 Sep 2019 16:41:57 0400
MessageID: <CALdWJ+wpwafYOddNYhTFY5Zz02k4GcWLBmZLGkekuJSMjrdd6Q@mail.gmail.com> (raw)
InReplyTo: <20190904152517.GA2014@Melchior.localnet>
[ Attachment #1: Type: text/plain, Size: 12124 bytes ]
Very interesting and thoughtprovoking writeup, thank you!
Incidentally, we're investigating the same venues, in our CMU BAP project,
as we found out that we need the extensibility in the style of type
classes/canonical structures to decouple complex dependencies which arise
in the program analysis domain.
In fact, we build our new BAP 2.0 framework largely on your
[taglessfinal][1] style which, let's admit it, works much better with type
classes. Therefore we ended up implementing extensible type representations
along with registries for our type classes. Unfortunately, the idea of
storing rules in the registry didn't visit us, but we're now thinking about
how to incorporate it (the classes that we have are very nontrivial,
usually having hundreds of methods, so we're currently using functors to
manually derive on class from another, and registering the resulting
structures  but using your approach we can register functors as well and
automate the derivation). We also didn't generalize the type class
instantiation, so our solutions do have some boilerplate (but I have to
admit, that the total number of type classes that we need is not very big,
so it really never bothered us). What could be surprising is that the
universe of types actually grew quite large, that large that the linear
search in the registry is not an option for us anymore. In fact, we have so
many comparisons between treps, that instead of extracting the extension
constructor number from an extensible variant we had to rely on our own
freshly generated identifier. But I'm running in front of myself, an
important lesson that we have learned is that treps should not only be
equality comparable but also ordered (and even hashable) so that we can
implement our registries as hash tables. It is also better to keep them
abstract so that we can later extend them without breaking user code (to
implement introspection as well as different resolution schemes). This is
basically an elaboration of your approach (which is also could be commonly
found in Janestreet's Core (Type_equal.Uid.t) and other implementations of
existentials). In our case, we ended up with the following implementation
```
type 'a witness = ..
module type Witness = sig
type t
type _ witness += Id : t witness
end
type 'a typeid = (module Witness with type t = 'a)
type 'a key = {
ord : int;
key : 'a typeid;
name : string; (* for introspection *)
show : 'a > Sexp.t; (* also for introspection *)
}
```
Now, we can use the `ord` field to order types, compare them, store in
maps, hash tables, and even arrays. E.g., this is how our `teq` function
looks like,
```
let same (type a b) x y : (a,b) Type_equal.t =
if x.id = y.id then
let module X = (val x.key : Witness with type t = a) in
let module Y = (val y.key : Witness with type t = b) in
match X.Id with
 Y.Id > Type_equal.T
 _ > failwith "broken type equality"
else failwith "types are not equal"
```
It is often used in the context where we already know that `x.id = y.id`,
e.g., when we already found an entry, so we just need to obtain the
equality witness (we use Janestreet's Type_equal.T, which is the same as
yours eq type).
Concerning the representation of the registry, we also experimented with
different approaches (since we have a few ways to make a type existential
in OCaml), and found out the following to be the most efficient and easy to
work with,
```
type ordered = {
order : 'a. 'a key > 'a > 'a > int;
} [@@unboxed]
```
Notice, that thanks to `[@@unboxed]` we got a free unpacked existential. We
will next store `ordered` in our registry, which is a hash table,
```
let ordered : ordered Hashtbl.M(Int).t = Hashtbl.create (module Int)
```
and register it as simple as,
```
let register: type p. p Key.t > (p > p > int) > unit = fun key order
>
Hashtbl.add_exn vtables ~key:(uid key) ~data:{
order = fun (type a) (k : a key) (x : a) (y : a) >
let T = same k key in (* obtain the witness that we found the right
structure *)
order x y
}
```
Instead of a hashtable, it is also possible to use `ordered array ref`
(since our `ord` is just an integer which we increment every time a new
class is declared). This will give us even faster lookup.
I hope that this was interesting. And if yes, I'm ready to elaborate more
on our design decision or to hear suggestions and critics. Here are a few
links:
 https://github.com/BinaryAnalysisPlatform/bap  the BAP project per se.
 https://binaryanalysisplatform.github.io/knowledgeintro1  a small
introductionary post about BAP 2.0 Knowledge representation

https://github.com/BinaryAnalysisPlatform/bap/blob/master/lib/knowledge/bap_knowledge.ml
 the implementation of the knowledge system

https://github.com/BinaryAnalysisPlatform/bap/tree/master/lib/bap_core_theory
 The Core Theory, an exemplar type class of the theory that we're
developing :)
Cheers,
Ivan Gotovchits
Research Scientist, CMU Cylab
[1]: http://okmij.org/ftp/taglessfinal/index.html
On Wed, Sep 4, 2019 at 11:23 AM Oleg <oleg@okmij.org> wrote:
>
> This is to announce a simple, plain OCaml library to experiment with
> typeclass/implicits resolution, which can be thought of as evaluating
> a Prologlike program. One may allow `overlapping instances'  or
> prohibit them, insisting on uniqueness. One may make the search fully
> deterministic, fully nondeterministic, or something inbetween.
> There is an immediate, albeit modest practical benefit: the facility
> like "#install_printer", which was restricted to toplevel, is now
> available for all  as a small, selfcontained, plain OCaml library
> with no knowledge or dependencies on the compiler internals. We show
> an example at the end of the message.
>
> This message has been inspired by the remarkable paper
> Canonical Structures for the working Coq user
> Assia Mahboubi, Enrico Tassi
> DOI: 10.1007/9783642396342_5
> Its introduction is particularly insightful: the power of
> (mathematical) notation is in eliding distracting details. Yet to
> formally check a proof, or to run a program, the omitted has to be
> found. When pressed to fill in details, people `skillful in the art'
> look in the database of the `state of the art', with the context as
> the key. Computers can be programmed similarly; types well represent
> the needed context to guide the search.
>
> Mahboubi and Tassi's paper explains very well how this eliding and
> fillingin is realized, as a programmable unification, and used in
> Coq. Yet their insights go beyond Coq and deserve to be known better.
> This message and the accompanying code is to explain them in
> plain OCaml and encourage experimentation. It could have been titled
> `Canonical Structures for the working OCaml (meta) programmer'.
>
> The rudiment of canonical structures is already present in OCaml, in
> the form of the registry of printers for userdefined types. This
> facility is available only at the toplevel, however, and deeply
> intertwined with it. As a modest practical benefit, this facility is
> now available for all programs, as a plain, small, selfcontained
> library, with no compiler or other magic. The full potential of the
> method is realized however in (multi) staged programming. In fact, I'm
> planning to use it in the upcoming version of MetaOCaml to implement
> `lifting' from a value to the code that produces it  letting the
> users register lifting functions for their own data types.
>
>
> http://okmij.org/ftp/ML/canonical.ml
> The implementation and the examples, some of which are noted below.
> http://okmij.org/ftp/ML/trep.ml
> A generic library of type representations: something like
> Typeable in Haskell. Some day it may be builtin into the compiler
> http://okmij.org/ftp/ML/canonical_leadup.ml
> A wellcommented code that records the progressive development of
> canonical.ml. It is not part of the library, but may serve as
> its explanation.
>
> Here are a few examples, starting with the most trivial one
> module Show = MakeResolve(struct type 'a dict = 'a > string end)
> let () = Show.register Int string_of_int (* Define `instances' *)
> let () = Show.register Bool string_of_bool
> Show.find Int 1;;
>
> However contrived and flawed, it is instructive. Here (Int : int trep)
> is the value representing the type int. The type checker can certainly
> figure out that 1 is of the type int, and could potentially save us
> trouble writing Int explicitly. What the type checker cannot do by
> itself is to find out which function to use to convert an int to a
> string. After all, there are many of them. Show.register lets us
> register the *canonical* int>string function. Show.find is to search
> the database of such canonical functions: in effect, finding *the*
> evidence that the type int>string is populated. Keeping CurryHoward
> in mind, Show.find does a _proof search_.
>
> The type of Show.find is 'a trep > ('a > string). Compare with
> Haskell's show : Show a => a > String (or, desuraging => and Show)
> show : ('a > string) > ('a > string). Haskell's show indeed does
> not actually do anything: it is the identity function. All the hard
> work  finding out the right dictionary (the string producing
> function)  is done by the compiler. If one does not like the way the
> compiler goes about it  tough luck. There is little one may do save
> complaining on reddit. In contrast, the first argument of Show.find is
> trivial: it is a mere reflection of the type int, with no further
> information. Hence Show.find has to do a nontrivial work. In the
> case of int, this work is the straightforward database search 
> or, if you prefer, running the query ? dict(int,R) against a logic
> program
> dict(int,string_of_int).
> dict(bool,string_of_bool).
> The program becomes more interesting when it comes to pairs:
> dict(T,R) : T = pair(X,Y), !,
> dict(X,DX), dict(Y,DY), R=make_pair_dict(DX,DY).
> Here is how it is expressed in OCaml:
> let () =
> let open Show in
> let pat : type b. b trep > b rule_body option = function
>  Pair (x,y) >
> Some (Arg (x, fun dx > Arg (y, fun dy >
> Fact (fun (x,y) > "(" ^ dx x ^ "," ^ dy y ^ ")"))))
>  _ > None
> in register_rule {pat}
>
> let () = Show.register (Pair(Bool,Bool))
> (fun (x,y) > string_of_bool x ^ string_of_bool y)
>
> Our library permits `overlapping instances'. We hence registered the
> printer for generic pairs, and a particular printer just for pairs of
> booleans.
>
> The library is extensible with userdefined data types, for example:
> type my_fancy_datatype = Fancy of int * string * (int > string)
>
> After registering the type with trep library, and the printer
> type _ trep += MyFancy : my_fancy_datatype trep
> let () = Show.register MyFancy (function Fancy(x,y,_) >
> string_of_int x ^ "/" ^ y ^ "/" ^ "<fun>")
>
> one can print rather complex data with fancy, with no further ado:
> Show.find (List(List(Pair(MyFancy,Int)))) [[(Fancy ...,5)];[]]
>
> As Mahboubi and Tassi would say, proof synthesis at work!
>
> We should stress that what we have described is not a typeclass
> facility for OCaml. It is *meta* typeclass facility. Show.find has
> many drawbacks: we have to explicitly pass the trep argument like
> Int. The resolution happens at run time, and hence the failure of the
> resolution is a runtime exception. But the canonical instance
> resolution was intended to be a part of a type checker. There, the
> resolution failure is a type checking error. The trep argument,
> representing the type in the object program, is also at
> hand. Likewise, the drawbacks of Show.find disappear when we use the
> library in a metaprogram (code generator). The library then becomes a
> typeclass/implicits facility, for the generated code  the facility,
> we can easily (re)program.
>
[ Attachment #2: Type: text/html, Size: 14810 bytes ]
<div dir="ltr"><div>Very interesting and thoughtprovoking writeup, thank you! <br></div><div><br></div><div>Incidentally, we're investigating the same venues, in our CMU BAP project, as we found out that we need the extensibility in the style of type classes/canonical structures to decouple complex dependencies which arise in the program analysis domain. </div><div>In fact, we build our new BAP 2.0 framework largely on your [taglessfinal][1] style which, let's admit it, works much better with type classes. Therefore we ended up implementing extensible type representations along with registries for our type classes. Unfortunately, the idea of storing rules in the registry didn't visit us, but we're now thinking about how to incorporate it (the classes that we have are very nontrivial, usually having hundreds of methods, so we're currently using functors to manually derive on class from another, and registering the resulting structures  but using your approach we can register functors as well and automate the derivation). We also didn't generalize the type class instantiation, so our solutions do have some boilerplate (but I have to admit, that the total number of type classes that we need is not very big, so it really never bothered us). What could be surprising is that the universe of types actually grew quite large, that large that the linear search in the registry is not an option for us anymore. In fact, we have so many comparisons between treps, that instead of extracting the extension constructor number from an extensible variant we had to rely on our own freshly generated identifier. But I'm running in front of myself, an important lesson that we have learned is that treps should not only be equality comparable but also ordered (and even hashable) so that we can implement our registries as hash tables. It is also better to keep them abstract so that we can later extend them without breaking user code (to implement introspection as well as different resolution schemes). This is basically an elaboration of your approach (which is also could be commonly found in Janestreet's Core (Type_equal.Uid.t) and other implementations of existentials). In our case, we ended up with the following implementation</div><div>```</div><div> type 'a witness = ..<br></div><div><br></div><div> module type Witness = sig<br></div><div> type t<br> type _ witness += Id : t witness<br> end<br><br> type 'a typeid = (module Witness with type t = 'a)<br><br> type 'a key = {<br> ord : int;<br> key : 'a typeid; </div><div> name : string; (* for introspection *)<br> show : 'a > Sexp.t; (* also for introspection *)<br> }<br></div><div>```</div><div>Now, we can use the `ord` field to order types, compare them, store in maps, hash tables, and even arrays. E.g., this is how our `teq` function looks like,<br></div><div>```</div><div> let same (type a b) x y : (a,b) Type_equal.t =<br> if <a href="http://x.id">x.id</a> = <a href="http://y.id">y.id</a> then<br> let module X = (val x.key : Witness with type t = a) in<br> let module Y = (val y.key : Witness with type t = b) in<br> match X.Id with<br>  Y.Id > Type_equal.T<br>  _ > failwith "broken type equality"<br> else failwith "types are not equal"<br></div><div>```</div><div><br></div><div>It is often used in the context where we already know that `<a href="http://x.id">x.id</a> = <a href="http://y.id">y.id</a>`, e.g., when we already found an entry, so we just need to obtain the equality witness (we use Janestreet's Type_equal.T, which is the same as yours eq type). </div><div><br></div><div>Concerning the representation of the registry, we also experimented with different approaches (since we have a few ways to make a type existential in OCaml), and found out the following to be the most efficient and easy to work with,</div><div><br></div><div>```</div><div>type ordered = {</div> order : 'a. 'a key > 'a > 'a > int;<br> } [@@unboxed]<br><div>```<br></div><div><br></div><div>Notice, that thanks to `[@@unboxed]` we got a free unpacked existential. We will next store `ordered` in our registry, which is a hash table,</div><div><br></div><div>```</div><div>let ordered : ordered Hashtbl.M(Int).t = Hashtbl.create (module Int)</div><div>```</div><div>and register it as simple as,</div><div>```</div><div> let register: type p. p Key.t > (p > p > int) > unit = fun key order ></div><div> Hashtbl.add_exn vtables ~key:(uid key) ~data:{<br> order = fun (type a) (k : a key) (x : a) (y : a) ><br> let T = same k key in (* obtain the witness that we found the right structure *)<br> order x y<br> }<br></div><div>```<br></div><div><br></div><div>Instead of a hashtable, it is also possible to use `ordered array ref` (since our `ord` is just an integer which we increment every time a new class is declared). This will give us even faster lookup. </div><div><br></div><div>I hope that this was interesting. And if yes, I'm ready to elaborate more on our design decision or to hear suggestions and critics. Here are a few links:</div><div><br></div><div> <a href="https://github.com/BinaryAnalysisPlatform/bap">https://github.com/BinaryAnalysisPlatform/bap</a>  the BAP project per se. </div><div> <a href="https://binaryanalysisplatform.github.io/knowledgeintro1">https://binaryanalysisplatform.github.io/knowledgeintro1</a>  a small introductionary post about BAP 2.0 Knowledge representation<br></div><div> <a href="https://github.com/BinaryAnalysisPlatform/bap/blob/master/lib/knowledge/bap_knowledge.ml">https://github.com/BinaryAnalysisPlatform/bap/blob/master/lib/knowledge/bap_knowledge.ml</a>  the implementation of the knowledge system</div><div> <a href="https://github.com/BinaryAnalysisPlatform/bap/tree/master/lib/bap_core_theory">https://github.com/BinaryAnalysisPlatform/bap/tree/master/lib/bap_core_theory</a>  The Core Theory, an exemplar type class of the theory that we're developing :)</div><div><br></div><div>Cheers,</div><div>Ivan Gotovchits</div><div>Research Scientist, CMU Cylab</div><div><br></div><div>[1]: <a href="http://okmij.org/ftp/taglessfinal/index.html">http://okmij.org/ftp/taglessfinal/index.html</a></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Sep 4, 2019 at 11:23 AM Oleg <<a href="mailto:oleg@okmij.org">oleg@okmij.org</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;borderleft:1px solid rgb(204,204,204);paddingleft:1ex"><br>
This is to announce a simple, plain OCaml library to experiment with<br>
typeclass/implicits resolution, which can be thought of as evaluating<br>
a Prologlike program. One may allow `overlapping instances'  or<br>
prohibit them, insisting on uniqueness. One may make the search fully<br>
deterministic, fully nondeterministic, or something inbetween.<br>
There is an immediate, albeit modest practical benefit: the facility<br>
like "#install_printer", which was restricted to toplevel, is now<br>
available for all  as a small, selfcontained, plain OCaml library<br>
with no knowledge or dependencies on the compiler internals. We show<br>
an example at the end of the message.<br>
<br>
This message has been inspired by the remarkable paper<br>
Canonical Structures for the working Coq user<br>
Assia Mahboubi, Enrico Tassi<br>
DOI: 10.1007/9783642396342_5<br>
Its introduction is particularly insightful: the power of<br>
(mathematical) notation is in eliding distracting details. Yet to<br>
formally check a proof, or to run a program, the omitted has to be<br>
found. When pressed to fill in details, people `skillful in the art'<br>
look in the database of the `state of the art', with the context as<br>
the key. Computers can be programmed similarly; types well represent<br>
the needed context to guide the search.<br>
<br>
Mahboubi and Tassi's paper explains very well how this eliding and<br>
fillingin is realized, as a programmable unification, and used in<br>
Coq. Yet their insights go beyond Coq and deserve to be known better.<br>
This message and the accompanying code is to explain them in<br>
plain OCaml and encourage experimentation. It could have been titled<br>
`Canonical Structures for the working OCaml (meta) programmer'.<br>
<br>
The rudiment of canonical structures is already present in OCaml, in<br>
the form of the registry of printers for userdefined types. This<br>
facility is available only at the toplevel, however, and deeply<br>
intertwined with it. As a modest practical benefit, this facility is<br>
now available for all programs, as a plain, small, selfcontained<br>
library, with no compiler or other magic. The full potential of the<br>
method is realized however in (multi) staged programming. In fact, I'm<br>
planning to use it in the upcoming version of MetaOCaml to implement<br>
`lifting' from a value to the code that produces it  letting the<br>
users register lifting functions for their own data types.<br>
<br>
<br>
<a href="http://okmij.org/ftp/ML/canonical.ml" rel="noreferrer" target="_blank">http://okmij.org/ftp/ML/canonical.ml</a><br>
The implementation and the examples, some of which are noted below.<br>
<a href="http://okmij.org/ftp/ML/trep.ml" rel="noreferrer" target="_blank">http://okmij.org/ftp/ML/trep.ml</a><br>
A generic library of type representations: something like <br>
Typeable in Haskell. Some day it may be builtin into the compiler<br>
<a href="http://okmij.org/ftp/ML/canonical_leadup.ml" rel="noreferrer" target="_blank">http://okmij.org/ftp/ML/canonical_leadup.ml</a><br>
A wellcommented code that records the progressive development of<br>
<a href="http://canonical.ml" rel="noreferrer" target="_blank">canonical.ml</a>. It is not part of the library, but may serve as<br>
its explanation.<br>
<br>
Here are a few examples, starting with the most trivial one<br>
module Show = MakeResolve(struct type 'a dict = 'a > string end)<br>
let () = Show.register Int string_of_int (* Define `instances' *)<br>
let () = Show.register Bool string_of_bool<br>
Show.find Int 1;;<br>
<br>
However contrived and flawed, it is instructive. Here (Int : int trep)<br>
is the value representing the type int. The type checker can certainly<br>
figure out that 1 is of the type int, and could potentially save us<br>
trouble writing Int explicitly. What the type checker cannot do by<br>
itself is to find out which function to use to convert an int to a<br>
string. After all, there are many of them. Show.register lets us<br>
register the *canonical* int>string function. Show.find is to search<br>
the database of such canonical functions: in effect, finding *the*<br>
evidence that the type int>string is populated. Keeping CurryHoward<br>
in mind, Show.find does a _proof search_.<br>
<br>
The type of Show.find is 'a trep > ('a > string). Compare with<br>
Haskell's show : Show a => a > String (or, desuraging => and Show)<br>
show : ('a > string) > ('a > string). Haskell's show indeed does<br>
not actually do anything: it is the identity function. All the hard<br>
work  finding out the right dictionary (the string producing<br>
function)  is done by the compiler. If one does not like the way the<br>
compiler goes about it  tough luck. There is little one may do save<br>
complaining on reddit. In contrast, the first argument of Show.find is<br>
trivial: it is a mere reflection of the type int, with no further<br>
information. Hence Show.find has to do a nontrivial work. In the<br>
case of int, this work is the straightforward database search <br>
or, if you prefer, running the query ? dict(int,R) against a logic<br>
program<br>
dict(int,string_of_int).<br>
dict(bool,string_of_bool).<br>
The program becomes more interesting when it comes to pairs:<br>
dict(T,R) : T = pair(X,Y), !, <br>
dict(X,DX), dict(Y,DY), R=make_pair_dict(DX,DY).<br>
Here is how it is expressed in OCaml:<br>
let () = <br>
let open Show in<br>
let pat : type b. b trep > b rule_body option = function<br>
 Pair (x,y) > <br>
Some (Arg (x, fun dx > Arg (y, fun dy > <br>
Fact (fun (x,y) > "(" ^ dx x ^ "," ^ dy y ^ ")"))))<br>
 _ > None<br>
in register_rule {pat}<br>
<br>
let () = Show.register (Pair(Bool,Bool)) <br>
(fun (x,y) > string_of_bool x ^ string_of_bool y)<br>
<br>
Our library permits `overlapping instances'. We hence registered the<br>
printer for generic pairs, and a particular printer just for pairs of<br>
booleans.<br>
<br>
The library is extensible with userdefined data types, for example:<br>
type my_fancy_datatype = Fancy of int * string * (int > string)<br>
<br>
After registering the type with trep library, and the printer<br>
type _ trep += MyFancy : my_fancy_datatype trep<br>
let () = Show.register MyFancy (function Fancy(x,y,_) ><br>
string_of_int x ^ "/" ^ y ^ "/" ^ "<fun>")<br>
<br>
one can print rather complex data with fancy, with no further ado:<br>
Show.find (List(List(Pair(MyFancy,Int)))) [[(Fancy ...,5)];[]]<br>
<br>
As Mahboubi and Tassi would say, proof synthesis at work!<br>
<br>
We should stress that what we have described is not a typeclass<br>
facility for OCaml. It is *meta* typeclass facility. Show.find has<br>
many drawbacks: we have to explicitly pass the trep argument like<br>
Int. The resolution happens at run time, and hence the failure of the<br>
resolution is a runtime exception. But the canonical instance<br>
resolution was intended to be a part of a type checker. There, the<br>
resolution failure is a type checking error. The trep argument,<br>
representing the type in the object program, is also at<br>
hand. Likewise, the drawbacks of Show.find disappear when we use the<br>
library in a metaprogram (code generator). The library then becomes a<br>
typeclass/implicits facility, for the generated code  the facility,<br>
we can easily (re)program.<br>
</blockquote></div>
next prev parent reply index
Thread overview: 4+ messages / expand[flatnested] mbox.gz Atom feed top
20190904 15:25 Oleg
20190904 20:41 ` Ivan Gotovchits [this message]
20190910 14:40 ` [Camllist] Typeindexed heterogeneous collections (Was: Implicits for the masses) Oleg
20190910 19:03 ` Ivan Gotovchits
Reply instructions:
You may reply publically to this message via plaintext email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and replytoall from there: mbox
Avoid topposting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the to, cc, and inreplyto
switches of gitsendemail(1):
git sendemail \
inreplyto=CALdWJ+wpwafYOddNYhTFY5Zz02k4GcWLBmZLGkekuJSMjrdd6Q@mail.gmail.com \
to=ivg@ieee.org \
cc=camllist@inria.fr \
cc=oleg@okmij.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/gitsendemail.html
* If your mail client supports setting the InReplyTo header
via mailto: links, try the mailto: link
camllist  the Caml user's mailing list
Archives are clonable:
git clone mirror http://inbox.vuxu.org/camllist
git clone mirror https://inbox.ocaml.org/camllist
Example config snippet for mirrors
Newsgroup available over NNTP:
nntp://inbox.vuxu.org/vuxu.archive.camllist
AGPL code for this site: git clone https://publicinbox.org/publicinbox.git