找回密码
 立即注册

QQ登录

只需一步,快速开始

WantSong

高级会员

65

主题

78

帖子

1113

积分

高级会员

积分
1113

活字格认证

WantSong
高级会员   /  发表于:2009-12-14 10:15  /   查看:5779  /  回复:0
Posted by "Zesson ", 2007-04-13, 19:24
-----------------------------------------------------
  • just programming from another concept: base-on or deal with reflection to construct programming.

    original text: http://www.cs.indiana.edu/~jsobel/rop.html


    An Introduction to Reflection-Oriented Programming
    Jonathan M. Sobel Daniel P. Friedman [Research partially supported by National Science Foundation Grant #CCR--9302114.]
    Computer Science Department, Indiana University
    215 Lindley Hall, Bloomington, IN 47405
    {jsobel,dfried}@cs.indiana.edu Note: The printed, typeset version of this paper contains a significant amount of mathematical notation. In particular, all the "programs" in the paper are in a compact, equational language that we made up. In this online version, we have attempted to render as much as possible in HTML, but please consult a printed version if you doubt the accuaracy of anything. The programs in the online version are rendered in Dylan, extended with several macros.

    Abstract
    Most accounts of reflection are in an interpreted framework and tend to assume the availability of particular pieces of the state of a program's interpretation, including the current source code expression. This paper presents a computational account of reflection, drawing a distinction between the meta-level manipulation of data or control and the mere availability of meta-circular implementation details. In particular, this account does not presume the existence of program source code at runtime. The paper introduces a programming paradigm that relies on reflective language features and draws on the power of object-oriented programming. Several examples of the use of these features are provided, along with an explanation of a translation-based implementation. The examples include the measurement of computational expense, the introduction of first-class continuations, and the modification of the semantics of expressed values, all in the framework of reflection-oriented programming.

    Introduction
  • Intuitively, reflective computational systems allow computations to observe and modify properties of their own behavior, especially properties that are typically observed only from some external, meta-level viewpoint. The concept of reflection is best understood by reaching back to the study of self-awareness in artificial intelligence: ``Here I am walking down the street in the rain. Since I'm starting to get drenched, I should open my umbrella.'' This thought fragment reveals a self-awareness of behavior and state, one that leads to a change in that selfsame behavior and state. It would be desirable for computations to avail themselves of these reflective capabilities, examining themselves in order to make use of meta-level information in decisions about what to do next.
    In this paper, a realization of this fundamental understanding of reflection is presented as a means to create a new programming paradigm in which already-compiled programs can be extended naturally by the addition of composable, reflective components. We call this new paradigm reflection-oriented programming. This research departs from previous work by striving to distinguish between the notions of computation and program interpretation. It also borrows concepts from object-oriented systems, which can be seen as closely related to reflective systems in many ways. The main contributions of this paper are a concise framework for computational reflection, a series of examples of its use, and a translation-based implementation that leaves code open to further reflection even after it has been compiled.
    In the next section, we review several other avenues of research about computational reflection and how our work relates to them. In section [->], we explain our approach to reflection, followed by a series of examples that demonstrate the power and utility of reflection-oriented programming. In section [->], we present one possible implementation strategy (the one we have used) for this system. In the final section, we discuss several potential research directions in reflection-oriented programming.

    Background and Related Research
  • An obvious step toward providing the background for a detailed explanation of reflection-oriented programming would be to give a precise, formal definition of computational reflection. Such a definition is elusive, however, for a variety of reasons. One is that we still lack an entirely satisfactory or theoretically useful definition of computation. [Models like Turing machines and lambda-calculi define computability, but they do not provide the sort of formal mathematical understanding of computation that we have for other concepts in programming languages, such as the meaning of a program or the type of a term.] Furthermore, the circularity inherent to the notion of reflection makes it hard to express a concrete, well-grounded definition.
    Thus, descriptions of reflection in programming languages have avoided the hazy notion of computation and relied instead on the concrete concept of program. Instead of addressing computation about computation, they have been restricted to programs about programs. In such a framework, reflection is just a property of programs that happen to be about themselves. In this paper, the process of program execution serves as an adequate model for computation; a more complete treatment of the topic of computation is beyond the scope of this research.

    Introspective Interpreters
  • Much of the research on reflection focuses on introspective interpreters. These are precisely the ``programs about themselves'' referred to above. A meta-circular interpreter is written in the language it interprets, with a few special forms added to the language to allow a program to access and modify the internal state of the interpreter running that program [cite jefferson-friedman:96]. Specifically, the process of converting some component of the interpreter's state into a value that may be manipulated by the program is called reification; the process of converting a programmatically expressed value into a component of the interpreter's state is called reflection. [It can be confusing that the term reflection refers both to a specific activity and to a broad subject. When reflection is discussed in relation to reification, the more specific meaning is the one we intend.] By reflecting code ``up'' to the interpreter, a program can cause that code to execute at the meta-level.
    Three notable examples of this genre of introspective computation are 3-Lisp [cite smith:82, smith:84], Brown [cite friedman-wand:84, wand-friedman:88], and Blond [cite danvy-malmkjaer:88]. In 3-Lisp, Smith introduces the idea of a reflective tower, in which an infinite number of meta-circular interpreters run, each executing the interpreter immediately ``below'' it. Since a program may reflect any code up to the next higher level, including code that accesses the next level, it is possible for a program to run code at arbitrarily high meta-levels. The other introspective interpreters essentially do the same thing, with variations on what interpreter state is available and what effect reflective code can have on ensuing computation. In each case, the whole concept of reflection is knotted very tightly to interpretation---so tightly, in fact, that it is commonplace to assume the availability of original source code during the execution of reflective programs. (Debugging aids, such as the ability to trace the execution of the source program, are often cited as examples of what can be done with introspective interpreters.) There have been some attempts to escape from the world of the interpreter [cite bawden:88] and to formalize the sort of reflection performed by these introspective interpreters [cite jefferson-friedman:96], but none of them overcome the extremely operational, implementation-dependent nature of this approach to reflection.
    By focusing on processes, rather than programs, we can free ourselves from the introspective meta-circular interpreter. Our implementation of reflection-oriented programming does not presume an interpreted language or the existence of source code during program execution; it allows pre-compiled code to be extended with reflective behaviors.

    Metaobject Protocols
  • One alternative to introspective interpreters has already grown out of object-oriented programming. In an object-oriented system, a program specifies the behavior of the objects of a class by specializing a set of methods with respect to that class (or its superclasses). The program invokes methods on objects without ``knowing'' precisely from which class they come, and the system chooses the most appropriate methods to run. A metaobject protocol [cite kiczales-al:91, maes:87] extends an object-oriented system by giving every object a corresponding metaobject. Each metaobject is an instance of a metaclass. The methods specialized with respect to some metaclass specify the meta-level behavior of the object-metaobject pair, where meta-level behaviors include, for example, the workings of inheritance, instantiation, and method invocation. (Most real implementations do not really create a metaobject for every object; instead, they use a metaobject protocol for some subset of the objects in the system, such as the classes or functions.)
    Metaobject protocols provide a powerful, flexible means of modifying the behavior of a language. Using such a tool, one might add multiple inheritance to a language that begins only with single inheritance, or even allow for the coexistence of several different kinds of inheritance and method dispatch. Some metaobject protocols even let a program have control over the way new objects are represented by the system.
    By nature, a metaobject protocol focuses on the objects in a system. This correspondence between objects and metaobjects has caused many of the languages that use metaobject protocols to limit themselves to reflection about values. Since the role of most programs is to manipulate data (i.e., values, objects), this added power is quite useful, but a traditional metaobject protocol does not let a program say anything directly about control flow or other meta-level concepts that do not correspond directly to objects in the problem domain. In reflection-oriented programming, on the other hand, the correspondence is between reflective objects and computations. In fact, it is possible to understand reflection-oriented programming as a kind of ``meta-computation protocol,'' which makes it a much more plausible framework for reflecting about computational properties.

    Monads
  • Monads are category-theoretic constructs that first received the attention of the programming language community when Moggi [cite moggi:89, moggi:91] used them to represent effect-laden computation in denotational semantics. Since then, they have seen growing acceptance as a practical tool, especially as a means of handling I/O and other side-effects in purely functional languages. Each monad is defined as a triple consisting of a type constructor and two operators. If the type constructor is T, the two operators (unit and extension) allow expressions over type alpha to be transformed into expressions over the type Talpha. Values of type Talpha are usually taken to represent computations over values of type alpha. Thus, the use of monads allows a program to manipulate computations as values, which is a kind of reflection. Monadic reflection has been explored on its own [cite filinski:94, moggi:91], but its relation to other forms of reflection is still largely unaddressed [cite mendhekar-friedman:96].
    The study of monads strongly influences the concept of computation used here---especially the dissection of computations---and yet it is possible to define and use reflection-oriented programming without any reference to monads. On the other hand, the implementation presented here, which is not actually monadic, would look very familiar to anyone acquainted with monads. The deeper connections, if any, between this work and monads has yet to be explored. One exciting possibility is that reflection-oriented programming, which allows multiple reflective properties to be defined independently, could be formalized to address the issue of monad composition. (Currently, the only systems for combining monads are very narrow in scope and either non-extensible or can only be extended with much work and theoretical awareness on the part of the user [cite espinosa:95, liang-hudak-jones:95].)

    Reflection-Oriented Programming
  • In reflection-oriented programming, we understand computation as process, rather than program. It is all right to think of a process as the execution of a program, as long as care is taken not to focus on some particular application of an interpreter to a source program. Computation is a program happening.
    What does reflection about computations add, and what is lost, in comparison with other approaches? For one thing, many other reflective systems, especially introspective interpreters (but also some metaobject protocols), have enabled a program to refer to its own variables. But the concept of a program variable does not necessarily make any sense in the context of pure processes, once the original source code has been thrown away. One of the arguments to a reifying procedure in Brown, for example, is the program text that the reifying procedure was applied to. The purely computational view of reflection disallows such examinations of code. The text of a program is merely a means to a process. On the other hand, in purely computational reflection we gain the ability to refer to such concepts as computational expense, flow of meta-information, and the meaning of expressed values.

    Reflection-Oriented Programming as a Paradigm
  • To better understand reflection-oriented programming as a paradigm, consider some programming task. Suppose that in addition to being able to write and edit a program to complete this task, it is also possible to examine and edit the source code of an interpreter for the programming language being used. In the course of programming, there might be several situations in which there exists a choice: make extra effort and go to great lengths to solve a problem in the user code, or modify and extend the interpreter. (This dilemma is also mentioned by Maes [cite maes:87].)
    The most familiar example of this situation might be the need for first-class continuations. When such a need arises, one can either convert one's entire program to continuation-passing style [cite plotkin:75], or one can convert the interpreter to continuation-passing style and provide some means for the program both to reify the current continuation and to replace the current continuation with another one.
    Measuring computational expense is another simple example. If a programmer needs some measure of the number of steps a program takes to complete a task, there are again two main options. The program can be written so that it updates a counter every time it takes a step (by threading an extra argument through the entire program---again necessitating continuation-passing style---or by modifying some global variable). Or the programmer can make similar modifications to the interpreter, along with provisions for user programs to examine and replace the current value of the interpreter's ``uptime'' counter.
    In cases like these, it is probably simpler just to rewrite the program if the program is very small or very simple (or if the right automatic translation tools are available). If the program is at all long or complex, though, it could be much more convenient to modify the interpreter and provide an interface to the new features. As a starting point, let us associate the reflection-oriented paradigm with choosing to modify the interpreter and providing an appropriate interface to those modifications. The name reflection-oriented programming is justified on the grounds that, in this style of programming, the computation directly depends upon information being created and maintained at a meta-level, and on the grounds that the computation can access and affect that information.
    Of course, in many practical settings, the programmer has neither access to the source code for an interpreter nor desire to use an interpreted language. Thus, we extend the reflection-oriented programming paradigm to be programming as if the interpreter had been modified. More precisely, we define reflection-oriented programming to be a programming style that uses any means available to extend the meta-level semantics of computation in order to avoid situations in which the local requirements of some program fragment lead to non-local rewriting of the program.

    Reflection-Oriented Concepts
  • Since our approach to reflection-oriented programming is based on the concepts of object-oriented programming, we start by reviewing the way an object-oriented program works. In object-oriented programming, one writes a program so that its ``shape'' is not yet completely fixed. Instead, its shape is determined by the shape of the data with which it comes into contact. We can imagine throwing an object-oriented program at some data and watching the execution of the program wrap around that data, conforming to the contours of what it hits. Likewise, we wish to have a system in which a program can wrap its execution around the shape of the computation that it hits. Then we want to throw it at itself. To give computations some shape, we divide them into two categories:
    • atomic -- The computation is completed in a single logical step.
    • compound -- The computation requires the completion of subcomputations.
    Some examples of atomic computations are numbers, references to variables, Boolean values, and the performing of primitive operations (after the computations corresponding to the arguments of those operations have already been completed). Some examples of compound computations are function applications, conditionals, and sequencing operations. In any compound computation, it is possible to identify some initial subcomputation. When the semantics of the source language provides a specification for ordering subcomputations, the initial subcomputation may be uniquely determined. Otherwise, it may be chosen arbitrarily from among the several subparts of the compound computation. We call the non-initial subcomputations the ``rest'' of the compound computation. If there is more than one non-initial subcomputation, then the rest of the subcomputations are, as a group, another compound computation. Now computations have shape; how can reflective programs be written to wrap around that shape? We introduce the concept of meta-information. Meta-information consists of knowledge available---and maintained---at a meta-level. For example, if we are interested in knowing about the effort expended by a program as it runs, we would think of the current number of steps completed in a given dynamic context as meta-information. Our view of the relationship between meta-information and computations is shown in the following diagram:
    where the circle is some computation, and the numbered boxes represent the states of some sort of meta-information. The numbers themselves are merely labels. Given some prior state of meta-information, the computation occurs, and then we are left with some new state of meta-information. In our computational expense example, the first box would represent the number of steps executed prior to this particular computation (e.g., 237), and the second box would represent the number of steps executed altogether (e.g., 1090), including those steps within the computation. In our system, we use a reflective to specify the behavior of some type of meta-information. A reflective, which is much like a class, describes the structure of its instances. The instances of reflectives, or reflective objects, are the carriers of meta-information. Thus, from a meta-level viewpoint, computations act as transformers of reflective objects. The current reflective object for a given reflective represents the meta-information as it exists at that instant. Like classes, reflectives can inherit structure and methods from other reflectives. When a computation is provided with meta-information---in the form of an instance of some reflective---the exact transformative effect on the meta-information is determined by methods specialized to that reflective or its ancestors.
    So far, we have found that reflectives can be placed naturally into one of two groups: simple reflectives and control reflectives. Reflective concepts such as environments, stores, and computational expense can be modeled by simple reflectives. Reflection about the meanings of expressed values can be carried out by a special subset of simple reflectives called value reflectives. Control reflectives can model concepts pertaining to flow and control.

    Notation
  • In the following sections, we presume an object-oriented language with generic functions. <reflective> is the convention for writing names of reflectives. Other types are simply capitalized, like Integer. Each parameter in a function definition can be followed by an optional specializer, which is a ``::'' followed by a reflective or some other type.
    define reflective <new> (<old>)
      slot field;
      ...
    end;
    defines a new reflective <new> that extends <old> with new fields field, .... Even if there are no new fields in the subreflective, we still write define reflective <new> (<old>) end;
    to make it clear that the result is a subtype of <reflective>. If r is an instance of a reflective, and f is the name of a field in r, then update(r, f: e)
    is an instance r' such that r'.f = e and r'.x = r.x for every other field x. reify(<reflective>) evaluates to the current instance of <reflective>. Evaluating reflect(<reflective>, r, e) causes the current instance of <reflective> to be r before evaluating and returning e. When <reflective> appears as text in an example, and not just as a meta-variable, it is intended to act as an empty base reflective type with no particular functionality. Application is left-associative, as usual.
    Simple Reflectives
  • The behavior of a simple reflective's instances is governed by three methods. The first method, atomic, specifies what sort of transition the instances should make in atomic computations. The second method, compound-initial, specifies what sort of transition should take place upon entering the initial part of a compound computation. Finally, compound-rest specifies what meta-information gets passed on to the rest of a compound computation, given the reflective object produced by the initial subcomputation and the reflective object representing the state of meta-information prior to the whole compound computation. The following figures, which are in the form of proof rules, demonstrate how these methods fit together with the whole process of computation:
    The filled circle in this rule represents an atomic computation. We can read the rule as, ``Given that an atomic computation produces v as its output, and given that the atomic method produces meta-information in state 2 when supplied with v and meta-information in state 1, we can conclude that the atomic computation causes a transition from state 1 to state 2 in the meta-information.'' For instance, if we define atomic to add one ``tick'' to the step counter of its argument when applied to instances of our computational expense reflective, then we can say that atomic computations take one computational step.
    In the second rule, the oval with other computations inside represents a compound computation. The circle is the initial subcomputation, and the small oval is the rest of the compound computation. Each of these subparts may be atomic or compound. Continuing our example, we might make compound-initial add one tick to the step counter of its argument. The initial subcomputation starts with the meta-information produced by compound-initial and transforms it to a new state, possibly going through many intermediate states along the way, if the subcomputation is itself compound. Thus, the box labeled 3 represents the total number of steps taken by the initial subcomputation plus the one tick added by compound-initial plus however many steps had taken place prior to the whole compound computation. Both this sum and the original meta-information (labeled 1) are passed to compound-rest. In this example, we want compound-rest to return its second argument. Finally, the rest of this compound computation transforms the output of compound-rest into the ultimate result for the computation (labeled 5). The total number of steps for the compound computation is one more than the sum of its subcomputations. (If we wanted to model a system in which returning from subcomputations---popping the stack---represents an expense, we could make compound-rest add another tick to its second argument.)
    The diagrams also make clear the separation between the meta-level behavior of information and base-level program behavior. The definitions of the three methods can be linked in after the program has already been compiled. Once the right ``hooks'' have been compiled into a program, it is possible to add new reflective behaviors at will, without modifying the original code. To make this idea more concrete, consider that the text of a program whose computational expense is to be measured is exactly the same as one in which no extra reflective information is being maintained. Furthermore, taking advantage of method selection, it is possible to define many different kinds of reflectives to be active at once.
    To make this whole system truly reflective, we add operations reify and reflect to the language to allow meta-information to be reified or reflected on demand. For example, it would be possible for a debugger to track the expense of the program it runs, excluding the cost of the debugger itself. The debugger could reify the current expense-measuring reflective object upon entry into its own part of the computation. It could reflect the saved expense information back into the meta-level just before handing control back over to the user's program.
    It is convenient to have default behaviors for atomic, compound-initial, and compound-rest that simply ``thread'' meta-information through the computations (like a store [cite schmidt:85]). Thus, we build the following definitions into our system:

    define reflective <simple-reflective> (<reflective>) end;

    define method atomic (v, outer :: <simple-reflective>)
      outer
    end;

    define method compound-initial (outer :: <simple-reflective>)
      outer
    end;

    define method compound-rest (outer :: <simple-reflective>,
                                 inner :: <simple-reflective>)
      inner
    end;
    Example: Measuring Computational Expense
  • We have already discussed the measurement of computational expense in some detail in the preceding section. To implement such behavior, we need only these definitions:
    define reflective <runtime> (<simple-reflective>)
      slot ticks, init-value: 0;
    end;

    define method atomic (v, outer :: <runtime>)
      update(outer, ticks: add1(outer.ticks))
    end;

    define method compound-initial (outer :: <runtime>)
      update(outer, ticks: add1(outer.ticks))
    end;
    The inherited definition of compound-rest suffices. Now suppose we want to evaluate some expression E without counting its cost. We could use the reify and reflect operations like this: let r = reify(<runtime>);
    let result = E;
    reflect(<runtime>, r, result);
    Here we capture the expense meta-information before evaluating E and save it as r. After evaluating E, we restore the old meta-information. The result from evaluating E is returned. Of course, any time we want to know the cumulative expense for the program, we can examine reify(<runtime>).ticks.
    Example: Using Environments in an Interpreter
  • The use of reflectives makes it possible to write a simple interpreter for an untyped call-by-value lambda-calculus without passing an environment argument down in recursive calls to the interpreter. First we define an environment reflective.
    define reflective <env> (<simple-reflective>)
      slot bindings, init-value: #();
    end;

    define method compound-rest (outer :: <env>, inner :: <env>)
      outer
    end;
    The only way that an environment acts differently from the default store-like behavior of simple reflectives is that the rest of a compound computation is executed using the bindings that were in effect outside the compound computation (i.e., prior to the initial part of the compound computation). Now suppose we have parsed lambda-calculus terms into objects of types Varref, Lambda, and App. We can write an interpreter for these terms as follows:
    define method interp (exp :: Varref)
      let e = reify(<env>);
      lookup(exp.name, e.bindings);
    end;

    define method interp (exp :: Lambda)
      let e1 = reify(<env>);
      let b = e1.bindings;
      method (a)
        let e2 = reify(<env>);
        reflect(<env>,
                update(e2, bindings: extend(b, exp.formal, a)),
                interp(exp.body))
      end;
    end;

    define method interp (exp :: App)
      (interp(exp.rator))(interp(exp.rand))
    end;
    We must use reify a second time inside the Lambda clause because some other reflective may have been derived from <env>; that is, the current instance of <env> may actually have more fields than just bindings, by virtue of inheritance. We must be careful not to throw away additional meta-information when we reflect a new instance back into the meta-level. (This is a typical issue in systems that support subtype polymorphism; it is not unique to reflection-oriented programming.) The environment and expense reflectives can coexist, so that it is possible to measure the expense of evaluating lambda-terms using this interpreter. More generally, many reflectives can exist simultaneously, so that an arbitrary amount of meta-information can be maintained by the system and accessed by programs.

    Value Reflectives
  • It is useful to build in a special subreflective of <simple-reflective> for modeling the concept of expressed values, or values produced as the result of computations. By inheriting from <value-reflective>, it is possible to modify or extend the semantics of expressed values. By default, standard concrete values are returned from computations and passed on to continuations, but the creation of a new value reflective allows for the introduction of abstract values. For example, instead of producing the number 3, some computation may produce the type Integer as its value. Value reflectives are probably the most complex to manage, because changing the semantics of values necessarily has wide, systemic ramifications. Just as in any abstract interpreter [cite cousot-cousot:77], some or all of the language's primitive operations often need to be redefined. This, too, is possible with value reflectives. (For example, addition might have to be redefined so that Integer + Integer produces the value Integer, where Integer is a value, not a meta-level description of some number.)
    An abridged version of the definitions of <value-reflective> and its methods appears below. In reality, the method definitions must be more complicated in implementation-specific ways in order to deal correctly with language primitives, etc., depending on how deeply reflection-oriented programming is embedded into the language.
    define reflective <value-reflective> (<simple-reflective>)
      slot base-value, init-value: #f;
    end;

    define method atomic (v, outer :: <value-reflective>)
      update(outer, base-value: v)
    end;
    As one would expect, the value coming from the outside is ignored in the default definition. The one field in the instances of this reflective acts as a ``carrier'' for what would be the standard (meta-level) expressed values in a meta-circular interpreter.
    Example: Bounding the Possible Values of Integers
  • A very simple example of the power of value reflectives is the restriction of the domain of integers in a computation to be between -1000 and 1000.
    define constant bound-to-1000
      = method (i)
          min(1000, max(-1000, i))
        end;

    define reflective <bounded-int> (<value-reflective>) end;

    define method atomic (v :: Integer, outer :: <bounded-int>)
      update(outer, base-value: bound-to-1000(v));
    end;

    define method compound-rest (outer :: <bounded-int>,
                                 inner :: <bounded-int>)
      let t = inner.base-value;
      if (instance?(t, Integer))
        update(inner, base-value: bound-to-1000(t))
      else
        inner
      end
    end;
    No new fields are added in the definition of <bounded-int>. This reflective merely modifies the behavior of integer values by overriding the default methods. (The new definition of atomic in our example applies only to integers; the inherited atomic still works for other types.) With these definitions in place, the computation corresponding to 600 + 600 would produce 1000 as its value. Furthermore, in complex calculations such as
    10^5 + 10^6 - 4572
    all the intermediate values are subject to the bounding. What happens if a program defines several disjoint subreflectives of <value-reflective>? For example, what if we define one value reflective to restrict the absolute values of integers to be less than 1000 and another (not as a subreflective of the first) to restrict integer values to be positive? One reasonable answer might be that such orthogonal definitions are simply disallowed. Instead, we have chosen to allow the existence of several independent value reflectives. At each step of the computation, the values in the various base-value fields are collected and passed, in turn, to the continuation. Of course, in the worst case, this can lead to an exponential increase in the number of steps a program takes! (An implementation can make some attempt to control this exponential explosion by recognizing duplicate values and passing only one of them to the continuation.)

    Control Reflectives
  • Unlike value reflectives, which are really just simple reflectives that get special treatment, control reflectives are a completely different species. The methods associated with simple reflectives specify only how meta-information should flow through computations on a per-reflective basis. The methods associated with control reflectives, on the other hand, specify both how control itself should be managed in computations and how meta-information about control should be propagated. There are only two methods used to define the behavior of a control reflective: atomic-control and compound-control. Each atomic computation is encapsulated as a function that executes that computation when it is applied to an appropriate instance of a control reflective. Likewise, each compound computation is encapsulated as two functions, one for the initial computation and one for the rest of the subcomputations. Since it is possible for control to split into more than one thread during a computation---in the presence of multiple value reflectives, for example---each of these functions returns a sequence of control reflective objects, as can be seen in the default definition of compound-control below.
    define method atomic-control (outer :: <control-reflective>,
                                  atom :: Function)
      atom(outer)
    end;

    define method compound-control (outer :: <control-reflective>,
                                    initial :: Function,
                                    rest :: Function)
      map/concat(rest, initial(outer))
    end;
    The default definition of atomic-control applies the function representing the atomic computation to the current instance of <control-reflective>. The default definition of compound-control passes its reflective object to the function representing the initial subcomputation and then maps the function representing the rest of the subcomputations over the sequence of objects returned by the initial function. The resulting sequences are concatenated to form the final result.
    Example: Implementing First-Class Continuations
  • An immediate use for control reflectives is the introduction of first-class continuations. First, we define a control reflective <cont> to model continuations; it has one field, used to hold the current continuation at any stage of the computation.
    define reflective <cont> (<control-reflective>)
      slot cc, init-value: list(list);
    end;

    define method atomic-control (outer :: <cont>, atom :: Function)
      map/concat(method (x)
                   head(x.cc)(update(x, cc: tail(x.cc)))
                 end,
                 atom(outer))
    end;

    define method compound-control (outer :: <cont>,
                                    initial :: Function,
                                    rest :: Function)
      initial(update(outer, cc: pair(rest, outer.cc)))
    end;
    The compound-control method passes to initial an instance of <cont> whose current continuation field has been supplemented with rest. In this way, the control flow ``walks'' down the leftmost (initial-most) branches of the computation tree until reaching the first atomic computation. At that point, the atomic computation is run, and the various resulting instances of <cont> are passed on to the continuation. Before the first ``piece'' of the continuation is run, the current continuation field is reset not to include that piece. Given these definitions, Scheme's call-with-current-continuation (or call/cc) can be defined as an ordinary function, using reify and reflect. We use reify to capture the continuation in the context where call/cc is invoked. Then, inside the function passed to the receiver function, we use reflect to replace the current continuation with the saved one.
    define constant call/cc
      = method (f)
          let x = reify(<cont>);
          if (instance?(x, List))
            head(x)
          else
            f(method (v)
                let new = reify(<cont>);
                reflect(<cont>,
                        update(new, cc: x.cc),
                        list(v))
              end)
          end if
        end method;
    Some cleverness is required to return the value of v to the right context; we use list(v) to guarantee that the type of x is different from <cont> the second time x is bound. It might seem that we could simply replace the entire reflective object new with the saved one (i.e., old), rather than just updating it with the old continuation, but that would throw away any additional information that the current instance of <cont> might be carrying. Remember that the object in question might actually be an indirect instance of <cont>, i.e., a direct instance of some subreflective of <cont>.
    An Implementation
  • Our implementation is translation-based. In essence, the translation lays out a program so that it syntactically resembles the computation/subcomputation model used by the reflectives and their methods. The next section describes the main portion of the translation; section [->] discusses the representation and propagation of meta-information; and section [->] extends the translation to support the definition of new reflectives, as well as reify and reflect.

    Program Translation
  • The goal of the translation is to recognize and demarcate each unit of syntax that corresponds semantically to a computation. After being translated, a program should have runtime ``hooks'' into each atomic and compound computation. Furthermore, in each syntactic construct that evaluates as a compound computation, the subexpression that evaluates as the initial subcomputation should be clearly set apart. Given these requirements, it is not surprising that the resulting translation bears a strong resemblance to continuation-passing style [cite plotkin:75], A-normal form [cite flanagan-al:93], and monadic style [cite moggi:89, wadler:92]. (See section [->].)
    The translation R given below places references to two functions in its output: alpha is applied to the program fragments that correspond to atomic computations, and gamma is applied to the program fragments that correspond to compound computations. The first argument to gamma corresponds to the initial subcomputation, and the second argument corresponds to the rest of the subcomputations, packaged as a function that expects to be applied to the base-value generated by the initial subcomputation. To make the rest of the translation more readable, we begin by defining a helpful piece of ``syntactic sugar'':
        bind v = E_v in E
        ==> gamma(E_v, method (v) E end)
    We use bind instead of gamma in the rest of the translation for two reasons: it is cumbersome to read the nested applications and lambda-expressions that would otherwise appear, and the new syntax makes it clearer which subexpression corresponds to the initial subcomputation. R[c]
        ===> alpha(c)

    R[x]
        ===> alpha(x)

    R[method (x) E end]
        ===> alpha(method (x) R[E] end)

    R[let x = E1;
      E2]
        ===> R[(method (x) E2 end)(E1)]

    R[if (E1)
        E2
      else
        E3
      end]
        ===> bind t = R[E1]
             in if (t)
                  R[E2]
                else
                  R[E3]
                end

    R[E0(E1, ..., Ek)]
        ===> bind e0 = R[E0]
             in bind e1 = R[E1]
                in
                   ...
                       bind ek = R[Ek]
                       in e0(e1, ..., ek)

    R[update(E0, f: E1)]
        ===> bind e0 = R[E0]
             in bind e1 = R[E1]
                in alpha(update(e0, f: e1))

    R[E.f]
        ===> bind e = R[E]
             in alpha(e.f)
    The translations for reify and reflect are absent because they depend on knowledge of the representation of meta-information to be able to shift reflective objects between the meta-level and the value domain.
    Representation
  • Our goal now is to implement alpha and gamma based on some particular representation of meta-information. Let us begin by restricting our interest to simple reflectives and returning to the idea of computations as transformers of meta-information. The translation above treats atomic computations as values, evidenced by the fact that the translation does not go ``inside'' those expressions that correspond to atomic computations. Suppose, for the moment, that there is only one reflective defined. To ensure that some sort of values are available to be returned by computations, let that one reflective be <value-reflective>.
    We know that alpha(v)---where v is a value produced by an atomic computation---should be able to accept a reflective object (an instance of <value-reflective>) and transform it according to the definition of atomic. Thus, we conclude that the type T of alpha(v) is
    T = S -->S
    where S is the type of simple reflectives. The type of gamma is induced by the type of alpha, since gamma is only applied to translated code. In fact, the translation guarantees that
    alpha : v --> T
    gamma : T --> (v --> T) --> T
    always holds as we vary T, where v is the type of values. Roughly following the diagrams in section [<-], we define alpha and gamma as follows: define method alpha (v)
      method (s)
        atomic(v, s)
      end
    end;

    define method gamma (i, r)
      method (s)
        let s2 = i(compound-initial(s));
        let s3 = compound-rest(s, s2);
        r(s3.base-value)(s3)
      end
    end;
    These definitions only support a single simple (value) reflective, though. In order to handle many simple reflectives simultaneously, we must complicate the definitions a little. Still restricting our world so that there are only simple reflectives and no sibling value reflectives (where one is not a subtype of the other), suppose we have a function find-value-obj that picks the value reflective object out of a set of simple reflective objects. Then we can support a system in which
    T = 2^S --> 2^S
    by extending alpha and gamma as follows: define method alpha (v)
      method (l)
        map(method (s) atomic(v, s) end,
            l)
      end
    end;

    define method gamma (i, r)
      method (l)
        let l2 = i(map(compound-initial, l));
        let l3 = map(compound-rest,l, l2);
        r(find-value-obj(l3).base-value)(l3)
      end
    end;
    We still have not dealt with control reflectives in any of these definitions. In the end, we would like to support multiple threads of control, each with its own idea of what the current meta-information is. To accomplish this objective, we define control reflectives so that each instance carries around a complete set of simple reflective objects (possibly including several value reflective objects).
    define reflective <control-reflective> (<reflective>)
      slot objs, init-value: #();
    end;
    Before we leap into full support of multiple threads, let us relax our restrictions only slightly, so that there can be one control reflective in addition to the one value reflective and arbitrarily many other simple reflectives. We fix that control reflective as the base one: <control-reflective>. Then we upgrade our system so that
    T = C --> C
    where C is the type of control reflectives. The new definitions of alpha and gamma are very similar to the previous ones, except that the list of simple reflectives is now carried in the objs field of a control reflective. Also, find-value-obj has been extended to get the value reflective object out of a control reflective object. define method alpha (v)
      method (c)
        update(c, objs: map(method (s) atomic(v, s) end,
                            c.objs))
      end
    end;

    define method gamma (i, r)
      method (c)
        let c2 = i(update(c, objs: map(compound-initial, c.objs)));
        let c3 = update(c2, objs: map(compound-rest, c.objs, c2.objs));
        r(find-value-obj(c3).base-value)(c3)
      end
    end;
    Finally, we are ready to support multiple threads of control. In this model, multiple value reflectives could cause a process to branch in different directions, leading to differing sets of reflective objects. We only leave one restriction in place now: no user-defined control reflectives. Even with this restriction, we have reached our final definition of T:
    T = C --> 2^C
    In the following definitions of alpha and gamma, we must use map/concat in two places to bring the type down from 2^2^C to 2^C. We also replace find-value-obj with the more general value-objs, which collects all the value reflective objects from among the set of objects carried by the control reflective object. define method alpha (v)
      method (c)
        list(update(c, objs: map(method (s) atomic(v, s) end,
                                 c.objs)))
      end
    end;

    define method gamma (i, r)
      method (c)
        map/concat(method (c2)
                     let c3 = update(c2, objs: map(compound-rest,
                                                   c.objs,
                                                   c2.objs));
                     map/concat(method (v) r(v.base-value)(c3) end,
                                value-objs(c3))
                   end,
                   i(update c, objs: map(compound-initial, c.objs)))
      end
    end;
    In the final revision of alpha and gamma, we allow for user-defined control reflectives. Remember that we must leave it up to atomic-control and compound-control to decide when to perform the computations, so we wrap most of the activity of alpha and gamma inside lambda-expressions, which are passed to the control functions.
    define method alpha (v)
      method (c)
        atomic-control
          (c,
           method (c1)
             list(update(c1, objs: map(method (s) atomic(v, s) end,
                                       c1.objs)))
           end)
      end
    end;

    define method gamma (i, r)
      method (c)
        compound-control
          (c,
           method (c1)
            i(update(c1, objs: map(compound-initial, c1.objs)))
           end,
           method (c2)
             let c3 = update(c2, objs: map(compound-rest,
                                           c.objs,
                                           c2.objs));
             map/concat(method (v) r(v.base-value)(c3) end,
                        value-objs(c3))
           end)
      end
    end;
    Looking back at the default definitions of atomic-control and compound-control in section [<-], we can see that we have merely uninstantiated those definitions in order to produce the final versions of alpha and gamma. One issue we have avoided so far is precisely how to choose the necessary set of reflectives for completely representing the user's choice of meta-information. It turns out that it is not necessary to instantiate all of the reflectives that are defined. Since any new reflective inherits the fields of its parent, and since the definitions of the methods specialized to the new reflective are intended to supersede those of the parent, it is sufficient to use only the leaves of the inheritance hierarchy. For example, once <bounded-int> has been defined, there is no longer any need to carry around an instance of <value-reflective> to represent expressed values.

    Interface
  • Now we are ready to return to the translation and extend it to support user-defined reflectives, reify, and reflect. The obvious and most convenient way to implement reflectives is as classes. Reflective objects are then merely instances of those classes. Thus, the translation of definitions of reflectives is an empty translation, if the notation for class definitions is the same as that of reflective definitions.
    In order to implement reify and reflect, we introduce two new functions: reflective-ref and reflective-subst. The reflective-ref function takes a control reflective object c and a reflective r and finds an instance of r in c. The reflective-subst function takes some reflective object o, a reflective r, and a control reflective object c and returns a control reflective object c' that is like c, except that o has been substituted for the instance of r in c. (The reflective object o should be of type r.)
    define method reflective-ref (c :: <control-reflective>, r)
      if (instance?(c, r))
        c
      else
        reflective-ref(c.objs r)
      end if
    end;

    define method reflective-ref (refls :: List, r)
      if (instance?(head(refls), r))
        head(refls)
      else
        reflective-ref(tail(refls), r)
      end if
    end;

    define method reflective-subst (o, r, c :: <control-reflective>)
      if (instance?(c, r))
        update(o, objs: c.objs)
      else
        update(c, objs: reflective-subst(o, r, c.objs))
      end if
    end;

    define method reflective-subst (o, r, refls :: List)
      if (instance?(head(refls), r))
        pair(o, tail(refls))
      else
        pair(head(refls), reflective-subst(o, r, tail(refls)))
      end if
    end;
    (The definitions above have been simplified a great deal. More robust definitions have to check for error cases and handle the appearance of more than one satisfactory target object in the list of reflective objects.) The translations of reify and reflect simply use these functions to do their work.
    R[reify(Er)]
        ===> bind r = R[Er]
             in method (c)
                  alpha(reflective-ref(c, r))(c)
                end
    R[reflect(Er, Eo, E]
        ===> bind r = R[Er]
             in bind o = R[Eo]
                in method (c)
                     R[E](reflective-subst(o, r, c))
                   end
    Each of these translations takes advantage of the knowledge that alpha and gamma are both functions expecting to be applied to instances of control reflectives.
    Conclusion
  • This paper has introduced reflection-oriented programming, both as a programming paradigm and as a system for conveniently directing the propagation of meta-level information through a process. The main contributions of this work have been to lay out a concise framework for computational reflection, to provide examples of its use, and to demonstrate a translation-based implementation. Using such an implementation enables reflection in compiled programs, even when some parts of the programs are compiled before it is known what meta-level concepts are to be modeled. Throughout the paper, a distinction has been made between reflection about programs and reflection about computations.
    This work leaves several issues unaddressed and open to further research. The most outstanding of these is a more precise formalization of reflection-oriented programming, so that it may be more carefully compared to monadic reflection and other formal accounts of reflection. It is clear that under some restrictions, the functions alpha and gamma, along with a variant of T, form a Kleisli triple [cite moggi:91]. Those restrictions should be made more precise, and the properties of reflectives that violate the restrictions should be investigated carefully.
    The power of the combination of value reflectives and control reflectives has not been fully explored. It should be possible to have a program perform a significant amount of ``static'' analysis on itself reflectively, where staticness is enforced by the manipulation of control reflectives. One extension of this idea is to create dynamically self-optimizing programs.
    Multiple inheritance and local (fluid) introduction of reflectives are not addressed in this paper, although we expect many of the discussions about these topics in object-oriented programming to carry over cleanly into the realm of reflection-oriented programming. Supporting multiple inheritance would enable the creation of libraries of reflectives from which a user could inherit in order to mix together several reflective properties. Supporting the fluid introduction of reflectives would allow such things as the measurement of computational expense for some segment of a computation without defining a new reflective globally.
    Our implementation is somewhat naive in its uniform translation of all programs, regardless of the particular reflectives defined in them. In order to make reflection usable and efficient, a great deal of attention needs to be focused on improving and optimizing our implementation. For example, in a closed system, where it can be assumed that no new reflectives can be introduced, it should be possible to eliminate many calls to alpha and gamma. (One approach would be to follow the methodology used by Filinski [cite filinski:94] for writing direct-style programs that use monads.) A related issue is the partial restriction of the extensibility of reflection-oriented programs. Sometimes, it might be desirable to leave a program only partially open to reflection, especially if total openness would create security risks.
    Reflection-oriented programming may have significant ramifications for characterizing programming language semantics more concisely and more modularly. In particular, presentations of interpreters and compilers could be greatly simplified by leaning more heavily on reflection, especially when combined with well-crafted use of object-orientation.

    AcknowledgmentsWe wish to thank Brian Smith for helpful comments on the nature of reflection and Erik Hilsdale and Jon Rossie for reading and critiquing our work. We also wish to thank anonymous referees whose critiques led to a revision of our notation and presentation strategies.

    References[1] Alan Bawden. Reification without evaluation. In Proceedings of the 1988 ACM Conference on LISP and Functional Programming, pages 342--351, Snowbird, Utah, July 1988. ACM Press.
    [2] Patrick Cousot and Rhadia Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Conference Record of the Fourth ACM Symposium on Principles of Programming Languages, pages 238--252. ACM Press, 1977.
    [3] Olivier Danvy and Karoline Malmkj&#230;r. Intensions and extensions in a reflective tower. In Proceedings of the 1988 ACM Conference on LISP and Functional Programming, pages 327--341, Snowbird, Utah, July 1988. ACM Press.
    [4] David A. Espinosa. Semantic Lego. PhD thesis, Columbia University, New York, 1995.
    [5] Andrzej Filinski. Representing monads. In Conference Record of POPL '94: 21st ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 446--457, New York, January 1994. ACM Press.
    [6] Cormac Flanagan, Amr Sabry, Bruce F. Duba, and Matthias Felleisen. The essence of compiling with continuations. In Proceedings of the ACM SIGPLAN '93 Conference on Programming Language Design and Implementation, pages 237--247. ACM Press, 1993.
    [7] Daniel P. Friedman and Mitchell Wand. Reification: Reflection without metaphysics. In Conference Record of the 1984 ACM Symposium on LISP and Functional Programming, pages 348--355, Austin, Texas, August 1984. ACM Press.
    [8] Stanley Jefferson and Daniel P. Friedman. A simple reflective interpreter. Lisp and Symbolic Computation, 9(2/3):181--202, May/June 1996.
    [9] Gregor Kiczales, Jim des Rivi&#232;res, and Daniel G. Bobrow. The Art of the Metaobject Protocol. MIT Press, 1991.
    [10] Sheng Liang, Paul Hudak, and Mark Jones. Monad transformers and modular interpreters. In Conference Record of POPL '95: 22nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, San Francisco, January 1995. ACM Press.
    [11] Pattie Maes. Concepts and experiments in computational reflection. Proceedings of OOPSLA '87, ACM SIGPLAN Notices, 22(12):147--155, December 1987.
    [12] Anurag Mendhekar and Daniel P. Friedman. An exploration of relationships between reflective theories. In Proceedings of Reflection '96, San Francisco, April 1996. To appear.
    [13] Eugenio Moggi. An abstract view of programming languages. Technical Report ECS-LFCS-90-113, Laboratory for Foundations of Computer Science, University of Edinburgh, Edinburgh, Scotland, April 1989.
    [14] Eugenio Moggi. Notions of computation and monads. Information and Computation, 93(1):55--92, July 1991.
    [15] Gordon D. Plotkin. Call-by-name, call-by-value and the lambda-calculus. Theoretical Computer Science, 1(2):125--159, December 1975.
    [16] David A. Schmidt. Detecting global variables in denotational specifications. ACM Transactions on Programming Languages and Systems, 7(2):299--310, April 1985.
    [17] Brian C. Smith. Reflection and semantics in a procedural language. Technical Report MIT-LCS-TR-272, Massachusetts Institute of Technology, Cambridge, Mass., January 1982.
    [18] Brian C. Smith. Reflection and semantics in lisp. In Conference Record of the Eleventh Annual ACM Symposium on Principles of Programming Languages, pages 23--35. ACM Press, January 1984.
    [19] Philip Wadler. Comprehending monads. Mathematical Structures in Computer Science, 2(4):461--493, December 1992.
    [20] Mitchell Wand and Daniel P. Friedman. The mystery of the tower revealed: A non-reflective description of the reflective tower. Lisp and Symbolic Computation, 1(1):11--38, June 1988.

0 个回复

您需要登录后才可以回帖 登录 | 立即注册
返回顶部