-
Notifications
You must be signed in to change notification settings - Fork 40
RFC: val declarations in structures
#59
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
base: master
Are you sure you want to change the base?
Conversation
|
(cc @Octachron, @garrigue, @t6s, @samsa1, @goldfirere with whom I discussed this during a synchronous meeting.) |
|
I think the
What about simply introducing all the new locally abstract types by default instead of requiring a separate syntax? val mem : type a . a -> a list -> bool
let mem elt li =
let rec loop : a list -> bool = function
| [] -> false
| x :: xs -> (x = elt) || loop xs
in loop li |
|
I mention the |
68150a7 to
4f4d77e
Compare
|
I like this idea, but I think we should also consider another possible interpretation of In some languages forward declarations are used to introduce recursion. For example, in Agda types can be forward-declared to introduce recursion, like this: -- forward-declare the `B` type
data B : Set -- Now `B` is in scope
data A where
a : B → A -- okay: `B` has been declared (but not yet defined)
data B where
b : A → BWe could give the same interpretation to val even : int -> bool
val odd : int -> bool
let even n = ...
let odd n = ...One motivation for this alternative interpretation of type unordered_tree (* forward declaration *)
module Tree_set : Set.OrderedType with type t = unordered_tree (* forward declaration *)
type unordered_tree = Empty | Node of int * Tree_set.t (* okay: Tree_set.t in scope *)
module Tree_set = Set.Make(struct type t = unordered_tree
let compare = ... (* also, Tree_set.compare in scope here *)
end)The current alternative is to use a multiple-binding module rec Unordered_tree :
sig
type t = Empty | Node of int * Tree_set.t
val compare : t -> t -> int
end =
struct
type t = Empty | Node of int * Tree_set.t
let compare = ...
end
and
Tree_set : Set.S with type elt = Unordered_tree.t
= Set.Make(Unordered_tree)Adding support for full forward declarations would require addressing various additional design and implementation questions. But I think we should explicitly decide whether we might like to give |
|
I have myself been thinking about more generalized forms of forward declarations, but none of them felt completely right so I haven't submitted a RFC about them. The difficulty in my view is the interaction between forward declarations and shadowing of definitions. But is it actually an incompatible interpretation? Naively I would expect that everything allowed with my proposal would keep the same interpretation with your proposal. Is that not the case, are there examples of programs that would be accepted here and rejected in your world, or whose semantics would change? |
|
Here's a distinguishing example: let f x = 1 + x
val f : int -> int
let f x = 2 + f x
let y = f 3If a forward declaration introduces recursion then this program doesn't terminate. If it instead serves as an ascription for the following definition then it terminates with |
Not necessarily. We could simply say that the forward definition declares the name everywhere after it except in the next definition. I think that it makes sense this way, otherwise things could get too complicated too quickly i think. which i think is probably sensible |
|
I don't understand that proposal. I can imagine two interpretations of "the next definition". Perhaps you mean "the next definition of the same name" (i.e. a binding let f x = 1 + x
val f : int -> int
let g x = 2 + f x
let f x = 3 + f x
Alternatively you might mean "the immediately following definition of any name", which would mean that in the following code val even : int -> bool
val odd : int -> bool
let even n = ...
let odd n = ...both Both of these seem perverse, so perhaps you mean something else. |
|
Sorry if i wasn't clear. Finding the right names for new things is a bit hard ^^ I meant the following (rough) semantics: I believe the difference between your semantics and mine is that your where Legend:
Side note: sorry if i made some gruesome mistakes/oversight when writing the semantics, i'm doing it from some memory/notes from 10 years ago. |
|
Okay, you mean roughly "the next definition of the same name", so that in the following code: let f x = 1 + x
val f : int -> int
let g x = 2 + f x
let f x = 3 + f xthe body of the last |
|
I would love to see this supported, indeed, under one form or another. The proposal about forward declaration is more involved and I'm not a big fan of this type of feature because I find they make the code harder to reason with, although I have been frustrated occasionally by the The original RFC, however, is not very far from the existing code base. In fact, the parser is already accepting |
My reaction to this example is that forward declarations should not implicitly make things recursive, so I think this is roughly similar to the intuition of @kit-ty-kate. |
|
As far as I can see, the point of forward declarations, whether in Agda or C or any other language, is that they make names available in the scope following, to allow referring to those names before the associated definitions are available. I can see the use for the separate signatures proposed in this PR (which are not forward declarations). But I don't understand why you would want forward declarations that don't bring names into scope. What am I missing? |
|
In OCaml (unlike C or Agda I believe) we have a distinction between recursive and non-recursive definitions, where "recursive" can be understood as "has access to things that are in the process of being defined". Note that only term-level definitions are non-recursive by defaults, others are recursive by default and some (but not all) have a So the less liberal interpretation of forward declarations that Kate and myself propose in fact probably coincides exactly with what you want for the cases of type-level recursion that you actually care about, it is only on terms that it is more debatable, and the semantics we propose have the benefits of aligning with the simpler interpretation in this PR (in particular it makes your proposal a secondary aspect that could be brought later on). |
|
Thinking about this a bit more, I can still see examples where the semantics is a bit tricky. let g = ...
val f : ...
val g : ...
let rec f = ... g ...
let rec g = ...With the restriction of your semantics that I propose, This example is close to having incompatible interpretations with my PR and with your proposal, even with the restriction that we proposed, but it isn't actually a counter-example. |
|
My view is that the "less liberal" design for forward declarations is simply not viable from a usability perspective. It's not palatable to have forward declarations that bring names into scope in some definitions but not others. So I personally don't really mind whether or not this PR is compatible with the "less liberal" interpretation, because I don't think there's any prospect of adopting that feature. |
|
I think you don't need the "Locally abstract types in val declarations" if we merge ocaml/ocaml#12732, so I wouldn't bother with that. I didn't read through the entirity of the discussion on forward declarations, so I could have missed it, but I don't think I saw anyone suggest one of the possibilities, which is that for a forward definition to be used before it is defined you need to do: val rec foo : int -> int
let bar i = foo (i + 1)
let foo i = i * 7
endwith a Note that val foo : int -> int
let foo i = foo (i + 1)
(* Error: `foo` cannot be used here as it is not recursive and has not yet been defined. *)You can of course still use val foo : int -> int
let rec foo i = foo (i + 1)In terms of how this affects this current proposal it would change it so that Jeremy's example was an error: let f x = 1 + x
val f : int -> int
let f x = 2 + f x
(* Error: `f` cannot be used here as it is not recursive and has not yet been defined. *) |
|
(While were are in this area, I'd really love to support: val foo x = x + 1too, with an additional restriction compared to |
|
I like the |
4f4d77e to
3db5562
Compare
|
I have updated the RFC to fix a minor mistake ( |
|
Why should we constraint val to immediately preceed the let declaration? In many cases, we never rebind declarations and we could introduce all the (usesul/important) val declarations at the beginning of the file let bindings later on. Perhaps, even more generally, we could let a val declaration just apply to the next declaration of that name. In case of rebinding the same name, it would not apply to the second binding. |
I feel, depending on the codebase, it's either never done or done everywhere. Personally, I shadow my top-level declarations all the time. Then I would argue we could consider doing the opposite, and apply the |
The reason for this restriction is that it is simple and predictable, and all my previous attempts to lift it resulted in weird corner cases (due to shadowing) that people do not agree on, or that I myself felt unsatisfying. This is a minimal proposal that has more chances of gathering consensus than something more complex. |
|
I agree with @gasche's message above, and I would go even further. For me, the one case where a This is not optimal because An extra benefit of this is morally like In contrast, treating |
|
The suggestion is interesting but I would worry that it makes the syntax ambiguous or non-regular. For example, I am not sure how to adapt it to the case of local declarations ( |
|
I'm largely in favor of this direction. I really miss being able to write type signatures, having come over from Haskell. A few points, mostly from my Haskell experience (numbered only for easy reference, not ordering):
Please do not let these subtleties or this post suggest that I want to slow this down. On the contrary: I love this idea and want it to be amazing! |
|
I am also in favor of having a syntax for signatures, and I like the suggested one with
I’m not sure what you mean by “using |
I cleverly left this as an exercise to the reader. :) Without thinking too too hard about it, I think I'd like I don't see this proposed above. And maybe there are holes in it? But something along these lines feels right to me. |
|
The RFC includes a proposal for an Alternative syntax that has the benefit of forming a single structure item, but that is probably going to lose because it looks a bit uglier and no one likes it. The following examples give the idea: let rec
val map : ('a -> 'b) -> 'a list -> 'b list
and map f li = ...
let rec val f : <type>
and val g : <type>
and f <def>
and g <def>
and val h : <type>
and h <def>
and i <def> |
|
Would it be an option to introduce a grammar rule like (in very pseudo code): ? As in allow a bunch of That probably means a breaking change in the parser because it wouldn't behave well with the current parsing of standalone |
Rendered version
Code examples in this PR. No drooling!