2019/07/14

Composition

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:
  • 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.
One example of each of these from the core library is the Interval class, the Sequential interface, and the Range mixin. Consider this simple example:

for (Int i : 10..20)
    {
    // do something
    }
The expression "10..20" is an Interval; it defines a "from value" and a "to value". The only requirement of an interval is that its type must be Orderable, which is the funky interface that allows two objects to be compared for purposes of order.

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 Interval class closely, you will notice that it does not implement Iterable. What it does, instead, is this:
const Interval<ElementType extends immutable Orderable>
        incorporates conditional Range<ElementType extends Sequential>
Translated into English, that reads: "An interval is a constant that contains elements, which must be of an orderable type. Additionally, for intervals whose elements are of a sequential type, the interval will automatically incorporate the capabilities of a range."

Think of the Sequential interface as the type that is necessary to support the "++" and "--" operators (pre-/post- increment/decrement). When an interval of a sequential type is constructed, the composition of the interval incorporates the Range mixin, which in turn, being iterable, provides an iterator that can be used by the for loop.

An interval of a non-sequential type cannot be iterated over, and an attempt to do so is detected by the compiler:
for (String s : "hello".."world")   // compiler error
    {
    // do something
    }
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:
  • 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.
Each of these, and the forms of composition available to each, will be covered in more detail in subsequent articles. In the meantime, if you're curious about the raw syntax, see bnf.x, and if you're curious about how the parsing of the syntax works, see parseTypeCompositionComponent() in the Parser. The AST node for type compositions is TypeCompositionStatement.

2 comments:

  1. 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.

    A 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.

    ReplyDelete
    Replies
    1. 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.

      Ecstasy 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 ....

      Delete

All comments are subject to the Ecstasy code of conduct. To reduce spam, comments on old posts are queued for review before being published.