We will argue that this simple message sending paradigm results in a lucid prototype-based programming language that is much safer than other prototype-based languages. It features a combination of controlled inheritance, controlled cloning and controlled reflective capabilities without neglecting the achievements of other prototype-based languages (such as dynamic object extension). It is the way these features are controlled by the language that makes Agora safe. We claim that this safety is an important prerequisite for prototype-based languages to become a viable alternative for the widely used class-based languages.
Agora features two kinds of messages expressions: ordinary messages and reifier messages. The evaluation strategy for ordinary messages is to send the message to the evaluated receiver with the evaluated arguments. This is much like function application in an applicative order functional language like Scheme. Reifier messages on the other hand can be seen as messages whose receiver and arguments are not necessarily evaluated. They thus correspond to the notion of reifier functions in 3-Lisp [Smith82] or special forms in Scheme. In the same way as special forms are `special functions' that are intercepted by the Scheme interpreter, reifier messages can be seen as `special messages' that are intercepted by the Agora interpreter. The Agora interpreter recognizes reifier messages by their boldfaced appearance.
An example of a reifier message is the variable: message, which should be sent to an identifier and whose argument can be any valid Agora expression. The currently chosen implementation of the variable: message will evaluate its argument and will declare its receiving identifier in the 'current' object. This idea is illustrated in the following code fragment that defines an ex-nihilo created object containing a single instance variable x with initial value 10.
If we would employ the evaluation strategy for ordinary messages here, a "variable x not declared" error would occur.
Like special forms make up the concrete syntax of Scheme, reifier messages define the particular flavour of Agora. The following section describes a subset of the currently implemented reifiers. An elaborate description can be found in the Agora user manual [DeMeuter96].
Like Self [Ungar&Smith87], Agora is a slot-based language, meaning that its variables are accessed through read and write accessor slots. Hence, a variable x defines a couple of method slots x and x: to read, respectively write the variable. As indicated above, variables are declared using the variable: reifier.
Inheritance is achieved by yet another kind of methods called mixin methods [Steyaert&al.93]. Upon invocation, mixin-methods extend the receiving object with the contents of their method body. When a mixin-method is declared functional, it returns a new object whose parent object is the receiver of the mixin-method. Functional mixin-methods can thus be used to create different views on objects. The usual inheritance rules govern the relation between the parent and each view. When a mixin-method is declared imperative, the receiving object and all its descendants will be destructively changed with the contents of the mixin-method. This can for example be used to turn an entire hierarchy of black and white graphical objects into a coloured hierarchy in one stroke.
Cloning is accomplished through cloning methods. These are methods whose body is executed in a shallow clone of the receiver. By definition, cloning methods are always functional, and their result is the clone of the receiver in which they were executed. The body of the cloning method can contain initialisation code.
Every method slot can be declared either public or local. Public slots are visible to everyone while local slots are only visible to the object itself.
All these features have been used in the bank account example below. While everyone can deposit money on an account, a client can only withdraw money from the account if she provides the correct user password. On request of a client, a clerk can create new bank accounts (by invoking a local cloning method). Unauthorised access by clients is prohibited by requiring a clerk password. Clerks can also extend existing accounts with phone banking functionality. This is achieved by invoking a local imperative mixin method.
[ account local variable: [ clerk local variable: "ClerkPwd"; user local variable: "UserPwd"; amount local variable: 5000; deposit:val public imperative method: [ amount:amount+val ]; withdraw:val userPwd:pwd public imperative method: [ (pwd=user) ifTrue: amount:amount-val ]; newAccount:initval userPwd:upwd clerkPwd:cpwd public functional method: [ makeClone local cloning: [ user: upwd; amount: initval ]; (cpwd=clerk) ifTrue: makeClone return ]; phoneBanking:cpwd numericCode:num public imperative method: [ makePhoneAccount local imperative mixin: [ code local variable: num; remoteWithdraw:val numericPwd:npwd public imperative method: [ (npwd=code) ifTrue: amount:amount-val ] ]; (cpwd=clerk) ifTrue: makePhoneAccount ] ]; account deposit:1000; "Change the account into a phone banking account with numeric code 2341" comment; account phoneBanking:"ClerkPwd" numericCode:"2341"; account remoteWithdraw:500 numericPwd:"2341"; "Clone the account with initial amount 10000 and password NewUser" comment; account2 variable: account newAccount:10000 userPwd:"NewUser" clerkPwd:"ClerkPwd"; account withdraw:2000 userPwd:"UserPwd" ]
Like object extension, cloning is accomplished by message sending, making it impossible to clone an object `from the outside'. The object itself decides how it can be cloned by implementing the necessary cloning methods. For this reason we speak of encapsulated cloning. Encapsulated cloning allows to avoid the so called prototype-corruption problem [Blaschek94], which arises when users of prototypes accidentally change the internal state of a prototype instead of a clone of the prototype. One possible solution is to preclude prototypes from being changed. This is exactly what happens in class-based languages, where a distinction is made between unchangeable entities (classes) and changeable entities (objects). The alternative solution to the prototype corruption problem is to control the way copies of prototypes are created, and this is precisely what cloning methods do.
As will be discussed below, both mixin-methods and cloning methods are consequences of the very secure Agora meta object protocol.
The implementation level representation of an Agora object is called its meta object. The set of messages understood by meta objects is called the meta object protocol (MOP). The Agora MOP consists solely of a send message. This reflects the fact that Agora objects only understand messages. The following code fragment shows what the MOP looks like in a C++-like language:
class Object { private: ... public: Object send(Message m, ListOfObjects args); };When writing meta programs (i.e. Agora code that explicitly deals with meta objects), the implementation level objects that become visible in Agora always satisfy the above protocol. Thus, it is impossible to bypass the message sending paradigm by `going meta'. The only meta operation applicable to objects is message sending[2].
The inheritance and cloning mechanisms of Agora are direct consequences of this simple MOP: every Agora object knows itself unencapsulated (the private information in the above code fragment) but can only be accessed by the `send' operation. Once a message arrives in an object, the object knows about its internal structures and can use these structures to deliver a clone or an extension of itself.
The very simple meta object protocol lying at the heart of the Agora philosophy only contains a message sending operation. This gives rise to very controlled extension, cloning and reflection mechanisms. The degree to which these operations are controlled by Agora determines the safety of Agora programs. A programmer is precluded from making accidental extensions, and although prototypes can be changed, the prototype corruption problem can easily be avoided using cloning methods. Furthermore, the safety barriers cannot be broken by using the reflective facilities of Agora. It is our strongest belief that this kind of safety is a necessary condition for prototype-based programming languages to become ready for the prime time.
An essential ingredient of safe programming languages is static typing. A static type system for dynamic extensions is being designed at our lab [Lucas&al.95], and remains to be fit into Agora. Another important field of future study is to find out how Agora (and prototype-based languages in general) handles the current shift in emphasis from `plain object-oriented programming' to more abstract software engineering notions like frameworks, design reuse and contracts. In this context we plan to incorporate the work of [Steyaert&al.96] in Agora. The design reuse formalism that is introduced by this work continues the "safety philosophy" that is akin to Agora.
[2]This is very similar to functional languages whose only meta operation is `apply'.
[Codenie&al.94] Codenie, W.; De Hondt, K.; D'Hondt, T. & Steyaert, P. - 1994. Agora: Message Passing as a Foundation for Exploring OO Language Concepts; SIGPLAN Notices, Volume 29, Number 12, December 1994, pp. 48-58, ACM Press.
[DeMeuter96] De Meuter, W. - 1996. Agora 96 Language Manual; Programming Technology Lab, Vrije Universiteit Brussel.
[Dony&al.92] Dony, C.; Malenfant, J. & Cointe, P. - 1992. Prototype-Based Languages: From a New Taxonomy to Constructive Proposals and Their Validation; OOPSLA `92 Proceedings, pp. 201-217, ACM Press.
[Lucas&al.95] Lucas, C.; Mens, K. & Steyaert, P. - 1995. Typing Dynamic Inheritance: A Trade-off between Substitutability and Extensibility; Technical Report vub-prog-tr-95-03, Vrije Universiteit Brussel.
[Mens&al.96] Mens, K.; De Volder, K. & Mens, T. - 1996. A Layered Calculus for Encapsulated Object Modification; Submitted to Foundations of Object-Oriented Languages Workshop 3.
[Smith82] Smith, B. C. - 1982. Procedural Reflection in Programming Languages; PhD thesis, MIT.
[Snyder87] Snyder, A. - 1987. Inheritance and the Development of Encapsulated Software Components; Research Directions in Object-Oriented Programming; pp. 165-188, MIT Press.
[Steyaert94] Steyaert, P. - 1994. Open Design of Object-Oriented Languages, A Foundation for Specialisable Reflective Language Frameworks; PhD thesis, Vrije Universiteit Brussel.
[Steyaert&al.93] Steyaert, P.; Codenie, W.; D'Hondt, T.; De Hondt, K.; Lucas, C. & Van Limberghen, M. - 1993. Nested Mixin-Methods in Agora; ECOOP `93 Proceedings, LNCS 707, pp. 197-219, Springer-Verlag.
[Steyaert&al.96] Steyaert, P.; Lucas, C.; Mens, K. & D'Hondt, T. - 1996. Reuse Contracts: Managing the Evolution of Reusable Assets; To appear in OOPSLA `96 Proceedings, ACM Press.
[Steyaert&DeMeuter95] Steyaert, P. & De Meuter, W. - 1995. A Marriage of Class- and Object-Based Inheritance Without Unwanted Children; ECOOP `95 Proceedings, LNCS 952, pp. 127-144, Springer-Verlag.
[Taivalsaari93] Taivalsaari, A. - 1993. A Critical View of Inheritance and Reusability in Object-oriented Programming; PhD thesis, University of Jyväskylä.
[Ungar&Smith87] Ungar, D. & Smith, R. B. - 1987. Self: The Power of Simplicity; OOPSLA `87 Proceedings, pp. 227-242, ACM Sigplan Notices, Vol. 22, No. 12, ACM Press.