ppx_deriving_yojson icon indicating copy to clipboard operation
ppx_deriving_yojson copied to clipboard

achieving @@deriving for extensible variants that have already been extended

Open chetmurthy opened this issue 4 years ago • 4 comments

I'm trying to figure out the right way to express @@deriving directives for extensible variants that have already been extended, and thought that I'd see if you all had some thoughts.

  1. Background when we want to express @@deriving for a type that's already been declared, we can do it thus:
type t = <type-name> = <type-expression> [@@deriving ...]

but there is no equivalent for extensible variants. If we have an extensible variant type "exn" that has already been extended, viz.

type exn += Failure of string

then how do we express that we want to @@derive functions for it? The form

type exn += Failure = Stdlib.Failure

doesn't help, b/c it doesn't carry the type-arguments of the constructor.

(2) I propose we can do this in two stages:

type exn += Failure of string [@rebind Stdlib.Failure] [@@deriving ....]

The first stage is the @@deriving rewriter that generats functions based on this type, as if "Failure" is freshly extending "exn". The second stage rewrites (directed by the @rebind) the rebind-extension-constructor to

type exn += Failure = Stdlib.Failure

This seems straightforward, and I don't see any problems, but I figured I'd ask if you all had thought about this, and whether it was problematic in some way I haven't understood.

chetmurthy avatar May 08 '20 18:05 chetmurthy

This seems very ad-hoc to me. I would rather:

  • consider using ppx_import, which is ad-hoc but its own pre-existing kind of ad-hoc
  • change the language to allow of ... in extensible variant rebinding, so that what you suggest can be written as valid OCaml.

One issue with the second approach is that the most natural syntax creates an ambiguity between "a rebind that leaves the parameters out" and "a rebind of constructors without parameters", which is not so nice. (But if people really want to insist on their intent to rebind a parameter-less constructor, maybe they can use the GADT syntax?)

gasche avatar May 09 '20 19:05 gasche

I don't believe you can use ppx_import to achieve the goal of allowing full @@deriving support for extensible polymorphic variants. I might be wrong, though. Here is my reasoning: (1) if I extend a type t with a constructor E, then the next time I extend it, it is a -different- constructor E. Since constructors are generative, right? (2) So either (a) I must not submit the extension declaration to the compiler, or (b) I must convert it into a rebind. In the second step above, I go with #b, but I'm happy to go with #a instead.

Changing the Ocaml language: well, I can't suggest that, b/c after all, we're talking about a macro-preprocessor, and it would be a great ask for the macro-preprocessor to demand changes to the language for its own convenience, right? Also, this is a corner-case, after all. And I agree with you that there's the problem that if we want to support both "rebind that omits constructor arguments" and "rebind that includes constructor arguments" then there's the ambiguity of an argument-less constructor.

This is why I suggested a macro-processor-only solution: the need is only in the macro-processor, and hence the solution could be only in the macro-processor.

Your thoughts?

chetmurthy avatar May 09 '20 20:05 chetmurthy

Also, the reason I started thinking about this, is support for the extensible type exn. ppx_sexp_conv has special support for it. But (AFAICT) none of the other derivers do. It would be nice if there were a way to support exn cleanly, and in a way that supported it as just another extensible variant, I think.

And if it is that way is defined properly, then every deriver could support it the same way.

P.S. the actual code for this is pretty trivial: I can code it in a few hours in my pa_ppx ( https://github.com/chetmurthy/pa_ppx/tree/master ) project (and support it for all derivers), but I figure, before doing so, it would be good to get some sort of agreement as to what the right syntax should look like.

chetmurthy avatar May 09 '20 20:05 chetmurthy

Here's a (working) example of what I mean:


module Exn = struct
type t = exn = .. [@@deriving show, sexp]
end

type Exn.t +=
    Not_found [@rebind_to Stdlib.Not_found ;]
  | Failure of string [@rebind_to Stdlib.Failure ;]
[@@deriving show { with_path = false }, sexp]

This yields show and sexp derivers that do the expected thing, viz.


let test_exceptions ctxt =
  assert_equal Stdlib.Not_found Not_found
; assert_equal ~printer:(fun x -> x) "Not_found" (Exn.show Stdlib.Not_found)
; assert_equal (Stdlib.Failure "foo") (Failure "foo")
; assert_equal ~printer:(fun x -> x) {|(Failure "foo")|} (Exn.show (Stdlib.Failure "foo"))
; assert_roundtrip_sexp Exn.show Exn.sexp_of_t Exn.t_of_sexp
    Not_found "(Not_found)"
; assert_roundtrip_sexp Exn.show Exn.sexp_of_t Exn.t_of_sexp
    (Failure"foo") {|(Failure "foo")|}

chetmurthy avatar May 10 '20 20:05 chetmurthy