-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nominal (abstract) types #4
base: master
Are you sure you want to change the base?
Conversation
I have no idea about typing part but using annotations to influence type-checker seems wrong? I would expect this to be part of language syntax proper. |
4a1cd9e
to
2f25fae
Compare
I'm not sure what the best syntax would be for this one. An alternative syntax could be |
Copying my response from the original thread, as it still represents my thoughts on this proposal:
Additionally I would like to add that I am strongly against using paths instead of arbitrary strings. Using path syntax for things that are not semantically paths and do not behave as paths (e.g. in substitution) is a recipe for confusion. |
Thanks for putting back the feedback here.
This problem has been left as is for more than 5 years, so I would like to eventually get to a solution. Of course, we still have time to refine things :) |
Concerning the syntax, my feeling is that the attribute syntax is closer to what we need here, as this information strengthens the type checker, but does not change the semantics of types. |
Could we see the examples from Nomadic labs? I think the details of such examples are important to the discussion. It would also help if someone who thinks that these labels are somehow paths instead of strings could give their reasoning. Fundamentally, I think this proposal needs to answer this question: how do users answer the question "What should its name be?" every time they create a new time?
Maybe we should consider more thorough solutions like tracking whether types are freshly created or not. It probably "just" requires using a bunched context for modules to track sharing. |
Sorry, going to Japan and back and all that delayed me a lot. I thought a bit about one of the main applications, which is allowing to define witnesses for parametric abstract types, but I could actually find a workaround in that case. Whether it should be described as practical is another question. Here is the code. The idea is to define a new abstract type module Vec : sig
type +'a t
val make : int -> (int -> 'a) -> 'a t
val get : 'a t -> int -> 'a
end = struct
type 'a t = Obj.t array
let make = Obj.magic Array.init
let get = Obj.magic Array.get
end
type ('a,'b) vec = Vec of 'a * 'b
type (_,_) ty =
| Int : (int,int) ty
| Fun : ('a,'c) ty * ('b,'d) ty -> ('a -> 'b,'c -> 'd) ty
| Vec : ('a,'b) ty -> ('a Vec.t, ('a,'b) vec) ty
type dyn = Dyn : ('a,_) ty * 'a -> dyn
type (_,_) eq = Refl : ('a,'a) eq
let rec eq_ty : type a b c d.
(a,c) ty -> (b,d) ty -> ((a,b) eq * (c,d) eq) option =
fun t1 t2 -> match t1, t2 with
| Int, Int -> Some (Refl, Refl)
| Fun (t11, t12), Fun (t21, t22) ->
begin match eq_ty t11 t21, eq_ty t12 t22 with
| Some (Refl, Refl), Some (Refl, Refl) -> Some (Refl, Refl)
| _ -> None
end
| Vec t1, Vec t2 ->
begin match eq_ty t1 t2 with
| Some (Refl, Refl) -> Some (Refl, Refl)
| None -> None
end
| _ -> None
let undyn : type a b. (a,b) ty -> dyn -> a option =
fun t1 (Dyn (t2, v)) ->
match eq_ty t1 t2 with
| Some (Refl, _) -> Some v
| None -> None
let v = Vec.make 3 (fun n -> Vec.make n (fun m -> (m*n)))
let d = Dyn (Vec (Vec Int), v)
let Some v' = undyn (Vec (Vec Int)) d This seems to work for this example, but I'm curious whether this solution is generic enough in practice. |
Here is another version using Oleg's workaround of reifying the equation. type (_,_) eq = Refl : ('a,'a) eq
type _ ty =
| Int : int ty
| Fun : 'a ty * 'b ty -> ('a -> 'b) ty
| Vec : 'a ty * ('a Vec.t, 'b) eq -> 'b ty
type dyn = Dyn : 'a ty * 'a -> dyn
let rec eq_ty : type a b. a ty -> b ty -> (a,b) eq option =
fun t1 t2 -> match t1, t2 with
| Int, Int -> Some Refl
| Fun (t11, t12), Fun (t21, t22) ->
begin match eq_ty t11 t21, eq_ty t12 t22 with
| Some Refl, Some Refl -> Some Refl
| _ -> None
end
| Vec (t1, Refl), Vec (t2, Refl) ->
begin match eq_ty t1 t2 with
| Some Refl -> Some Refl
| None -> None
end
| _ -> None
let undyn : type a. a ty -> dyn -> a option =
fun t1 (Dyn (t2, v)) ->
match eq_ty t1 t2 with
| Some Refl -> Some v
| None -> None
let v = Vec.make 3 (fun n -> Vec.make n (fun m -> (m*n)))
let int_vec_vec = Vec (Vec (Int,Refl), Refl)
let d = Dyn (int_vec_vec, v)
let Some v' = undyn int_vec_vec d |
PR#9500 proposes an alternative solution to this RFC, through injectivity annotations on type parameters. Its implementation is simpler, and it is finer grain, but this does not allow to distinguish abstract types which cannot have the same implementation. Even for injectivity, they are in some way complementary:
|
I've read through the ml workshop abstract and the relevant parts of the related Haskell work. My current thoughts on the proposal are as follows. Firstly, I think that contractiveness is useful to express for non-generative types, especially polymorphic variants and object types. It is also closer to injectivity than generativity, being a property of a type parameter rather than a whole type constructor. I think it would be worth having e.g.: type ^'a t for a contractive type definition. Matchability in OCaml amounts to being a datatype and it is a property of the whole type constructor -- essentially a different arrow in the kind language as in the Haskell work. Personally, I'd propose making that very explicit and having a syntax like: datatype 'a t to represent a matchable type. Since all datatype are injective and contractive that would imply I remain somewhat unconvinced of the need for expressing the separability of datatypes that have a definition. I can see the need for creating fresh separate types, and I'd support adding syntax like: type 'a t = external "file handle" where I'm not strongly against having the ability to attach a name to a type in order to force it to be separate, but I still haven't seen convincing examples of the need for it, and remain a bit worried about how a library author is to decide whether to give their type a name or not. Perhaps using attribute syntax to add the feature as you propose and leaving it marked "experimental" in the "extensions" section of the manual for a while would allow people to try it out and find some good examples for it. |
Thank you for your feedback. This is indeed the alternative approach I had in mind. |
Yeah, there seems to be quite a large potential design space for ways of controlling and exposing separability between types.
It's true that matchability implies that a type is a datatype which implies that it is contractive. However, not all contractive types are matchable. In particular, polymorphic variants and objects are contractive but not matchable, and I think people using these types are much more likely to run into problems with contractiveness anyway. Certainly I have tried in the past to use functors to construct recursive polymorphic variant types, and found the contractiveness restrictions to be a real pain for that. I think that as long as declaring a type matchable implicitly declares it contractive (and injective) in all its parameters then the contractiveness annotations (and indeed the injectivity annotations) will only be needed very rarely and will not confuse people too much. |
Minor point: instead of having a zoo of weird non-variance signs that we always never see in code and are not self-discoverable (good luck googling for |
I think that is a reasonable argument for using a (contextual?) keyword rather than a symbol, but I dislike using attributes and extension nodes for genuine language features. Half the point of attributes and extension nodes was to make it clear when you were using an extension or extra-linguistic feature. |
I agree with @lpw25 , we should not abuse attributes for actual linguistic features. I think it's ok for backend-related stuff, but the use of attributes even for constructor unboxing is already in the gray-area (removing one annotation can break type checking). We have used several times (cryptic) operator symbols to avoid introducing actual keywords (e.g. for method override, shadowing open). Adding reserved keywords is always tricky, but one could rely on identifiers with some custom syntactic delimiters (e.g. |
A proposal for a uniform handling of abstract types w.r.t. GADTs and injectivity.
Cf. #9042, #5985.