1.1 Design Goals and Major Features
Cecil's design results from several goals:
- Maximize the programmer's ability to develop software quickly and to reuse and modify existing software easily.
- In response to this goal, Cecil is based on a pure object model: all data are objects and objects are manipulated solely by passing messages. A pure object model ensures that the power of object-oriented programming is uniformly available for all data and all parts of programs. The run-time performance disadvantage traditionally associated with pure object-oriented languages is diminishing with the advent of advanced implementations.
- Our experience also leads us to develop a classless (prototype-based) object model for Cecil. We feel that a classless object model is simpler and more powerful than traditional class-based object models. Cecil's object model is somewhat more restricted than those in other prototype-based languages [Borning 86, Lieberman 86, LaLonde et al. 86, Ungar & Smith 87, Lieberman et al. 87], in response to other design goals.
- Since message passing is the cornerstone of the power of object-oriented systems, Cecil includes a fairly general form of dynamic binding based on multiple dispatching. Multi-methods affect many aspects of the rest of the language design, and much of the research on Cecil aims to combine multi-methods with traditional object-oriented language concepts, such as encapsulation and static type checking, not found in other multiple dispatching languages.
- Inheritance also plays a key role in organizing software and factoring out commonalities. Cecil extends traditional inheritance mechanisms with predicate objects to support an automatic form of classification of objects into specialized subclasses based on their run-time state. Since this state can be mutable, an object's classification can change over time. This mechanism enables inheritance and classification to be applied even when modelling time-varying properties of an object. For example, a rectangle can be automatically classified as the predicate subobject square whenever it satisfies the predicate that its length equals its width, even if the rectangle's length and width are mutable.
- Instance variables (called fields in Cecil) are also accessed solely by sending messages, enabling fields to be replaced or even overridden with methods, and vice versa, without affecting clients. Fields can be given default initial values as part of their declaration. An initialization expression is evaluated lazily, when the field is first referenced, acting as a kind of memoized constant function. By allowing the initialization expression to reference the object that it will become a part of, circular data structures can be constructed, and more generally, the value of one field can be computed from the values of other fields of an object.
- Support production of high-quality, reliable software.
- To help in the construction of high-quality programs, programmers can add statically-checkable declarations and assertions to Cecil programs. One important kind of static declaration specifies the types of (i.e., the interfaces to) objects and methods. Cecil allows programmers to specify the types of method arguments, results, and local variables, and Cecil performs type checking statically when a statically-typed expression is assigned to a statically-typed variable or formal argument. The types specified by programmers describe the minimal interfaces required of legal objects, not their representations or implementations, to support maximum reuse of typed code. In Cecil, the subtype graph is distinguished from the code inheritance graph, since type checking has different goals and requirements than have code reuse and module extension [Snyder 86, Halbert & O'Brien 86, Cook et al. 90].
- To support the independent construction of subsystems, Cecil includes a module system. A module encapsulates its internal implementation details and presents an interface to external clients. This encapsulation mechanism is specially designed to work in the presence of multi-methods and inheritance/subtyping across module boundaries. Modules can be used to encapsulate "roles" [Andersen & Reenskaug 92] or "subjects" [Harrison & Ossher 93], programming idioms where pieces of the total interface of an object are split apart into application-specific facets. A given module can include method and field declarations that extend one or more previously-defined objects with additional specialized state and behavior.
- Cecil includes other kinds of static declarations. An object can be annotated as an abstract object (providing shared behavior but not manipulable by programs), as a template object (providing behavior suitable for direct instantiation but otherwise not manipulable by the program), or as a concrete object (fully manipulable and instantiated as is). Object annotations inform the type checker how the programmer intends to use objects, enabling the type checker to be more flexible for objects used in only a limited fashion.
- Cecil encourages a functional programming style by default, as this is likely to be easier to understand and more robust in the face of programming changes. By default, both local variables and fields are initialize-only; an explicit
var
keyword is required to assert that a variable or field can be mutated. An object can be created and its fields initialized to desired values in a single atomic operation; there are no partially-initialized states as are found during execution of a constructor in C++.
- Finally, Cecil omits certain complex language features that can have the effect of masking programming errors. For example, in Cecil, multiple dispatching and multiple inheritance are both unbiased with respect to argument order and parent order; any resulting ambiguities are reported back to the programmer as potential errors. This design decision is squarely at odds with the decision in CLOS and related languages. Additionally, subtyping in Cecil is explicit rather than implicit, so that the behavioral specification information implied by types can be incorporated into the decision about whether one type is a behavioral subtype of another.
- Support both exploratory programming and production programming, and enable smooth migration of parts of programs from one style to the other.
- Central to achieving this goal in Cecil is the ability to omit type declarations and other annotations in initial exploratory versions of a subsystem and incrementally add annotations as the subsystem matures to production quality. Cecil's type system is intended to be flexible and expressive, so that type declarations can be added to an existing dynamically-typed program and achieve static type correctness without major reorganization of the program. In particular, objects, types, and methods may be explicitly parameterized by types, method argument and result types may be declared as or parameterized by implicitly-bound type variables to achieve polymorphic function definitions, and (as mentioned above) the subtype graph can differ from the inheritance graph. The presence of multiple dispatching relieves some of the type system's burden, since multiple dispatching supports in a type-safe manner what would be considered unsafe covariant method redefinition in a single-dispatching language.
- Additionally, an environment for Cecil could infer on demand some parts of programs that otherwise must be explicitly declared, such as the list of supertypes of an object or the set of legal abstract methods of an object, so that one language can support both exploratory programmers (who use the inferencer) and production programmers (who explicitly specify what they want). This approach resolves some of the tension between language features in support of exploratory programming and features in support of production programming. In some cases, the language supports the more explicit production-oriented feature directly, with an environment expected to provide additional support for the exploratory-oriented feature.
- Avoid unnecessary redundancy in programs.
- To avoid requiring the programmer to repeat specifying the interface of an object or method, Cecil allows a single object declaration to define both an implementation and a type (an interface). Similarly, where the subtype hierarchy coincides with the code inheritance hierarchy, a single declaration will establish both relations. This approach greatly reduces the amount of code that otherwise would be required in a system that distinguished subtyping and code inheritance. Without this degree of conciseness, we believe separating subtyping from code inheritance would be impracticably verbose.
- Similarly, Cecil's classless object model is designed so that a single object declaration can define an entire data type. This contrasts with the situation in Self, where two objects are needed to define most data types [Ungar et al. 91]. Similarly, Cecil's object model supports both concise inheritance of representation and concise overriding of representation, unlike most class-based object-oriented languages which only support the former and most classless object-oriented languages which only conveniently support the latter.
- Finally, Cecil avoids requiring annotations for exploratory programming. Annotations such as type declarations and privacy declarations are simply omitted when programming in exploratory mode. If this were not the case, the language would likely be too verbose for rapid exploratory programming.
- Be "as simple as possible but no simpler."
- Cecil attempts to provide the smallest set of features that meet its design goals. For example, the object model is pure and classless, thus simplifying the language without sacrificing expressive power. However, some features are included in Cecil that make it more complex, such as supporting multiple dispatching or distinguishing between subtyping and implementation inheritance. Given no other alternative, our preference is for a more powerful language which is more complex over a simpler but less powerful language. Simplicity is important but should not override other language goals.
Cecil's design includes a number of other features that have proven their worth in other systems. These include multiple inheritance of both implementation and interface, closures to implement user-defined control structures and exceptions, and, of course, automatic storage reclamation.