A prototype-based approach to object reclassification

08/13/2018 ∙ by Ciaffaglione Alberto, et al. ∙ 0

We investigate, in the context of functional prototype-based lan- guages, a calculus of objects which might extend themselves upon receiving a message, a capability referred to by Cardelli as a self-inflicted operation. We present a sound type system for this calculus which guarantees that evaluating a well-typed expression will never yield a message-not-found runtime error. The resulting calculus is an attempt towards the definition of a language combining the safety advantage of static type checking with the flexibility normally found in dynamically typed languages.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Object calculi and languages can be divided in the two main categories of class-based and prototype-based (a.k.a. object-based) ones. The latter, whose best-known example is JavaScript, provide the programmer with a greater flexibility compared to those classed-based, e.g. the possibility of changing at runtime the behaviour of objects, by modifying or adding methods. Although such a flexibility is normally payed by the lack of static type systems, this is not necessarily the case, as it is possible to define a statically typed, prototype-based language. One example in this direction was the Lambda Calculus of Objects (), introduced by Fisher, Honsell, and Mitchell [FHM94] as a first solid foundation for the prototyped-based paradigm.

 is a lambda calculus extended with object primitives, where a new object may be created by modifying or extending an existing prototype. The new object thereby inherits properties from the original one in a controlled manner. Objects can be viewed as lists of pairs (method name, method body) where the method body is (or reduces to) a lambda abstraction whose first formal parameter is always the object itself (this in C and Java). The type assignment system of  is set up so as to prevent the unfortunate message-not-found runtime error. Types of methods are allowed to be specialized to the type of the inheriting objects. This feature is usually referred to as “mytype method specialization”. The high mutability of method bodies is accommodated in the type system via an implicit form of higher-order polymorphism, inspired by the the work of Wand on extensible records [Wan87].

The calculus  spurred an intense research in type assignment systems for object calculi. Several calculi inspired by , dealing with various extra features such as incomplete objects, subtyping, encapsulation, imperative features, have appeared soon afterwards (see e.g. [FM95, BL95, BBDL97, FM98, BF98]).

More specifically,  supports two operations which may change the shape of an object: method addition and method override. The operational semantics of the calculus allows method bodies in objects to modify their own self, a powerful capability referred to by Cardelli as a self-inflicted operation [Car95].

Consider the method belonging, among others, to a object with an field:

When is called to with argument “”, written as , the result is a new object where the field has been set (i.e. overridden) to . Notice the self-inflicted operation of object override (i.e. ) performed by the method.

However, in all the type systems for calculi of objects, both those derived from  and those derived from Abadi and Cardelli’s foundational Object Calculus [AC96], the type system prevents the possibility for a method to self-inflict an extension to the host object. We feel that this is an unpleasant limitation if the message-passing paradigm is to be taken in full generality. Moreover, in  this limitation appears arbitrary, given that the operational semantics supports without difficulty self-inflicted extension methods.

There are plenty of situations, both in programming and in real life, where it would be convenient to have objects which modify their interface upon an execution of a message. Consider for instance the following situations.

  • The process of learning could be easily modeled using an object which can react to the “teacher’s message” by extending its capability of performing, in the future, a new task in response to a new request from the environment (an old dog could appear to learn new tricks if in his youth it had been taught a “self-extension” trick).

  • The process of “vaccination” against the virus can be viewed as the act of extending the capability of the immune system of producing, in the future, a new kind of “-antibodies” upon receiving the message that an -infection is in progress. Similar processes arise in epigenetics.

  • In standard typed class-based languages the structure of a class can be modified only statically. If we need to add a new method to an instance of a class we are forced to recompile the class and to make the modification needlessly available to all the class instances, thereby wasting memory. If a class had a self-extension method, only the instances of the class which have dynamically executed this method would allocate new memory, without the need of any re-compilation. As a consequence, many sub-class declarations could be easily explained away if suitable self-extension methods in the parent class were available.

  • Downcasting could be smoothly implementable on objects with self-extension methods. For example, for a colored point extending the object above, the following expression could be made to type check (details in Section 5):

    where is intended to be a self-extension method of (adding a method) and is the name of the standard binary equality method.

  • Self-extension is strictly related to object evolution and object reclassification (see Sections 7 and 8), two features which are required in areas such as e.g. banking, GUI development, and games.

Actually, the possibility of modifying objects at runtime is already available in dynamically typed languages such as Smalltalk (via the become method), Python (by modifying the _class_ attribute), and Ruby. On the other hand, the self-extension itself is present, and used, in the prototype-based JavaScript language.

In such a scenario, the goal of this paper is to introduce the prototype-based , a lambda calculus of objects in the style of , together with a type assignment system which allows self-inflicted extension still catching statically the message-not-found runtime error. This system can be further extended to accommodate other subtyping features; by way of example we will present a “width-subtyping" relation that permits sound method override and a limited form of object extension. In fact, this manuscript completes and extends the paper [DGHL98].

We remark that the research presented in this article belongs to a series of similar investigations [Zha10, CHJ12, Zha12], whose aim is to define more and more powerful type assignment systems, capable to statically type check larger and larger fragments of a prototype-based, dynamically typed language like JavaScript. The ultimate goal is the definition of a language combining the safety advantage of static type checking with the flexibility normally found in dynamically typed languages.

Self-inflicted extension

To enable the  calculus to perform self-inflicted extensions, two modifications of the system in [FHM94] are necessary. The first is, in effect, a simplification of the original syntax of the language. The second is much more substantial and it involves the type discipline.

As far as the syntax of the language is concerned, we are forced to unify into a single operator, denoted by , the two original object operators of , i.e. object extension () and object override (). This is due to the fact that, when iterating the execution of a self-extension method, only the first time we have a genuine object extension, while from the second time on we have just a simple object override.

Example 1.1

Consider the method, that adds a field to the “point” object :

When is sent to with argument “”, i.e. , the result is a new object where the field has been added to and set to :

If is sent twice to , i.e. , then, since the field is already present in , it will be overridden with the new “” value:

Therefore, only the rightmost version of a method will be the effective one.

As far as types are concerned, we add two new kinds of object-types, namely , which can be seen as the type theoretical counterpart of the syntactic object , and , a generalization of the original in [FHM94], named -type. Intuitively, if the type is assigned to an object ( represents the type of self), can respond to all the methods . Mandatory, the list of pairs contains all the methods together with their corresponding types; moreover, may contain some reserved methods, i.e. methods that can be added to either by ordinary object-extension or by a method in which performs a self-inflicted extension (therefore, if did not contain reserved methods, would coincide with of [FHM94]).

To convey to the reader the intended meaning of -types, let us suppose that an object is assigned the type . In fact, is not typable, but as has the effect of adding the method to the interface of , thus updating the type of to , then is typable.

The list of reserved methods in a -type is crucial to enforce the soundness of the type assignment system. Consider e.g. an object containing two methods, , and , each of them self-inflicting the extension of a new method . The type assignment system has to carry enough information so as to enforce that the same type will be assigned to whatever self-inflicted extension has been executed.

The typing system that we will introduce ensures that we can always dynamically add new fresh methods for -types, thus leaving intact the original philosophy of rapid prototyping, peculiar to object calculi.

To model specialization of inherited methods, we use the notion of matching, a.k.a. type extension, originally introduced by Bruce [Bru94] and later applied to the Object Calculus [AC96] and to  [BB99]. At the price of a little more mathematical overhead, we could have used also the implicit higher-order polymorphism of [FHM94].

Object subsumption.

As it is well-known, see e.g. [AC96, FM94], the introduction of a subsumption relation over object-types makes the type system unsound. In particular, width-subtyping clashes with object extension, and depth-subtyping clashes with object override. In fact, on -types no subtyping is possible. In order to accommodate subtyping, we add another kind of object-type, i.e. , which behaves like except that it can be assigned to objects which can be extended only by making longer the list (by means of reserved methods that appear in ). On -types a (covariant) width-subtyping is permitted111The and terminology is the same as in Fisher and Mitchell [FM95, FM98]..

Synopsis.

The present paper is organized as follows. In Section 2 we introduce the calculus , its small-step operational semantics, and some intuitive examples to illustrate the idea of self-inflicted object extension. In Section 3 we define the type system for  and discuss in detail the intended meaning of the most interesting rules. In Section 4 we show how our type system is compatible with a width-subtyping relation. Section 5 presents a collection of typing examples. In Section 6 we state our soundness result, namely that every closed and well-typed expression will not produce wrong results. Section 7 is devoted to workout an example, to illustrate the potential of the self-inflicted extension mechanism as a runtime feature, in connection with object reclassification. In Section 8 we discuss related work. The complete set of type assignment rules appears in the Appendix, together with full proofs.

The present work extends and completes [DGHL98] in the following way: we have slightly changed the reduction semantics, substantially refined the type system, fully documented the proofs, and, in the last two novel sections, we have connected our approach with the related developments in the area.

2 The lambda calculus of objects

In this section, we present the Lambda Calculus of Objects . The terms are defined by the following abstract grammar:

where , , are meta-variables ranging over sets of constants, variables, and names of methods, respectively. As usual, terms that differ only in the names of bound variables are identified. Terms are untyped -terms enriched with objects: the intended meaning of the object-terms is the following: stands for the empty object; stands for extending/overriding the object with a method whose body is ; stands for the result of sending the message to the object .

The auxiliary operation searches the body of the method within the object . In the recursive search of , removes methods from ; for this reason we need to introduce the expression , which denotes a function that, applied to , reconstructs the original object with the complete list of its methods. This function is peculiar to the operational semantics and, in practice, could be made not available to the programmer.

To lighten up the notation, we write as syntactic sugar for , where . Also, we write in place of if ; this mainly concerns methods, whose first formal parameter is always their host object: e.g. and are usually written and , respectively.

2.1 Operational semantics

We define the semantics of  terms by means of the reduction rules displayed in Figure 1 (small-step semantics ); the evaluation relation is then taken to be the symmetric, reflexive, transitive and contextual closure of .

In addition to the standard -rule for -calculus, the main operation on objects is method invocation, whose reduction is defined by the rule. Sending a message to an object which contains a method reduces to , where the arguments of have the following intuitive meanings:

-arg.

is a sub-object of the receiver (or recipient) of the message;

-arg.

is the message we want to send to the receiver;

-arg.

is a function that transforms the first argument in the original receiver.

By looking at the last two rules, one may note that the function scans the receiver of the message until it finds the definition of the called method: when it finds such a method, it applies its body to the receiver of the message. Notice how the function carries over, in its search, all the informations necessary to reconstruct the original receiver of the message. The following reduction illustrates the evaluation mechanism:

That is, in order to call the first method of an object-term with two methods, , one needs to consider the subterm containing just the first method and construct a function, , transforming the subterm in the original term.

Figure 1: Reduction Semantics (Small-Step)
Proposition 2.1

The reduction is Church-Rosser.

A quite simple technique to prove the Church-Rosser property for the -calculus has been proposed by Takahashi [Tak95]. The technique is based on parallel reduction and on Takahashi translation. It works as follows: first one defines a parallel reduction on -terms, where several redexes can be reduced in parallel; then one shows that for any term there is a term , i.e. Takahashi’s translation, obtained from by reducing a maximum set of redexes in parallel. It follows almost immediately that the parallel reduction satisfies the triangular property, hence the diamond property, and therefore the calculus in confluent. With respect to the -calculus,  contains, besides the -rule, reduction rules for object terms; however, the latter do not interfere with the former, hence Takahashi’s technique can be applied to the  calculus.

A deterministic, call by name, evaluation strategy over terms may be defined on  by restricting the set of contexts used in the contextual closure of the reduction relation. In detail, we restrict the contextual closure to the set of contexts generated by the following grammar:

The set of values, i.e. the terms that are well-formed (and typable according to the type system we introduce in Section 3) and where no reduction is possible, is defined by the following grammar:

2.2 Examples

In the next examples we show three objects, performing, respectively:

  • a self-inflicted extension;

  • two (nested) self-inflicted extensions;

  • a self-inflicted extension “on the fly”.

Example 2.1

Consider the object , defined as follows:

If we send the message to , then we have the following computation:

i.e. the method has been added to . If we send the message twice to , i.e. , the method is only overridden with the same body; hence, we get an object which is “operationally equivalent” to the previous one.

Example 2.2

Consider the object , defined as follows:

If we send the message to , then we obtain:

i.e. the method has been added to . On the other hand, if we send first the message and then to , both the methods and are added:

Example 2.3

Consider the object , defined as follows:

If we send the message to , then we get the following computation:

i.e. the following steps are performed:

  1. the method calls the method with actual parameter the host object itself augmented with the method;

  2. the method takes as input the host object augmented with the method, and sends to this object the message , which simply returns the constant .

3 Type system

In this section, we introduce the syntax of types and we discuss the most interesting type rules. For the sake of simplicity, we prefer to first present the type system without the rules related with object subsumption (which will be discussed in Section 4). The complete set of rules can be found in Appendices A and B.

3.1 Types

The type expressions are described by the following grammar:

In the rest of the article we will use as meta-variable ranging over generic-types, over constant types, over object-types. Moreover, is a type variable, a metavariable ranging over rows, i.e. unordered sets of pairs (method label, method type), a method label, and a metavariable ranging over the unique kind of types .

To ease the notation, we write as or or else simply in the case the subscripts can be omitted. Similarly, we write either or for , and for . If , then we denote by , and we write if and .

As in [FHM94], we may consider object-types as a form of recursively-defined types. Object-types in the form are named -types, where is a binder for the type-variable representing “self” (we use -conversion of type-variables bound by ). The intended meaning of a -type is the following:

  • the methods in are the ones which are present in the -type;

  • the methods in , being in fact a subset of those in , are the methods that are available and can be invoked (it follows that the -type corresponds exactly to the object-type in [FHM94]);

  • the methods in that do not appear in are methods that cannot be invoked: they are just reserved.

In the end, we can say that the operator “” is used to make active and usable those methods that were previously just reserved in a -type; essentially, is the “type counterpart” of the operator on terms . In the following, it will turn out that we can extend an object with a new method having type only if it is possible to assign to an object-type of the form ; this reservation mechanism is crucial to guarantee the soundness of the type system.

3.2 Contexts and judgments

The contexts have the following form:

Our type assignment system uses judgments of the following shapes:

The intended meaning of the first three judgments is standard: well-formed contexts and types, and assignment of type to term . The intended meaning of is that is the type of a possible extension of an object having type . As in [Bru94], and in [BL95, BBL96, BBDL97, BB99], this judgment formalizes the notion of method-specialization (or protocol-extension), i.e. the capability to “inherit” the type of the methods of the prototype.

3.3 Well formed context and types

The type rules for well-formed contexts are quite standard. We just remark that in the rule:

we require that the object-types used to bind variables are not variable types themselves: this condition does not have any serious restriction, and has been set in the type system in order to make simpler the proofs of its properties.

The rule:

asserts that the object-type is well-formed if the object-type is well-formed and the type is well-formed under the hypothesis that is an object-type containing the methods in . Since may contain a subexpression of the form , with , we need to introduce in the context the hypothesis to prove that is a well-formed type.

The rule:

asserts that in order to activate the methods in the object-type , the methods need to be present (reserved) in .

3.4 Matching rules

The rule:

asserts that an object-type with more reserved and more available methods specializes an object-type with less reserved and less available methods.

The rule:

makes available the matching judgments present in the context. It asserts that, if a context contains the hypothesis that a type variable specializes a type , and itself, incremented with a set of methods , specializes a type , then, by transitivity of the matching relation, , incremented by the methods in , specializes .

The rule:

concerns object-types built from the same type variable, simply asserting that a type with more available methods specializes a type with less available methods.

3.5 Terms rules

The type rules for -terms are self-explanatory and hence they need no further justification. Concerning those for object terms, the rule assigns to an empty object an empty -type, while the rule:

asserts that an object having type can be considered also an object having type , i.e. with more reserved methods. This rule has to be used in conjunction with the one; it ensures that we can dynamically add fresh methods. Notice that cannot be applied when is a variable representing self; in fact, as explained in the Remark 3.1 below, the type of can only be a type variable. This fact is crucial for the soundness of the type system.

The rule:

can be applied in the following cases:

  1. when the object has type (or, by a previous application of the rule, ). In this case the object is extended with the (fresh) method ;

  2. when is a type variable . In this case can be the variable s, and a self-inflicted extension takes place.

The bound for is the same as the final type for the object ; this allows a recursive call of the method inside the expression , defining the method itself.

The rule:

is quite similar to the rule, but it is applied when the method is already available in the object , hence the body of is overridden with a new one.

Remark 3.1

By inspecting the and rules, one can see why the type of the object itself is always a type variable. In fact, the body of the new added method needs to have type . Therefore, if reduces to a value, this value has to be a -abstraction in the form . It follows that, in assigning a type to , we must use a context containing the hypothesis . Since no subsumption rule is available, the only type we can deduce for is .    

The rule:

is the standard rule that one can expect from a type system based on matching. We require that the method we are invoking is available in the recipient of the message.

In the rule:

the first two conditions ensure that the method is available in , while the last one that is a function that transforms an object into a more refined one.

4 Dealing with object subsumption

While the type assignment system , presented in Section 3, allows self-inflicted extension, it does not allow object subsumption. This is not surprising: in fact, we could (by subsumption) first hide a method in an object, and then add it again with a type incompatible with the previous one. The papers [AC96, FM94, FHM94, BL95] propose different type systems for prototype-based languages, where subsumption is permitted only in absence of object extension (and a fortiori self-inflicted extension). In this section, we devise a conservative extension of , that we name  (Appendix B collects its extra rules), to accommodate width-subtyping.

In the perspective of adding a subsumption rule to the typing system, we introduce another kind of object-types, i.e. , named -types. The main difference between the -types and the -types consists in the fact that the rule cannot be applied when an object has type ; it follows that the type permits extensions of an object only by enriching the list , i.e. by making active its reserved methods. This approach to subsumption is inspired by the one in [FM95, Liq97]. Formally, we need to extend the syntax of types by means of -types and the kind of rigid, i.e. non-extensible, types:

The subset of rigid types contains the -types and is closed under the arrow constructor. In order to axiomatize this, we introduce the judgment , whose rules are reported in Appendix 10. Intuitively, we can use the matching relation as a subtyping relation only when the type in the conclusion is rigid:

This is in fact is the rule performing object subsumption: it allows to use objects with an extended signature in any context expecting objects with a shorter one.

It is important to point out that, so doing, we do not need to introduce another partial order on types, i.e. an ordinary subtyping relation, to deal with subsumption. By introducing the sub-kind of rigid types, we make the matching relation compatible with subsumption, and hence we can make it play the role of the width-subtyping relation. This is in sharp contrast with the uses of matching proposed in the literature ([Bru94, BPF97, BB99]). Hence, in our type assignment system, the matching is a relation on types compatible with a limited subsumption rule.

Most of the rules for -types are a rephrasing of the rules presented so far, replacing the binder with . We remark that the rule

asserts that subsumption is unsound for methods having in contravariant position with respect to the arrow type constructor. Therefore, the variable is forced to occur only covariantly in . A natural (and sound) consequence is that we cannot forget binary methods via subtyping (see [BCC96, Cas95, Cas96]). The rule

promotes a fully-specializable -type into a limitedly specializable -type with less reserved and less available methods.

5 Examples

Let be and . Then:

where the first two premises are derived straightforwardly and as follows:

Figure 2: A derivation for

In this section, we give the types of the examples presented in Section 2.2, together with some other motivating examples. The objects , , and , of Examples 2.1, 2.2, and 2.3, respectively, can be given the following types:

A possible derivation for is presented in Figure 2.

Example 5.1

We show how class declaration can be simulated in  and how using the self-inflicted extension we can factorize in a single declaration the definition of a hierarchy of classes. Let the method be defined as in Example 1.1, and let us consider the simple class definition:

Then, the object can be used to create instances of both points and colored points, by using the expressions:

Example 5.2 (Subsumption 1)

We show how subsumption can interact with object extension. Let be:

and let and be of type and , respectively. Then, we can derive:

where the equality function has type . Notice that the terms:

would not be typable without the subsumption rule.

Example 5.3 (Subsumption 2)

We show how subsumption can interact with object self-inflicted extension. Let be:

By assuming and as in Example 5.2, we can derive:

Notice in particular that the object would not be typable without the subsumption rule.

Example 5.4 (Downcasting)

The self-inflicted extension permits to perform explicit downcasting simply by method calling. In fact, let and be objects with methods (checking the values of and the pairs , respectively), and the self-extension method presented in Example 5.1, typable as follows:

where . Then, the following judgments are derivable:

6 Soundness of the Type System

In this section, we prove the crucial property of our type system, i.e. the Subject Reduction theorem. It needs a preliminary series of technical lemmas presenting basic and technical properties, which are proved by inductive arguments. As a corollary, we shall derive the fundamental result of the paper, i.e. the Type Soundness of our typing discipline.

We first address the plain type assignment system without subsumption , then in Section 6.1 we extend the Subject Reduction to the whole type system . The proofs are fully documented in Appendices 11 and 12.

In the presentation of the formal results, we need as metavariables for generic-types and for object-types. Moreover, is a metavariable ranging on statements in the forms , , , , and on statements in the forms , .

Lemma 6.1

(Sub-derivation)

  • If is a derivation of , then there exists a sub-derivation of .

  • If is a derivation of , then there exists a sub-derivation of .

  • If is a derivation of , then there exists a sub-derivation of .

Lemma 6.2

(Weakening)

  • If and , then .

  • If and , then .

Lemma 6.3

(Well-formed object-types)

  • if and only if