Deep thoughts about what scope really means

| | Comments (7)

While pondering how classes within classes work, I was struck by the fact that I really needed to solidify my ideas of what scope's purpose is in REALbasic. Thus far, it's been a pretty straight-forward concept, but with the advent of namespaces has become a bit more blurry. Let's start with some purely hypothetical example code:


Class Outer
Private Class Inner
Sub Wahoo()
MsgBox "Wahoo!"
End Sub
End Class

Function Foobar() as Inner
return new Inner
End Function
End Class


The interesting question here is: what's this mean? Outer.Foobar returns an instance of Outer.Inner -- the static type of the object must remain the same, that much is clear. And if the caller of Foobar happens to live within the Outer context, then everything is hunky dory. But what about outside of the Outer context? It's obvious that no one should be able to say Dim i as Outer.Inner from outside the context of Outer. The user obviously said "Inner is private", so the type cannot be seen. But since the static type of the object remains as Outer.Inner, what happens if the user says:

Dim o as new Outer
o.Foobar.Wahoo

Given the fact that the way lookups currently work in REALbasic is by asking the symbol "give me every member you contain named XXX", this code works today (well, except for the fact that you cannot do classes within classes -- but the concept is the same with classes within modules).

Before I go any further, I want to point out that while this looks like a silly edge case, it is still a very practical design pattern. The memento design pattern is built around the concept of an object being able to hand out a stateful representation of itself that is totally opaque. The consumer of this token can then pass it back to a factory to reconstitute the original object. So, in more concrete terms:


someArray.Append( someItemInst.GetMemento )
// time passes
dim someItemInst as SomeItem
someItemInst = SomeItem.CreateFromMemento( someArray( X ) )

The intermediate holder of mementos cannot actually peek into the memento to get any information at all (so the memento has no public APIs), but the original item can produce or consume mementos to save and restore state.

So that's the background on what got me thinking about scopes; should it be legal to do o.Foobar.Wahoo or not? Research time passed, and what I found was rather interesting: C++ allows it, Java, C# and VB.NET do not. So there's no clear industry standard that says "XXX is the right way to do it." (Not a huge surprise as this is clearly an edge case for any OOP language.)

I decided to have a heart-to-heart with a very smart compiler guy I know to find out what their thoughts are on the topic. We started off by having an even more theoretical discussion (if you can imagine that!) on what the tenets of linguistic design are for RB. The mission statement we came up with is: the language should encourage users to design bullet-proof APIs while still remaining intuitive and easy to approach. REALbasic is really a language that's designed with non-programmers in mind, but should be powerful enough for professionals to easily utilize it as well.

My original take was that the way we're doing things currently is not intuitive, and can in fact be dangerous. I consider myself to be a reasonably smart guy, and I had no clue that this behavior existed. My stance is that most users think of scope as a recursive access modifier. So if you mark a class as private within a given context, that means "only items within this context can know anything about me." In my example above, it means that to items outside of Outer, there's no such thing as Inner -- much less Inner.Wahoo! The current behavior is "dangerous" in that I am assuming I've successfully hidden all of the implementation details about Inner, but I've not! The user can still say o.Foobar.Wahoo, and bypasses my assumption that Inner is totally hidden from view.

My friend's take is that this behavior encourages people to design APIs for immediate use. Instead of saying "give me back an object instance that I can store for an indeterminate amount of time and eventually get around to calling APIs on", it's saying "Give me back that instance so I can call XXX on it right now." The latter is a superior API design because it's more tightly-controlled than the former (and more control is better).

Both of our viewpoints are correct and reasonable definitions of the behavior. The fact that even the "major" languages of the world cannot agree on the behavior just confirms the fact that there's many different ways to skin this cat. Eventually, we came to the agreement that the VB.NET way is sensible and intuitive in this case: the access to a member is never larger than that of the member's containing type. In layman's terms: if you have a private class that cannot be accessed from a particular context, then you can never access any member of that private class either. So with our example, you wouldn't get an error on the o.Wahoo.Foobar line, you'd get the error on Outer.Foobar's declaration, because Inner cannot be exposed.

This rule still has some interesting questions, mostly related to what Inherits and Implements mean. If it's not one thing, it's always another, right? Time for another example:


Module Test
Private Class One
Sub Foobar()
End Sub
End Class

Public Class Two
Inherits One
End Class
End Module


In this case, Test.Two is exposed, but Test.One is not. However, Test.Two inherits the Foobar function from Test.One, but Test.One is private. So what's that mean? Well, inheritance is essentially an automatic copy and paste function. Anything which is inherited from Test.One is "copied" into Test.Two, and so become part of Test.Two's declaration space. Interfaces have a similar example, but with less questions. Let's say Test.One is a private interface, and Test.Two implements it. That's no big deal because anything outside of the Test module cannot access Test.One (so you cannot declare something with its type, can't use IsA or CType on it, etc). Also, since it's illegal to declare anything which can publicly return Test.One (since it's private), that's a non-issue. Fully encapsulated!

Clawing our way out of theory and back into practicality, what does this actually mean? Well, for starters, it means there's a "bug" in the compiler in that it doesn't currently match this expected behavior when dealing with classes or interfaces inside of modules. I use quotes around bug because it's actually behaving by design, it's just that I think the design should change to be more intuitive (based on my current line of thinking, of course). In turn, this means that it's possible there is code relying on said "bug" which may break if it's relying on the current behavior. Of course, since there's no documentation on what the current behavior is, that's not entirely terrible either. ;-) Finally, I think that it's a more well-defined behavior even if the code is a bit of an edge case. I like it when I can describe a linguistic behavior in a few short sentences.

7 Comments

Since you're agonizing over edge cases -- suppose you next declare a subclass SubOuter of Outer. What happens if SubOuter declares a nested class Inner?

And while I'm at it, there is another way to expose instances of otherwise-private classes, and that's through delegates. I don't see a way to break namespacing using them, but it's something to keep in mind as you ponder issues of language.

@Charles -- I assume that SubOuter and Outer are siblings (because otherwise the discussion can end there). In that case, it depends on Inner's scope in exactly the same fashion as any other type declaration. So let's say you have:


Class Outer
End Class

Class SubOuter
Inherits Outer
Class Inner
End Class
End Class

That means you can say: Dim foo as SubOuter.Inner

Maybe I'm missing your point?

@Charles -- the delegate concept isn't a worry to me because that's explicit. It's no different than the fact you can have a private class implement a public interface, or a private method satisfy an interface's contract. In either case, you're explicitly using polymorphism to alter the type, and in either case, only the type owner can make that decision. You can't make a delegate that gets access to a private method from outside the class' scope.

I agree that keeping private/protected(?) nested classes out of the public interface of the containing class makes good intuitive sense. The fact that C++ was the only language on your list that differed on that point says a lot. I would think the philosophy of C++ is pretty close to the opposite of RB's.

I look forward to seeing some of these thoughts work they're way into RB. Nested classes come in handy at times.

Perhaps not quite on the same level as your discussion, but what about this?

Class Foo
Private Sub Constructor
End Sub

Shared Function GimmehFoo() As Foo
End Function
End Class

You're not legally allowed to instantiate a new Foo object using the keyword 'new' (as the constructor isn't exposed), but you certainly can achieve the same thing and have access to Foo by calling the shared GimmehFoo method.

I do realize, though, that these two discussions are different; viz., in your comments, an entire class is private, whereas in mine, the class API is exposed but instantiation is limited. Nonetheless, I would think one could make the argument that you might deliberately design your code as you've shown, giving public access to an object that is also declared as private. Maybe it's a further limitation on scope (no way to call shared methods, for example), or maybe it's a limitation on the API access while still having the ability to get an instance of the object. In the latter case, one might do something like:

Dim o As Object = Outer.Foobar() // works
Dim Outer.Inner = Outer.Inner(o) // fails -- Outer.Inner is private

@Adam -- you're certainly right, you can do that sort of thing. Remember, the type is private -- that just means you cannot know anything about it in specific terms. That doesn't mean you can't use polymorphism on it. So you can do what your example shows -- you can assign it to Object, or Variant. You just can't call any methods on it, or declare its type (unless you're in the proper context, of course).

Leave a comment