Skip to content

Support inherited field in object type expression#1118

Merged
gasche merged 17 commits intoocaml:trunkfrom
objmagic:object-inherit
Jul 28, 2017
Merged

Support inherited field in object type expression#1118
gasche merged 17 commits intoocaml:trunkfrom
objmagic:object-inherit

Conversation

@objmagic
Copy link
Copy Markdown
Contributor

This PR allows one to inherit closed object type expression when defining object type.

Here is an example. Entering the following code in interpreter

type t = <m:int>
type g = <n:string; t>
type h = <x:string; y:int; g>

and you will have the following output

type t = < m : int >
type g = < m : int; n : string >
type h = < m : int; n : string; x : string; y : int >

This is an OCaml Labs compiler hacking task. Also, there was a thread on caml-list last month discussing about it.

One small refactor along with this PR is that I added location on object field label and polymorphic variant tags. See parsetree.mli#L156.

@objmagic
Copy link
Copy Markdown
Contributor Author

objmagic commented Mar 23, 2017

One problem I am aware of is that the following code

class type t1 = object method f : int end
type t = < #t1 >

will give you error message: "Error: The type #t1 is not an object type".

Not sure if it makes sense to support this pattern though.

Update (04/06):

class type t1 = object method f : int end
type t = < t1 >

is supported now

@bobzhang
Copy link
Copy Markdown
Member

bobzhang commented Mar 23, 2017

this looks really useful, structural typing are very useful for projects like JSOO and BuckleScript. some bikeshedding about syntax, g looks punning, maybe we can it more explicit:

type t = < x : int ; y : int; inherit g >

@lpw25
Copy link
Copy Markdown
Contributor

lpw25 commented Mar 23, 2017

some bikeshedding about syntax, g looks punning, maybe we can it more explicit

There's no punning anywhere in the type algebra so I doubt it will confuse people. Whereas using inherit will likely perpetuate people's misunderstandings about the relation between class and object types. Inheritance is about inheriting behaviour, whilst it can cause the object type associated with a class to be extended it is not the same thing and I don't think it should use the same syntax.

@alainfrisch
Copy link
Copy Markdown
Contributor

Inheritance is about inheriting behaviour

inherit is also used in class types, where it doesn't imply any inheritance of behavior:

class type c = object method x: int end
class type d = object inherit c method y: int end

@bluddy
Copy link
Copy Markdown
Contributor

bluddy commented Mar 23, 2017

So maybe it's more accurate to say that inherit is associated with nominal typing rather than structural, and we don't want to confuse the two?

@lpw25
Copy link
Copy Markdown
Contributor

lpw25 commented Mar 23, 2017

inherit is also used in class types, where it doesn't imply any inheritance of behavior

I know and it's kind of wrong there too. The class system has quite a few things like this which subtly confuse different concepts, and I've always thought that it was one of the reasons people have a lot of trouble understanding it. I'd rather not add one more to the list, even if it would not be the worst offender.

@yallop
Copy link
Copy Markdown
Member

yallop commented Mar 23, 2017

I like the syntax that @objmagic has implemented because it parallels the syntax for polymorphic variants:

  type d = [`A of int | `B of int | c ]
  type d = < a :  int ;  b :  int ; c >

Rather less importantly, it's also reminiscent of the functional update syntax for records:

  { r with x = a ; y = b }
  < r ;    x : a ; y : b >

@gasche
Copy link
Copy Markdown
Member

gasche commented Mar 23, 2017

it parallels the syntax for polymorphic variants

This is the killer argument, and I think it concludes the syntactic discussion.

Any takers for an actual code review? :-)

Copy link
Copy Markdown
Member

@yallop yallop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to have a few more tests, e.g. where an extended type is more than just a simple name

   type 'a t = < m: 'a >
   type s = < int t >

   module M = struct type t = < m: int > end
   type u = < M.t >

   type r = < a : int; < b : int > >

and where an extended type is empty

   type e = < >
   type r1 = < a : int; e >

   type r2 = < a : int; < > >

@objmagic
Copy link
Copy Markdown
Contributor Author

@yallop Thanks! These are good tests. I fixed one bug when handling empty object type expression (7743e96)

@objmagic
Copy link
Copy Markdown
Contributor Author

@yallop would you like to also have a look at my question at #1118 (comment)?

@Octachron would you like to help review change on odoc? I was thinking to add some tests for that but I do not know how to do that.

@garrigue
Copy link
Copy Markdown
Contributor

I've been thinking of adding this features a number of times.
I always stopped short of doing it, because one can already do that using class types.
So this is a "political decision".
I agree that re-establishing the symmetry with polymorphic variants would be nice, but we need to discuss that on caml-devel.

For the implementation, I'm a bit concerned by the way transl_fields is implemented.
This is clearly not symmetrical with polymorphic variants: rather than unification, fields are copied, and as a result repeated declaration of a field is not allowed. This may be a problem with multiple inheritance. I think that keeping things in line with polymorphic variants would be better; actually the restriction on duplicate field declarations doesn't seem relevant for object types.

@objmagic
Copy link
Copy Markdown
Contributor Author

objmagic commented Mar 24, 2017

@garrigue I agree that implementation should be symmetrical with polymorphic variants' implementation. However I don't understand your statement "the restriction on duplicate field declarations doesn't seem relevant for object types". Do you mean type t = <a: int; a: string> (not allowed now) should give you type t = <a:string>, following the rule "last wins" in class?

@garrigue
Copy link
Copy Markdown
Contributor

@objmagic No, this should give you a unification error, like for polymorphic variants. We're talking about objects, not modules.

@objmagic
Copy link
Copy Markdown
Contributor Author

objmagic commented Mar 27, 2017

@garrigue Thank you very much for the comments! I have updated my PR to use unification instead (d459674). If I understand your words correctly, I removed "restriction on duplicate field declarations" (as a side-effect of using unification)

@Octachron
Copy link
Copy Markdown
Member

@objmagic On the ocamldoc side, if you want to add tests, there are two kind of tests available in the testsuite. First, basic type printing tests are located in testsuite/tests/tool-ocamldoc, in particular testsuite/tests/tool-ocamldoc/t01.ml already tests the printing of object types.
Then, the various backend are tested in

  • tool-ocamldoc-2: latex
  • tool-ocamldoc-html: html
  • tool-ocamldoc-man: man pages

Adding an Object_types.mli test file in at least one of the back-end tests would probably be a good idea.

Looking at the code and ocamldoc output, currently the information about inherited object type is not used by ocamldoc. Consequently

type t = <a:int (** comment for a *) ; b:int (** comment for b*) >
type r = < t (**comment for inherited t*) ; c:int (** comment for c*) >

is printed as

type t = <a:int (** comment for a *) ; b:int (** comment for b*) >
type r = < a:int; b:int ; c:int (** comment for c*) >

Ideally, I think it would be better to not expand inherited fields in the documentation and add the possibility to comment on inherited fields. However, these are quality of life improvements, and could be the object of a subsequent PR, since ocamldoc is neither crashing nor missing object fields in presence of inherited fields.

| Otag ({txt=name}, _, ct) ::
((Oinherit ct2) as ele2) :: q
| Otag ({txt=name}, _, ct) ::
((Otag (_, _, ct2)) as ele2) :: q ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worthwhile to fusion the two cases here into

| (Otag ({txt=name}, _, ct) ) :: ((Oinherit ct2 | Otag (_, _, ct2) ) as ele2) :: q

let pos2 = Loc.ptyp_start ct2 in
let (_,comment_opt) = just_after_special pos pos2 in
(name, comment_opt) :: (f (ele2 :: q))
| _ :: q -> f q
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the OInherit _ case is essentially missing here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Octachron Thanks for your review. Coming back to this comment after three months, I am still not sure what to do here because inherited field gets expanded in documentation (in elsewhere as well, e.g. toplevel display). This means when we encounter an inherited field (Oinherit of core_type), we want to fetch documentation associated with it somewhere, which means we need to save doc generation result for all object type declaration. I am not sure how to handle cross-module/file case either. Any suggestion is welcomed here...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@objmagic, sorry for the delayed answer. I will try to have by a look by the end of the month to see if it is possible to implement documentation comment for inherited fields in a reasonable way within ocamldoc. However, I don't think that non-optimal support within ocamldoc should block integration ((inlined record had a quite imperfect support for a time, and documentation comment can still not be attached to polymorphic variants)).

@yallop
Copy link
Copy Markdown
Member

yallop commented Mar 28, 2017

One problem I am aware of is that the following code

class type t1 = object method f : int end
type t = < #t1 >

will give you error message: "Error: The type #t1 is not an object type".

That's the correct behaviour, since #t1 is not a closed object type. The following should be accepted, though (and apparently is):

class type t1 = object method f : int end
type t = < t1 >

@objmagic
Copy link
Copy Markdown
Contributor Author

@garrigue Ping again. What do you or caml-dev think about this PR?

@garrigue
Copy link
Copy Markdown
Contributor

Should check it in more detail, but the feature is straightforward and the behavior reasonable.
The only question left is whether we want to include something that is already possible using class type. I suppose a strong argument is from Javascript users, who use object types but not classes.

Ultimately, since this is a new feature, I think we need a consensus: discuss it on caml-devel or at a physical meeting.

@gasche
Copy link
Copy Markdown
Member

gasche commented May 25, 2017

If the code is good (and I'm not competent to review it), I would be in favor of taking the feature in. It does not add complexity (especially given that polymorphic variants already support it), and I don't find the redundancy with class types an issue here -- and I have used object types myself in situations where this would have been handy.

@fpottier
Copy link
Copy Markdown
Contributor

The feature seems generally reasonable to me.

What is the desired behavior in case two methods have the same name?
Do we get a type error? a unification of the two types of the two methods? the more recent method hiding the older one?

Also, considering that repeated method declarations are currently not allowed:

# type t = < m: int; m: int >;;
Error: This is the second method `m' of this object type.
       Multiple occurences are not allowed.

it might make sense to allow this if it is allowed indirectly (via inheritance).

@objmagic
Copy link
Copy Markdown
Contributor Author

@fpottier Thanks for your review.

it might make sense to allow this

I agree. And I'd like to hear other people's opinion of your comments as well.

@yallop
Copy link
Copy Markdown
Member

yallop commented Jul 22, 2017

When both inherited types have methods with the same names I think unifying the types of the methods is the best approach.

OCaml already performs unification when two inherited polymorphic variable types have the same tag. For example, in the following code 'a and 'b are unified, since the type annotation on x inherits the types [`M of 'a] and [`M of 'b]:

# fun (_ : ('a * 'b)) x -> (x : [ [`M of 'a] | [`M of 'b] ]);;
- : 'a * 'a -> [ `M of 'a ] -> [ `M of 'a ] = <fun>

With this PR the behaviour for object types is similar, unifying the types of the methods:

# fun (_ : ('a * 'b)) x -> (x : < <m: 'a> ; <m: 'b> >);;
- : 'a * 'a -> < m : 'a > -> < m : 'a > = <fun>

This PR also relaxes the constraint that currently prohibits having multiple occurrences of methods with the same name. In OCaml 4.05.0:

# type 'a t = [`M of 'a | `M of int];;
type 'a t = [ `M of 'a ] constraint 'a = int
# type 'a t = <m: 'a ; m: int>;;
Characters 12-28:
  type 'a t = <m: 'a ; m: int>;;
              ^^^^^^^^^^^^^^^^
Error: This is the second method `m' of this object type.
       Multiple occurrences are not allowed.

With this PR:

# type 'a t = <m: 'a ; m: int>;;
type 'a t = < m : 'a > constraint 'a = int

@objmagic
Copy link
Copy Markdown
Contributor Author

Yes I just realized this has been implemented. I have updated tests.

@damiendoligez
Copy link
Copy Markdown
Member

This PR also relaxes the constraint that currently prohibits having multiple occurrences of methods with the same name.

I'd like to hear what @garrigue has to say about this point.

@garrigue
Copy link
Copy Markdown
Contributor

If we seek symmetry with polymorphic variants, I think it is natural to remove this constraint.

Copy link
Copy Markdown
Contributor

@garrigue garrigue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation is clean, and in clear symmetry with polymorphic variants.
If nobody objects, I propose merging this before 4.06.

@gasche
Copy link
Copy Markdown
Member

gasche commented Jul 25, 2017

I think that merging earlier is better than later to avoid conflicts. I'm planning to go ahead and merge in a few days if nobody does it before or objects.

I checked that the CI failures are not an issue (they are failure in Camlp4 currently not building under trunk).

@objmagic
Copy link
Copy Markdown
Contributor Author

@gasche Sounds good. I agree that if this PR looks all good, merge it sooner so that we don't need to deal with more merge conflicts.

@gasche gasche merged commit aa6b5b9 into ocaml:trunk Jul 28, 2017
gasche added a commit to gasche/ocaml that referenced this pull request Jul 28, 2017
…objects

PR ocaml#1118 had turned polymorphic variant constructors from 'label' to
its definition 'string' for consistency with field names in object
types; instead, we consistently name 'label' the method and instance
variable names throughout the AST. This does not break compatibility
as the two types are synonym, but it should improve readability of
parsing/parsetree.mli.
@gasche
Copy link
Copy Markdown
Member

gasche commented Jul 28, 2017

I made a mistake when assessing the travis CI results: it is indeed this PR that is breaking Camlp4, and this should be fixed. In the meantime, #1265 proposes to temporarily disable camlp4 building to avoid breaking other PRs as well.

gasche added a commit to gasche/camlp4 that referenced this pull request Jul 28, 2017
@gasche
Copy link
Copy Markdown
Member

gasche commented Jul 28, 2017

See camlp4/camlp4#123.

gasche added a commit to gasche/camlp4 that referenced this pull request Jul 28, 2017
gasche added a commit to camlp4/camlp4 that referenced this pull request Jul 28, 2017
aantron added a commit to ocamllabs/odoc-dev that referenced this pull request Nov 22, 2017
4.06 incompatibility was caused by changes to Typedtree in
ocaml/ocaml#1118 and ocaml/ocaml#1249.
jonludlam pushed a commit to jonludlam/odoc-parser-cleaned that referenced this pull request Jul 1, 2021
4.06 incompatibility was caused by changes to Typedtree in
ocaml/ocaml#1118 and ocaml/ocaml#1249.
stedolan pushed a commit to stedolan/ocaml that referenced this pull request Mar 21, 2023
* Add layout to params of primitives

* update tests
EmileTrotignon pushed a commit to EmileTrotignon/ocaml that referenced this pull request Jan 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.