Extending open to accept arbitrary module expression#1506
Extending open to accept arbitrary module expression#1506objmagic wants to merge 46 commits intoocaml:trunkfrom
open to accept arbitrary module expression#1506Conversation
Signed-off-by: Runhang Li <marklrh@gmail.com>
Signed-off-by: Runhang Li <marklrh@gmail.com>
Check if we can eliminate identifier in module sig no matter if we are in nested open.
|
cc @alainfrisch May I have a round of review? |
Octachron
left a comment
There was a problem hiding this comment.
After a quick look, I had a few questions in term of user-facing messages and API, and code readability:
driver/compmisc.ml
Outdated
| let loc = Location.in_file "command line" in | ||
| let lid = {loc; txt = Longident.parse m } in | ||
| let me = Parsetree.({pmod_desc=Pmod_ident lid; pmod_loc=loc; pmod_attributes=[]}) in | ||
| snd (Typemod.type_open_ ?toplevel:None Override env lid.loc me) |
There was a problem hiding this comment.
This pattern of first creating a module expression from a Longident.t then applying type_open_ appears quite frequently. Would it make sense to have a type_open_id function fot this common case?
There was a problem hiding this comment.
Alternatively, one could introduce a helper function such as:
let modexpr_of_lid lid = {Parsetree.pmod_desc=Pmod_ident lid; pmod_loc=lid.loc; pmod_attributes=[]}(or adapt Ast_helper.ident to use the lid location when ~loc is not passed explicitly)
There was a problem hiding this comment.
Or just use Ast_helper.ident as is, since type_open would use the location of the lid, not of the module_expr in that case anyway.
| sig | ||
| val mk: ?loc: loc -> ?attrs:attrs -> ?docs:docs -> | ||
| ?override:override_flag -> lid -> open_description | ||
| ?override:override_flag -> module_expr -> open_description |
There was a problem hiding this comment.
Similarly, I think it would be useful to have specialized function for the lid case. Moreover, since mk is exposed in compiler-libs, I am wondering if it would not be more pratical for users to add a mk_expr for the generic case and let mk as the specialized version.
typing/typemod.ml
Outdated
| let gen_mod_ident () = | ||
| let n = !mod_ident_counter in | ||
| incr mod_ident_counter; | ||
| let ident = Ident.create (Printf.sprintf "M#%d" n) in |
There was a problem hiding this comment.
I find this naming scheme M#%d quite lacking since both the name M and the identifier %d are arbitrary and not really informative for users. Would it be possible to use open#%location#optional_disambiguation_id or a more expressive naming scheme?
There was a problem hiding this comment.
Also, cosmetic: let ident = ... in ident => ....
typing/typemod.ml
Outdated
| let remove = | ||
| List.filter ( | ||
| function | ||
| | Sig_module({Ident.name}, _, _) when String.contains name '#' -> false |
There was a problem hiding this comment.
This feels a bit brittle and not really informative, would it be possible to define a is_open_generated (or a better name) function rather than use String.contains name '#' ?
There was a problem hiding this comment.
I'd also suggest inlining remove where it is used (in the case for Mty_signature), and drop the aux inner function (just making remove_inserted_modtype recursive). (Btw, it's not yet obvious to me why we need to recurse under mty_arg -- I haven't reviewed all the PR yet.)
There was a problem hiding this comment.
One can openin functor argument signature
module M (S: sig open struct ... end ... end) = struct ... end
typing/typemod.ml
Outdated
| if StringSet.mem name val_names then k | ||
| else (component :: sg, StringSet.add name val_names) | ||
| | Sig_module({Ident.name}, _, _) :: sg | ||
| when String.contains name '#' -> aux sg |
There was a problem hiding this comment.
Same remark as above: having a specific function rather String.contains name '#' would be nice.
typing/typemod.ml
Outdated
| | exception Not_found -> | ||
| raise(Error(sod.popen_loc, env, | ||
| Cannot_eliminate_anon_module(id, rem))) | ||
| | _ -> assert false |
There was a problem hiding this comment.
Why is this case impossible? A comment would be nice.
typing/typemod.ml
Outdated
| "This is an alias for module %a, which is missing" | ||
| path p | ||
| | Invalid_open _me -> | ||
| fprintf ppf "Invalid open" |
There was a problem hiding this comment.
This error message is a bit dry. Why is the open invalid? Why is the module expression not used?
There was a problem hiding this comment.
now Invalid_open is no longer used so I removed it.
| fprintf ppf "Invalid open" | ||
| | Cannot_eliminate_anon_module (id, sg) -> | ||
| fprintf ppf "The module identifier %a cannot be \ | ||
| eliminated from %a" ident id signature sg |
There was a problem hiding this comment.
I would add an enclosing box @[…@] and at least a break hint before the signature (from@ %a).
There was a problem hiding this comment.
The message should be improved to mention that the module identifier has been synthesized and correspond to the opened module expression. Even better of course would be to never leak the identifier in the error message and pin-point the actual signature item whose type still refer to that identifier, but it's more difficult.
typing/typemod.ml
Outdated
| | None -> | ||
| let md = Env.find_module path env in | ||
| ignore (extract_sig_open env lid.loc md.md_type); | ||
| assert false |
There was a problem hiding this comment.
I know the code was here originally, but it may be nice to add a comment explaining that extract_sig_open raises an structure expected since the opened module is a functior in this branch.
There was a problem hiding this comment.
I have refactored so that extraction is no longer needed
typing/typemod.ml
Outdated
| push_current_mid (ident, md, newenv); | ||
| let root = Pident ident in | ||
| match Env.open_signature ~loc ?used_slot ?toplevel ovf root newenv with | ||
| | None -> assert false |
There was a problem hiding this comment.
I still think that a comment by assert false sounds nice.
|
This would be awesome for me! I'm implementing a sort of typeclass thing, and it'd be great to not have to call the typeclass implementation functors, except in (right now, I have something like (* in Result *)
module Monad(E: Type) = struct
type error = E.t;
...
end
(* in cafec/parse/error.re *)
module Monad_result = Result.Monad(sig type nonrec t = t end);
(* in cafec/parse/parse.re *)
open Error.Monad_result;I'd rather write (* Result is the same *)
(* in cafec/parse/parse.re *)
open Result.Monad(sig type t = Error.t end)) |
|
There's a bug in this patch. I've installed this and switched to it, and attempted to install jbuilder: |
|
@gasche ah, okay :) thanks! on another thing - the paper doesn't support local opens, apparently, but I'd really love local opens to support this as well, for purposes similar to typeclasses. module type Print = sig
type t
val print: t -> unit
end
module Print_int: Print with type t = int = struct
...
end
module Print_list(P: Print): Print with type t = P.t list = struct
...
end
let print_list_of_int lst = Print_list(Print_int).(print lst) |
|
@ubsan Thanks for your comment! Extension on local |
|
@objmagic thanks <3 |
parsing/ast_invariants.ml
Outdated
| let open_description self opn = | ||
| super.open_description self opn; | ||
| simple_longident opn.popen_lid | ||
| super.module_expr self opn.popen_expr |
There was a problem hiding this comment.
Is this call to super.module_expr really necessary? It should be done already by super.open_description, no?
There was a problem hiding this comment.
Good catch. Fixed.
typing/typedtree.ml
Outdated
| | Texp_constraint of core_type | ||
| | Texp_coerce of core_type option * core_type | ||
| | Texp_open of override_flag * Path.t * Longident.t loc * Env.t | ||
| | Texp_open of override_flag * Longident.t loc * Env.t |
There was a problem hiding this comment.
Cannot we arrange to keep the Path.t here? (This concerns goes away if you manage to support local opens, in which case one would get the full module_expr.)
There was a problem hiding this comment.
Also, if the Tpat_open, Texp_open, Tcl_open, Tcty_open constructors change, perhaps it would be a good opportunity to have them refer to open_description for uniformity (adding the Env.t to open_description).
There was a problem hiding this comment.
I am not sure about supporting local opens. @yallop and I agree that such extension seems not very useful. However, one paper reviewer, you, and ubsan (in this PR) believe it is nice to have.
Let me see if it is possible to resolve grammar ambiguity first.
There was a problem hiding this comment.
I have managed to support local open and refactored all of those variants to use open_description. I only extended Pexp_open this time but not opens in class.
typing/typemod.ml
Outdated
| let type_open_ ?used_slot ?toplevel ovf env loc me = | ||
| match me.pmod_desc with | ||
| | Pmod_functor _ | Pmod_unpack _ | Pmod_extension _ -> | ||
| raise(Error(me.pmod_loc, env, Invalid_open me)) |
There was a problem hiding this comment.
Why is Pmod_unpack always rejected here? Should we plumb funct_body through type_open_ to type_module?
There was a problem hiding this comment.
Fixed. Pmod_unpack is now supported.
Should we plumb funct_body through type_open_ to type_module
I am not following. Can you explain a bit more here?
typing/typemod.ml
Outdated
| ignore (extract_sig_open env lid.loc md.md_type); | ||
| assert false | ||
| end | ||
| | _ -> begin |
There was a problem hiding this comment.
It seems more idiomatic/common to put the begin...end around the tail match...with, not around the whole case.
typing/typemod.ml
Outdated
| let ident = gen_mod_ident () in | ||
| let tme = !type_module_fwd env me in | ||
| leave_struct (); | ||
| begin |
There was a problem hiding this comment.
begin match on the same line is more idiomatic.
typing/typemod.ml
Outdated
| begin | ||
| match tme.mod_type with | ||
| | Mty_signature _ -> () | ||
| | _ -> raise(Error(me.pmod_loc, env, Invalid_open me)); |
There was a problem hiding this comment.
It would be better to list explicitly all cases, and assert false for cases that cannot happen (Mty_alias, I believe).
typing/typemod.ml
Outdated
| mb_attributes=od.open_expr.mod_attributes; | ||
| mb_loc=od.open_expr.mod_loc} in | ||
| Some (tm, md, id, env) | ||
| | _ -> assert false |
There was a problem hiding this comment.
Please list remaining constructors explicitly, it helps reasoning about the code.
typing/typemod.ml
Outdated
| | Psig_open sod -> begin | ||
| let (newenv, od) = type_open env sod in | ||
| let (trem, rem, final_env) = transl_sig newenv srem in | ||
| let remr = ref rem in |
There was a problem hiding this comment.
Please avoid this reference here.
There was a problem hiding this comment.
fixed. I forgot to clean this up.
typing/typemod.ml
Outdated
| end); | ||
| mksig (Tsig_open od) env loc :: trem, | ||
| rem, final_env | ||
| !remr, final_env |
There was a problem hiding this comment.
Can trem contains reference to the generated identifier? It could confuse external tools that would see references to unbound module identifiers. What about keeping the generated identifier in the typedtree?
|
I think it's not really an ideal time to restart this whole discussion, after @objmagic was told that the feature was discussed and approved at the developers' meeting, and subsequently spent significant effort on implementation. While it's always fine to raise new technical issues, the objections seem to be old ones, and rather "soft" than technical, which makes it difficult to reach a definite conclusion. Currently, since |
That only happened a couple of weeks ago, and I don't think Gabriel accurately reflected the discussion at the meeting, where I believe I already raised these points. I have also raised them with @objmagic in person in the past. If everyone else is in favour, then as I said, I'm not going to try and stop it, but I thought that the public discussion on this feature should include my objections. (This was at least partly motivated by a conscious attempt to try to move more of these discussions into the public domain).
The problem with signatures indicates that it is not compatible with both mental models. If |
No, I told him the day of the meeting (November last year).
I agree that having the objections & discussion publicly available is a good idea. My only real objection is to the timing. |
Understandable, but to be fair it's not like I'm making new design objections at the last minute, I'm just reiterating publicly design objections that I've already made privately and consistently to all parties involved. |
Nobody objects to that, of course. But you're also planning to implement and propose a competing design, which is a reasonable thing to do before consensus is reached, but not so much afterwards, unless new issues come to light. |
Well, as I said, I'm going to do that even if this feature is already released because it addresses important use cases not handled by this feature. I don't think the overlap is likely to cause users any confusion because, whilst they can be used to solve the same problems, the mechanisms they use are very different from a user's perspective. |
|
Thank you for posting your view, @lpw25. I believe at least some of the support this feature has received (including my own) stems from the fact that we had no idea you had reservations about it, which only stresses how important it is to have a transparent process. @lpw25 do you think this can get us into similar issues as the recent backwards-incompatible module type change? And in terms of a meta-discussion: Should we start a discussion on Discourse about procedures for these things? Other languages seem to have very specific procedures in place for RFCs and such. |
My objections are purely on the design side. There are no technical issues with this change, nor any backwards compatibility concerns.
PRs are not the place for meta-discussion. Please use one of the other forums (e.g. discuss, caml-list) for such discussions. |
| ref ((fun _env _md -> assert false) : | ||
| Env.t -> Parsetree.module_expr -> Typedtree.module_expr) | ||
|
|
||
| let gen_mod_ident : (Parsetree.module_expr -> Ident.t option) ref = ref (fun _ -> assert false) |
There was a problem hiding this comment.
Could we have a comment on top of this forward declaration, with the declaration site, as for the others above and below?
|
I don't think the eventually of another way to do something similar should stop us from merging this PR. What it does is clean from a semantical point of view. A note on
For these reasons, I'm not stopping you from making a proposal in that direction (actually at least then I thought it was handy), but there is no guarantee that it would be accepted. |
|
Notes from today's caml-dev meeting:
|
|
Are there any notes of the caml-dev discussion? I think it would be better to keep the signature version, too, both for symmetry/consistency, and because it makes it possible to give better signatures in some cases. For example, the following module type t = T
open Set.Make(String)
let union_all : t list -> t = List.fold_left union emptycan be given a signature that matches the implementation very directly: type t = T
open Set.Make(String)
val union_all : t list -> tWithout extended
Both of these seem worse than the version with |
|
@yallop The comment was a bit short indeed. |
|
@garrigue: yes, I've seen #2122, and my comment above makes use of the So, even if #2122 (which I quite like) is merged, I still think it would be better to stick with the original proposal from this PR, and extend |
|
Sorry, seems I didn't read your post to the end.
I'm not sure about your example of of open on paths containing applications. |
|
I'm personally not keen on open with arbitrary module expressions in signatures. Essentially, I don't think that side-effecting expressions should appear in positions where the side-effects won't be executed. However, this position is complicated by a number of issues with OCaml's current module system. OCaml's notion of a pure module expression is a path, but this is simultaneously too restrictive -- I would like to also include structures whose elements are all defined by aliases and functors whose result is a pure module expression -- and too permissive -- in some positions paths are allowed to include applications of side-effecting functors. I have plans, which were discussed at the meeting, to address the first of these issues by extending paths to a more general notion of pure module expression. The second issue is hard to address backwards-compatibly, especially without tracking side-effects in types, so those will probably remain in an awkward middle ground. So I think that open in signatures should be restricted to paths -- including applicative functor applications. Once the notion of path is extended this will include structures containing only aliases. I think that will cover all the useful cases for open in signature. It also obeys the basic idea that side-effect expressions should not be allowed in signatures -- modulo the case of functor applications where OCaml is already a bit off. This is more restrictive than the current patch because it doesn't allow structures that define values, module coercions or generative functor applications, but I think those are both quite dubious in signatures and not really useful for anything. |
|
I mostly agree with @lpw25 on this: it's not especially useful for
Transparent ascription (i.e. the operation |
Yes, I've been calling them something like "pure module expressions" when working on the modular implicits specification.
Yes, I didn't mention it, but adding transparent ascription to the pure module expressions was also discussed at the meeting. You can see the specification that I was using here, although the transparent ascription is somewhat hidden because it is included as part of module aliases -- other uses of transparent ascription can be simplified away so they are not in the formal spec. |
|
Thanks for all the feedback and I am sorry for being absent. I was going through some personal life event and have just moved to a new place. I will go back working on this PR as soon as possible |
|
@trefis has been looking at updating this PR so that it shares its implementation with the code that was added #1892 and is also used by #2122. I think his branch also takes account of the recent feedback (i.e. it restricts open in signatures to paths -- including functor applications), so you two should probably talk before you start updating your code. |
|
Indeed, I had started to look at this PR last week and to rebase it on top of mine. I think I'm close to being done and I will open a new PR in the next few days (while I squashed many commits together, I make sure to keep you as the author as well as keeping your name in the changelog). |
|
superseded by #2147 |
This pull request allows
opento accept an arbitrary module expression.Full paper accepted at OCaml Workshop 2017 (online playground is not up-to-date with this pull request)
Summary
OCaml provides two operations for introducing names exported from one module into another module:
openandinclude. Both operations introduceM's bindings into the current scope.includealso re-exports the bindings from the current scope.A second difference between
openandincludeconcerns the form of the argument. The argument toopencan only be a module path:open A.B.C. The argument toinclude, however, can be any module expression:This PR proposes extending
opento eliminate that second difference, so that bothopenandincludeaccept an arbitrary module expression as argument:includeopeninclude A.B.Copen A.B.Cinclude (M: S)open (M:S)include struct ... endopen struct ... endinclude F(X)open F(X)This extension is not added into local opens.
Examples
The extended
openhas many applications. Please see examples in paper above.Implementation
Most of the work was done in
typing/typemod.ml. For an extended open statement likeopen struct ... end, we go over the following steps:M#1, then insertmodule M#1 = struct ... endbefore open statementEnv.open_signatureonM#1to export bindings into environmentDifferent from paper, this patch also introduced open extension for let-expression. Scope checking of generated module identifier comes for free in let-expression.
I would appreciate reviews and corner cases that I fail to cover.
testsuite/tests/typing-modules/open-struct.mlcontains some test cases that one may find useful when reviewing this patch. If you want to try out this patch, follow the tips here.I am grateful to @yallop and @lpw25 for their help and review.