Crack has a problem in the lack of distinction between scoping and
dereferencing. Scoping is used to reference a symbol that is part of an
external namespace, dereferencing is used to access fields or methods on an
object.
Scoping example:
class A {
class B {}
}
# Access class B which is scoped to A.
b := A.B();
Dereferencing example:
class A {
void f() {}
}
# Apply method f() to an instance of A.
A().f();
The first example is notable in that it doesn't work: in fact, there's
currently no way to access inner classes except through the typeof() operator.
The only places where we can really currentlly do scoping is when invoking
static methods and when explicitly accessing a member for a base class:
class A {
void f() {
cout `in A.f()\n`;
}
}
class B {
void f() {
cout `in B.f()\n`;
A.f(); # Call A's f() method directly.
}
@static g() {}
}
B.g(); # Invoke a static method.
There is a semantic distinction between the two uses: scoping is simply
accessing a definition within a namespace, dereferencing is accessing a
definition for a specific target object. In the case of virtual methods, the
definition can vary depending on the runtime value of the target object.
Our approach of using the dot-operator for both scoping and dereferencing was
motivated by languages like Java and Python, which do not make a distinction
between scoping and dereferencing. However, in both of these languages there
are mitigating factors: in java, namespace constructs like packages and
classes cannot be deferenced, so the dot notation unambiguously refers to
scoping when used with these entities. In python, scoping is implemented via
dereferencing, so there really is no distinction.
But in Crack, there is some degree of ambiguity between the two forms because
classes are both namespaces and objects. Classes have attributes (e.g. "name"
and "bases") and methods (e.g. "isSubclass()"). So we get weird behavior like
this:
import
crack.io cout;
class A {
@static bool isSubclass(Class other) { return false; }
}
cout `$(A.isSubclass(Object))\n`; # Prints "false"
Class a = A;
cout `$(a.isSubclass(Object))\n`; # Prints "true"
The first 'cout' calls the static isSubclass(), the second calls the
isSubclass() method on the class.
As stated earlier, one of the main use-cases for class scoping is explicitly
identifying a base class variation of a virtual function. But this only works
in a method defining the "this" variable, and can only be applied to the value
of that variable. There is no direct way to do this on any other value.
To do so, the user needs to explicitly provide a method to expose the base
class method:
class A {
void f(A other) {}
}
class B : A {
void __A_f() {
return A.f();
}
void f(A other) {
other.__A_f();
}
}
Beyond all of this, as suggested earlier, the implementation of scoping in
Crack is fundamentally broken. The dot operator doesn't have any magic to
support scoping for classes, the only reason scoping works at all is that we
copy method definitions to meta-classes. This works for some subset of
features, but not universally.
There are several ways we could fix all of this.
1) Just fix scoping. Make the dot operator first do a scoping lookup when
applied to a class, then a do a referencing lookup on failure. Nested
class resolution would work, users could disambiguate between scoping and
dereferencing a class by enclosing the class in parenthesis: (A).f() (Not
currently supported.)
There would still be no way to specify an explicit base class variation of
a virtual function, we'd have to sacrifice that.
2) Provide an explicit scoping operator '::'. Use the scoping operator
exclusively for scoping, the dot operator exclusively for dereferencing.
This is probably easier to implement than #1. It also has the advantage
of very clear semantics and it fixes all of the problems I've identified.
Explicit base class variations of virtual functions would be accessed as
follows: other.A::f().
The disadvantage is that users need to fully understand the difference
between the two operators. It also creates dissonance with the way that
imports are done: to be fully consistent, we would have to specify
module names as scoping operator separated: import foo::bar baz;
Finally, this approach breaks backwards compatibility. Some of our code
would have to be rewritten.
3) The compromise. Fix scoping as in #1, and _allow_ (but do not require)
the use of the scoping operator to allow users to force scoping in cases
where it would otherwise be inaccessible.
This fixes all of the problems and is backwards compatible, but it's less
straightforward than either of the other options. Users must be aware
that there are edge cases where the scoping operator is necessary, and
they have the option to use the dereference operator in cases where it is
inappropriate.
I'm currently leaning toward option #1. If anyone can cite a compelling case
for any of the other options, please let me know.
=============================================================================
michaelMuller =
mmu...@enduden.com |
http://www.mindhog.net/~mmuller
-----------------------------------------------------------------------------
There is no way to find the best design except to try out as many designs as
possible and discard the failures. - Freeman Dyson
=============================================================================