Unique types#9042
Conversation
|
We discussed in the past (see #5985 (comment) for example ) a more general feature, new type <params> <tyname>
new type <params> <tyname> = <tyexpr>In the second case, the type is defined to be convertible from and to the given |
|
Thank you for finding the reference. |
|
If only types of the form new type t1
new type t2must be implemented with new type t1 (= ty1)
new type t2 (= ty2)and therefore |
|
You're meaning that they cannot be copied from one module to another? |
How is this different from: type zero = private Zero |
|
For one thing, |
|
But unique types do seem rather like "extra-private" types, with no constructors, no destructors, and no visible representation. |
|
I'd missed this part:
So now I think I see what @garrigue is aiming for. I've always thought that the problem with and type_kind =
Type_abstract
| Type_record of label_declaration list * record_representation
| Type_variant of constructor_declaration list
| Type_opento and type_kind =
Type_abstract
| Type_record of label_declaration list * record_representation
| Type_variant of constructor_declaration list
| Type_open
| Type_float
| Type_stringHowever, the same basic issue arises for other forms of datatype. For example, type float = external "float"This is basically what @garrigue is proposing, but with what I think is a more intuitive syntax. |
|
Reading back the discussion on new types, I've beefed up this proposal. type 'a stack = {mutable c : 'a list} [@@unique]Then it can be turned into a unique abstract type in the interface. type 'a stack [@@unique "stack"]To still guarantee than unique abstract types are incompatible with (non unique) datatypes, I require that the definition is forgotten together with the identity. I.e., the following is illegal: module Stack : sig type 'a stack = {mutable c : 'a list} end =
struct type 'a stack = {mutable c : 'a list} [@@unique] endAdditionally, uniqueness implies injectivity. Since all unique types are nominal from the start, they are by definition injective. This allows to define types that were not allowed before: type _ t = T : 'a -> 'a Queue.t tsince See https://github.com/garrigue/ocaml/tree/unique-types/testsuite/tests/typing-unique for more examples. |
|
Using module F(X: sig type 'a t [@@unique] end) = ... |
|
What is the use case for Can we try and describe what |
I hope not. If we want to have abstraction over injective types then we should have specific syntax to mean "injective type" -- just like we do for say "covariant type". |
|
I think the semantics are: "unique" means "generative". The name (string) refers to the generated identifier, which there's currently no way of mentioning in programs. So a generative type definition now has both an optional identifier and an optional representation. That also means |
|
It's not just a "name" field alongside the representation and equation though, because that wouldn't imply injectivity. So far I don't see why we would prefer this to adding an additional |
|
Sorry, I started with something that was probably just your I'll try to write a full explanation shortly, but to give you the gist of it, while I added it as an extra field in |
Answering my own implicit question, it is a name field alongside the representation and equation together with a requirement that any equations respect the name field (just as they currently respect the representation field). That is why you can't have non-injective types with a name. |
|
Let me try to explain the current status, since it changed a bit since the original external type proposal. ConceptThe basic idea is to add a new case to IntroductionThis name must be introduced since the first definition of the type, either pure abstract (i.e. an abstract type defined in an implementation), or a datatype. type 'a t [@@unique "foo"]
type 'u ab = A | B [@@unique "bar"]Those types are indeed generative, but only in the sense that they are nominal. module F(X : sig type t end)() = struct type t = T of X.t [@@unique "F.t"] endAn aside of their being generative, is that they are injective in all their type parameters: there is no way to expand them. One can define abbreviations of named types, public or private, that will still be recognized as named. type 'a t = 'a ab = A | B [@@unique "bar"]The reason for this repetition is that allowing writing the above without requiring uniqueness could result in forgetting it, if SubtypingUniqueness can be kept through subtyping. Aside of reflexivity, the following are allowed type ('a1,...,'an) t = ('b1,...,'bm) u <: type ('a1,...,'an) t [@@unique s]
if u is [@@unique s] and ('a1,...,'an) \subset ('b1,...,'bm)
type 'a t = A | B [@@unique s] <: type 'a t [@@unique s]
type 'a t = 'a u = A | B [@@unique s] <: type 'a t [@@unique s]
type 'a t = 'a u = A | B [@@unique s] <: type 'a t = A | B [@@unique s]The inclusion condition is needed to preserve injectivity (it could be relaxed more). type 'a t = A | B [@@unique s] <\: type 'a t = A | Bis not allowed, because it would break the invariant that named abstract types and unnamed datatypes are always incompatible. The above subtyping rules seem sufficient to support usual programming patterns. For instance: module M : sig type b [@@unique "M.b"] end = struct
module M1 = struct type a = A [@@unique "M.b"] end
type b = M1.a
endNote that since exporting through aliases is allowed, it would indeed be possible to pass a unique type to a functor. However, since the functor would also need to give an expected name, this does not seem that useful. It would be more usable if we had a notion "unique type without name" as a supertype of "unique type", but I'm not sure there is much demand for that. Conclusion and syntaxModulo the need for explicit names, this seems to solve at once the problem of the uniqueness and injectivity of abstract types, both for external ones and for those implemented by datatypes. Those implemented by type abbreviations are not directly supported, but one can still wrap them. We could also have injectivity annotations on individual type variables, but the improvement would be minor. The syntax is yet to define. @Drup suggested to use longidents rather than strings for names, maybe with a mechanism to retrieve them automatically from the context. A real syntax would be nice. Maybe something like type 'a t = new
type 'a t = new M.t
type 'a t = new M.t = A | B
type 'a t = new M.t = 'a ab = A | Bbut I'm not really convinced (the last case is particularly ugly, and is only needed for a technical reason) |
|
Side remark: if you look at the code, I have added uniqueness annotations in the standard library. This is good for examples, but I'm not sure all of them are useful, in particular those in |
|
An interesting remark, which I found in a discussion with @Ekdohibs, is that by restricting a bit the current proposal to allow only exporting named types with the same arity, it would be possible to make identically named types whose names are identical incompatible when their parameters are incompatible. type 'a t [@@unique "M.t"]
type 'a u [@@unique "M.t"]would allow to deduce that
It would still be possible to export a completely instantiated type with no parameters: module M : sig type x [@@unique "M.t"] = struct type x = int t endsince module M : sig type 'a t [@@unique "M.t"] type 'a u [@@unique "M.t"] end = struct
type ('a,'b) s [@@unique "M.t"]
type 'a t = ('a,bool) s
type 'a u = (int, 'a) s
endsince A consequence is that, for module type S = sig
type key
type 'a t1 constraint 'a = key
type t = key t1
...
endAgain, I'm not really suggesting to modify the standard library in this direction, this just demonstrates a useful expressive power. |
|
This tension between uniqueness and injectivity suggests that, as @lpw25 said, the two concepts would be more precisely handled as separate (eventhough incompatibility requires both). |
|
Thanks for the new summary. I have some thoughts on the details of the design and some thoughts about the general approach. DetailsFrom your description of the desired behaviour and subtyping relation, I think that it is best to think of this proposal as a change to the type's representation rather than its manifest. It is essentially equivalent to replacing: and type_kind =
| Type_abstract
| Type_record of label_declaration list * record_representation
| Type_variant of constructor_declaration list
| Type_openwith and type_kind =
| Type_abstract
| Type_concrete of { name : string; repr : type_representation option }
and type_representation =
| Type_record of label_declaration list * record_representation
| Type_variant of constructor_declaration list
| Type_openwith the expected subtyping relation of That should be sufficient to get the behaviour you are after and it avoids changing anything around manifests. I think messing with manifests is more dubious proposition -- see for example the issues where private types cause equations to be lost. I don't know what the best syntax for your proposal is, but I think that "unique" is quite a loaded term. I would prefer something like I strongly oppose using longidents rather than strings: these labels have no structure to them and certainly no notion of binding, we should not pretend that they do. Overall approachAll in all, I'm still not convinced by this proposal as compared to your original In particular, I'm think your proposal creates a new question that must be answered for each new type the user creates: "What should its name be?". I don't see a simple systematic way for users to answer that question and that worries me. In my experience, GADTs are either proper GADTs -- in which case the indices are ordinary types and their compatibility doesn't really matter -- or they are poor-mans indexed types -- in which case some fake types are created to encode some form of data and the |
|
@lpw25 Thanks for looking at the proposal. Concerning the representation, I really think this is a question of point of view. For the syntax, I have no strong opinion, but indeed, while playing with this branch, it became clear that More importantly, about the need of being able to name defined abstract types.
module M : sig type +'a t [@@unique "M.t"] end =
struct type 'a t = Nil | Cons of 'a * 'a t [@@unique "M.t"] end
type _ ty = M : 'a ty -> 'a M.t ty;; (* error without [@@unique] *)There is a workaround, but it is particularly clumsy: module M : sig type +'a t0 and 'a t = M_t of 'a t0 end =
struct type 'a t0 = Nil | Cons of 'a * 'a t0 and 'a t = M_t of 'a t0 end
type _ ty = M : 'a ty -> 'a M.t ty;;
module M : sig type +'a t [@@unique "M.t"] val create : 'a list -> 'a t end = struct
type 'a t = Nil | Cons of 'a * 'a t [@@unique "M.t"]
let rec create = function [] -> Nil | a::l -> Cons (a, create l)
end
type _ exp = M : 'a list -> 'a M.t exp | Int : int -> int exp
let eval_int : int exp -> int = function Int x -> x;;So yes, I think this is useful, exactly in the same way as it is useful for external data. Last, and orthogonal, on whether to use quoted strings or paths. |
|
Implemented the nominal types RFC.
|
I'm not sure that the current proposal completely solves the injectivity question. One situation in which injectivity often crops up is where one type is defined as a specialization of another injective type. For example, the type constructor for the environment/reader monad is defined as a particular type of function type: type 'a t = s -> 'aAlthough module type monad = sig type +'a t [@@nominal] ... end |
|
I think the recent paper "Higher-order Type-level Programming in Haskell" (Csongor Kiss et al, ICFP '19) might be relevant here. What's called "unique" / "nominal" here is called "generative" there, and they say a type constructor is "matchable" if both generative and injective. It's Haskell, so they're starting from the opposite position (in Haskell 98, all type constructors are matchable), but the problem looks similar. |
|
@yallop Indeed, I do not attempt to support type abbreviations (when they are not simple re-exports). While it is technically possible to infer injectivity independently of nominality, I'm afraid this would have only a limited impact: you can always rewrap your abbreviation inside a nominal type definition. @stedolan Thanks for the pointer. This looks relevant enough. Generative could be a better name, but I was a bit afraid of the confusion with generative functors. |
Agreed! I mentioned "generative" only to explain the relationship to that paper, I think we should not use the same term here. |
|
Rebased on trunk (3.12) on 2020-08-07. |
This is a proof of concept implementation of unique types, which allows to use GADTs with any abstract types, as long as they are marked as unique with different identifiers.
As a result predefined types such as
floatorcharare not treated specially, just declared unique.As a result, any unique type is incompatible with any non-abstract type, and two abstract types with different identifiers are incompatible.
This subsumes #8900 .
In this experimental implementation the syntax is based on attributes.
An alternative syntax would be to just use strings: