Extensible nesting of classes

Many languages support aggressive nesting, from the syntax-only nesting of Java and C# to the more semantically meaningful nesting of BETA and Scala; e.g., a nested class in Scala is qualified by the type of its creating objecting. In any case, a class Inner is only nested in a class Outer if it is physically expressed within it.

I was wondering if anyone ever considered containment constraints before. Rather than nest class Inner in class Outer, why not just specify that class Inner could only be instantiated in the context of an instance of class Outer? This makes containment more like inheritance: class Subclass can inherit from class Superclass after Superclass has been created, and perhaps even in a different module. Why not do the same with containment?

Any references would be helpful. I've turned up some of the basics nesting papers, however I haven't found anyone ever considering containment constraints before. That seems strange as this seems like one of those obvious generalizations.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Isn't this just like a type

Isn't this just like a type class constraint, ie. Outer => Inner?

I get that nesting is often

I get that nesting is often just sugar, but its aesthetics can't be ignored, especially when you considering tooling; e.g., when I type "a." what appears in the list of completions?

That's defined by the

That's defined by the environment (imports, lexical scope) and what's known about 'a' at that point. If 'a' is 'Num a', then you see all of Num's functions. I'm not sure if that answers your question; it seems so straightforward that I suspect you have some trickier issue you'r thinking of.

Possibly related. I'm not

Possibly related. I'm not knowledgeable in how advanced and effective tooling for Haskell is; i.e., is it able to list functions whose first argument is a value of some type?

Another angle on this problem is that we might want to constrain outer objects of arbitrary levels up the object tree. So we might want to say that A requires one of its outer objects to extend B, without specifying if this is the closest outer object.

Sounds familiar

If you ask me, it sounds like you want functors...

Having said that, nesting provides lexical scope, and like other members, an inner class typically has access to the private bits from its surrounding scope. What you describe sounds like the class equivalent of extension methods. With OO, unless you have a language that can define class members for interfaces, it doesn't seem to be equally useful, though.

Functors provide for

Functors provide for parameterized class scope (you can use them to build mixins), not for extensible nesting.

Extension methods allow you to define methods on instances of a class from the outside. I guess what I'm asking about are extension classes, or classes whose instances are qualified by instances of another class, from the outside. I'm designing a new language, so what existing OO languages can and cannot do is not important.

Let's say I define an actor class that is designed only to work inside some game; i.e., it's initialization code depends on it's outer context being an instance of that game. Now, does it make sense to put that restriction in the type system or to have it their more informally via a constructor argument? The primary advantage might just be in tooling; e.g., intellisense adds the new actor class to the set of objects that can be qualified by a game of that type.

"Containment"

But what does "containment" mean if not lexical scoping? All your feature seems to provide to a class is a reference to some other class that client code can then instantiate. You won't have special access to it. AFAICS, all you'd gain is the ability to write o.C instead of C(o) to name the class. So what?

Typing

I suspect this is a hint, from the OP: "e.g., a nested class in Scala is qualified by the type of its [containing] object." I'm guessing that Sean wants:

o : O ⊦ new o.C() : o.C,

or at least

o : O ⊦ new o.C() : O.C.

In either case obviously the semantic nesting is significant, although only the first case actually seems useful. For a constructor argument, of course, we only have

o : O ⊦ new C(o) : C.

Syntax confusion

I didn't mean C(o) as a constructor invocation but as generic instantiation (or functor application, or what you prefer). So C[o] in more Scala'ish syntax. Or with your example:

o:O ⊦ new C[o]() : C[o]

My point is that I fail to see the substantial semantic difference between this and the first version above. I think this could only ever be relevant if you had object types that contain classes, and you'd need this kind of extension classes to adapt a given object. But that seems pretty esoteric.

Non-standard notion of functor?

I would say that the usual notion of functor would give the type C[O], rather than C[o]. This is pretty much the difference between my original first and second versions. Some form of dependent typing is required to get the latter, and path-dependence via nesting has proven to be workable. So I'm still not 100% sure we're talking about the same thing, but I may well be misunderstanding.

Ah, no

Ah, no. A functor can give you this kind of dependency. That was my point.

How?

Could you explain how a functor can give this kind of dependency? Little o is a value, not a type. In most settings that I'm familiar with, you cannot even apply a type constructor to a value, so the application C[o] would be nonsensical.

(In fact, something like this is legal in Scala, but only because they have subtyping and singleton types. So I actually can write something like C[o.type].)

Suddenly it occurs to me that maybe you specifically mean something like ML functors and not just type constructors or generics. As I recall, it is possible to parameterize ML functors by values. Is this what you have in mind?

To be clear, I'm not playing dumb, I really don't get what you mean, and am hoping for enlightenment here...

Yes

Well, yes, I talked about functors. I wasn't very specific about what exactly I had in mind there, and there are several possibilities. Most directly, you can just take the module level itself: with functors it is possible to parameterize a signature (i.e. a module type) by a structure (i.e. a module value). And structures are closely resembling objects.

More concretely, e.g. in Ocaml you can say

module F(X : O) =
struct
  module type S = ...
end

and then F(M).S would be a signature, instantiated by a module M.

Now, the syntax is a bit awkward, and unlike modules, objects are first-class by default. But those aren't crucial differences in this context, I think.

Got it

Yes, that makes sense. I thought you meant functor in a much more general "type constructors are functors in some category" sense. Now I see what you mean.

A bit more general

Say you could express a type O..C that would match any instance of class C in an instance of type O with any number of objects in between in the object graph.

Also, for your last case, we could go with dependent classes, then

o : O ⊦ new C(o) : C(o)

But even then, we still have the problem that containment, namely indirect containment, cannot be captured directly in a type system that doesn't model it first class.

Sorry, I still don't get it

Sorry, I still don't get it. If I have a variable x : O..C, what could I do with x that I cannot do with x : C? If I write a class C that is "contained" in O (or o? I'm still not sure what you're actually asking for), what can I do inside it that I cannot do with a parameter O (or o : O)?

Concept Oriented Programming

Concept Oriented Programming seems related - a cousin of OOP with heavy emphasis on containment relationships. Though I've not paid much attention to its development over the last five years, so you'll need to use your google-fu to find more.

Great reference! They seem

Great reference! They seem to call this concept inclusion. So you can specify that "Button in Panel." I would prefer "Button is Widget" and then "Widget in Panel".

Concepts seem like just another kind of object system. The weird name really turns me off and makes the work difficult to get through.

So you can specify that

So you can specify that "Button in Panel." I would prefer "Button is Widget" and then "Widget in Panel".

In concept-orientation (both the programming and the data models), to be a member of a set means to be more specific than this set and to inherit from this set.

IS-A is a particular case of IS-IN relationship where a parent has one child only. For example, "Button is Widget" means that there is always one Button object included in a Widget object - yet, it is still inclusion. In this case, the Button object does not have its own identity - its identity is inherited from and shared with the parent Widget object.

No, it isn't

Naasking: There is no requirement that Inner be a subclass of Outer at all, and it frequently isn't. What it means is that instance/class methods of Inner also have access to the instance/class fields/members/variables of Outer.

In principle, there is no reason why contained scopes of any sort have to be textually within the containing scope, but as far as I know, no programming language that is still based on text has abandoned this constrai

Naasking: There is no

Naasking: There is no requirement that Inner be a subclass of Outer at all, and it frequently isn't. What it means is that instance/class methods of Inner also have access to the instance/class fields/members/variables of Outer.

Yes, I know what it means given I work with C# every day. The scoping rules for nested types in such languages are very ad-hoc though, and the type class constraint I mentioned was intended to rigourously define the static context/environment/scope available to Inner. This requires a more explicit specification of the bindings that Outer exposes, but I'm not sure that's a bad thing.

Whether introducing the inheritance relationship is a problem is debatable. It doesn't seem ideal at first blush, but any rigourous specification of the opened context will be publicly visible at Inner's constructor in some way, so why not a type class inheritance relationship?

I find this an interesting

I find this an interesting discussion. By total coincidence, over the last week I've been wondering if some version of the "class" construct could provide a broader concept of "context" for other language constructs.

My own initial motivation was to explore whether the runtime guts (or some of the guts) of various types of applications - GUI programs, command line programs, .so/DLL's, daemons/services, various instances of formal component models, various GUI or application extensions, etc. - could be made 1) first class in the language; 2) accessed with more or less convenience by the application programmer.

Then these various "application runtime contexts" might become language level artifacts (classes in libraries, etc.) open to extension and creation, and not exist only as extra-linguistic magic incantations during linking or check boxes in an IDE and so on (to the typical application programmer).

I haven't got very far, but one minor suggestion is to think of nested abstract classes that are then subclassed "from outside" by concrete classes that automagically inherit the abstract classes' contextual scope and access privileges. Just one idea I've been toying with.

- S.

Multidimensional Virtual Classes

Discussed previously on LtU, the Multidimensional Virtual Classes approach decouples virtual classes from nesting. A virtual class in their system may depend on the type of multiple objects (and not just a single object of a syntactically-distinguished "outer" class).

There seems to be some confusion, though, about whether you mean "containment" in the sense of a virtual-class-like discipline (where classes are properties of objects - perhaps externally-defined properties), or in a simpler sense of scoping and visibility of names.

I'm well aware of the work.

I'm well aware of the work. If you want to reduce nesting to a simple parameter, it will work. However, if you want to abstract over the object tree, then you would need something stronger with containment built in.

Both scoping and semantics are meaningful here, since if you can abstract over the object tree, scoping is no longer simply syntactic sugar.

So this is where I am confused

Your original post says "why not just specify that class Inner could only be instantiated in the context of an instance of class Outer." isn't this precisely what parameterizing class Inner on an instance of class Outer achieves?

In that case the members of the Outer instance would not implicitly be in scope for the body of Inner, but that could hypothetically be handled by a separate "using" sort of mechanism.

I'm not sure what you mean by "abstract over the object tree," beyond what a system like the above provides. I'm interested to know, though, so if you have the time to explain I'd appreciate it.

A couple of points:

A couple of points:

  • Aesthetics are a concern here. Does it make sense to have syntax for saying that a Button can only be created inside a Panel? Or is it better to (a) syntactically nest the Button class inside the Panel class so button objects are always qualified by a panel object or (b) include Panel as an argument to Button (the common approach)?
  • Abstracting over the object tree means being able to talk about properties of a node or nodes along a path in the tree vs. a very specific node, like the one immediately above the object. For example, if a Button can only be created in a Panel, does it make sense for that the Panel object to always be the immediate parent of the Button object, or could Panel just be some parent of the Button?

Part of my reason for posting is to understand and clarify the problem, or to figure out if there is really any problem at all :)

More than aesthetic

Does it make sense to have syntax for saying that a Button can only be created inside a Panel? Or is it better to (a) syntactically nest the Button class inside the Panel class so button objects are always qualified by a panel object or (b) include Panel as an argument to Button (the common approach)?

All the syntactic nesting approaches make it hard to adapt the framework when next weeks UI paradigm allows buttons somewhere else as well, you can't syntactically nest in two places. So changes are likely to break existing client code.

Passing parent as a parameter (b) is more flexible but may not find illegal parents until runtime (depends on your implementation).

Making the parent a type parameter (by any available mechanism) should allow it to be checked at compile time.

I think the problem is you are trying to express the nesting of instances (eg buttons in panels) with nesting of the types themselves.

I see this as similar to the old OO design argument of design by nouns in the problem space but they may be implemented by a different set of nouns in the code space vs problem space nouns ARE the nouns in the code space.

I'm not really interested in

I'm not really interested in the difference between type nesting and object nesting, to me they are pretty much the same and its only an accident that type nesting was ever considered separately. OO types really should just be seen as quantifiers over objects, and nested types would fall naturally out of object nesting.

You can generalize Button's container from being a panel to being a more general type. This is a hard change that requires editing the source code, but would you expect anything else? You should also be able to modularly create a new kind of button that expects a more specific kind of parent (this is possible in Scala).

Doesn't Beta already do this?

Rather than nest class Inner in class Outer, why not just specify that class Inner could only be instantiated in the context of an instance of class Outer? This makes containment more like inheritance: class Subclass can inherit from class Superclass after Superclass has been created, and perhaps even in a different module. Why not do the same with containment?

Doesn't Beta do this or am I misunderstanding your question?

Let me illustrate with an example using C++ like syntax (it has been years so I may have forgotten or misremembered a few things):

class X {
  int a, b, c;
  class Y { int d, e; };
};

X    x1, x2;
x1.Y y1, y2;
x2.Y z1, z2;

Here y1, an instance of X::Y is created in the context of x1, an instance of X and so on. Thus y1.a and y2.a refer to x1.a, while z1.a and z2.a refer to x2.a1.

To make this work, the compiler arranges to store a hidden ref to the outer class instance along with an inner class instance. Beta uses "origin" to refer to the containing object. In other words,

&x1 == &y1.origin == &y2.origin
&x2 == &z1.origin == &z2.origin

In C++ you can define `static' variables within a class, to be shared by all instances but these are globals (even if you can only reference them in the context of the containing class). Beta is better in that different instances can share different containing class variables. I think implementing such an extension to C++ would be almost trivial!

In Beta you can extend nested classes as well. For instance:

XX: X {
  ...
  int d;
  YY: Y { ... d ... };
};

XX xx1;
xx1.YY yy1;

Here YY extends X::Y so yy1.d refers to the d defined in xx1.Y and not xx1.d.

See Semantic Analysis of Virtual Classes and Nested Classes

1 This is analogous to referring to variables defined in a containing function of a nested function (which may be why Beta authors unified both functions and classes into "patterns").