In an OO language, one of the first questions to ask is how classes and types are composed. Sometimes, when looking at a new language, it's easy to get side-tracked by clever syntax, or syntactic "features", but while these are ultimately important, there is nothing more important in a language than being able to describe the shape of what one is building.
Ecstasy provides three basic shapes from which classes are composed:
The ability of a type to be ordered is a necessary but insufficient capability for iteration, which is what the for loop requires, and if you examine the Range class closely, you will notice that it does not implement Iterable. What it does, instead, is this:
Think of the Sequential interface as the type that is necessary to support the "++" and "--" operators (pre-/post- increment/decrement). When a range of a sequential type is constructed, the composition of the range incorporates the Interval mixin, which in turn, being iterable, provides an iterator that can be used by the for loop.
A range of a non-sequential type cannot be iterated over, and an attempt to do so is detected by the compiler:
Ecstasy provides three basic shapes from which classes are composed:
- Classes, which (just like in Java and C#) are useful for defining instantiable combinations of state and behavior.
- Interfaces, which (just like in Java and C#) are useful for defining contracts, and may allow default behavior to be defined.
- Mixins, which are used to define cross-cutting functionality.
The expression "10..20" is an Range; it defines a "from value" and a "to value". The only requirement of a range is that its type must be Orderable, which is the funky interface that allows two objects to be compared for purposes of ordering.for (Int i : 10..20) { // do something }
The ability of a type to be ordered is a necessary but insufficient capability for iteration, which is what the for loop requires, and if you examine the Range class closely, you will notice that it does not implement Iterable. What it does, instead, is this:
const Range<Element extends Orderable>
incorporates conditional Interval<Element extends Sequential>
Translated into English, that reads: "A range is a constant that contains elements, which must be of an orderable type. Additionally, for ranges whose elements are of a sequential type, the range will automatically incorporate the capabilities of an interval."Think of the Sequential interface as the type that is necessary to support the "++" and "--" operators (pre-/post- increment/decrement). When a range of a sequential type is constructed, the composition of the range incorporates the Interval mixin, which in turn, being iterable, provides an iterator that can be used by the for loop.
A range of a non-sequential type cannot be iterated over, and an attempt to do so is detected by the compiler:
In the "const Interval" declaration shown above, the keyword used to declare the class was "const". To declare a class (in the abstract sense of the term), Ecstasy provides eight keywords:for (String s : "hello".."world") // compiler error { // do something }
- module is used to declare a unit of compilation, or a unit of deployment. Java has a related concept, also called a module, and C# uses the term assembly. A module is a singleton const class; see Modules Overview.
- package is used to declare a namespace within a module, which is kind of like creating a directory within a file system. Like module, a package is also a singleton const class.
- class is used to declare any class that is not specialized as either a const or a service. Classes may be made immutable at run-time, but may not be singletons. For example, see ListMap.
- const is used to declare a class that is immutable by the time that it finishes construction. Furthermore, it automatically provides implementations of a number of common interfaces, including both Orderable, Hashable, and Stringable. Consts can be singletons, and are always immutable. For example, see Int64, aka Int.
- enum is used to declare an enumeration of values. The enumeration itself is an abstract const, and each enum value is a singleton const. For example, see Boolean.
- service is used to declare a potentially asynchronous object, conceptually similar to a Java or C# thread, but in many ways, much closer to an Erlang process. Services may be singletons, and may not be immutable. There aren't any good examples of service in the core library, but the services.x test highlights the asynchronous and continuation-based behaviors of the service, using both an explicit Future-style programming model, and the implicit async/await style.
- interface defines just the surface area (the API) of a class, and may include default implementations of that API.
- mixin declares a cross-cutting composition that can be incorporated into another composition.
I don't know if it's too late for this feedback, but you actually don't need all of class, interface and mixin, just class. Let me explain. (I call this the Celebes Kalossi model of class composition.) For simplicity, I'll just talk about methods and not fields, partly because I think fields should always be private anyhow.
ReplyDeleteA class Derived can be declared as extending another class Base if the public fields and methods of Base are a subset of the public fields and methods available in Derived, and type compatible with them. This is pure interface inheritance: basically class Base is used as just an interface. It means that variables or object fields of type Base can hold objects of type Derived. But it does not mean that any code from Base is automatically run when methods of class Derived are called. Instead, Derived must arrange for implementations itself, which does not necessarily mean containing those implementations in its code.
So how do we achieve implementation inheritance? We don't as such. We provide three method visibilities, public, private, and default (not the same as Java default visibility). Public and private mean the usual things: visible to any class and visible to no class except the defining class. But default-visibility methods are visible to not only their own class but also the classes that incorporate their own class.
What happens when class Foo incorporates class Bar? What methods can method Foo.x() call? Well, any method in class Foo and any public method anywhere. But also the default-visibility methods of Bar.
Just as classes can inherit from any number of classes, they can also incorporate any number of classes. How are conflicts between method names (other than static overloads) handled, since incorporating and incorporated classes are on the same level rather than hierarchically arranged? Not by automatic means, which is very complex and hard to get right (Python does it one way, Dylan another, Common Lisp a third): often the wrong method ends up being called. Instead, the programmer of Foo must, when incorporating Bar, mention any methods of Bar that are to be excluded or renamed so as to avoid conflicts with Foo's methods. Something like incorporates Bar excluding x, renaming y as z. Alternatively, incorporates Bar only a, b, renaming c as d. If this is not done and there is a conflict between Foo and Bar method names that hasn't been resolved in this way, it's a compiler error.
If you find this interesting, let me know at cowan@ccil.org.
The Ecstasy class/type model is relatively firmed up at this point. Ecstasy "fields" (and the "structures" that they are part of) are basically never used directly, except during construction (before the "this" object itself exists) and for things like object serialization. (We don't have that many people coding in Ecstasy at this point, so our experience is mostly from our own use of the language to date.) Here is the code for the Struct interface: https://github.com/xtclang/xvm/blob/master/xsrc/system/Struct.x ... if a class were to define "Int x" and "Int y" properties, then the structure for the class would contain those same properties, but on the structure, those properties on the structure would represent the raw (naked) fields, and not the properties with their potentially dynamic behavior. Outside of serialization, we have no examples of code that has needed to work directly with the structure and the fields, but I'm sure we'll learn more over time.
DeleteEcstasy inheritance, in its simplest form, is similar to C++ (note: with pure virtuals, but without multiple inheritance and the diamond problem) or Java/C# inheritance. Ecstasy mixin incorporation allows mixins to be placed either immediately below ("incorporates") or above (via annotation) the "this" virtual tier. Unsurprisingly, annotations are mixins. Modules, packages, and enums are all examples of immutable singleton classes, and cannot be inherited from or externally annotated. Services are potentially-asynchronous (i.e. message-based, with async/await or promise aka future) objects, and can be singletons as well. Interfaces are similar in nature to those found in Java/C#, with the significant feature difference of funky interfaces (abstract functions -- i.e. not methods -- on an interface). All types are fully reified.
Undesired name collisions _can_ occur, despite the obviously-infinite domain of possible names. (Apparently, there exists a much smaller, and quite obviously-finite domain of good names.) However, inheritance is far less used in Ecstasy than in Java/C#, for example, because it isn't the only tool that exists. ("When every problem looks like a nail ....") Ecstasy supports type delegation, for example, which allows a single line of code delegate (re-route) an entire interface of methods to another object, which makes "composition over inheritance" far, far easier than I've ever seen it done previously (outside of maybe Smalltalk)!
I've never gotten to work in either Smalltalk or Lisp, but I'm guessing that a few of these ideas will seem quite familiar if you have ....
@JohnCowen Your idea of decoupling inheritance from implementation is very interesting. But if I understand you well, how do you manage to create the concept of an interface or abstract class or abstract method with your proposed mechanism? The languages like Python, Dylan or Common Lisp are not as compile time strongly typed like say Java, C# or Ecstasy, so your model while being a smart simplification/generalisation of object-orientation is probably more suited to those more dynamic language beasts, no?
DeleteI'm not sure exactly what you're asking, so I'm not sure if I will be able to do a good job answering it :-D ... Ecstasy is *extremely* strongly typed -- far more than Java or C#, for example. But it's also designed to make the developer's life much easier, with much better type inference, and far fewer compiler hacks (like hard-coded primitive conversions). The other trick that it uses extensively is co-variance, which is how most developers think. (In some languages, a List of Person is not a List. Seriously, WTF?)
DeleteWhile we do not have an "abstract" keyword, we have an annotation that does the same thing, but most of the time the compiler just figures it out on its own. So yes, we have abstract classes (like Java and C#). And we have interfaces (like Java and C#). We also have mix-ins. And we generally do not use type erasure (the concept exists, but it is buried at a depth where developers will generally never experience it).
Sometimes, Ecstasy _feels_ more like a dynamic language, which I think is awesome (there are aspects of Python and Lisp that I really like), but it is far _less_ dynamic than even Java.