• 沒有找到結果。

Kung Chen · Shu-Chun Weng · Jia-Yin Lin · Meng Wang · Siau-Cheng Khoo

Received: date / Accepted: date

Abstract Many side-effecting programming activities, such as profiling and tracing, can be formulated as crosscutting concerns and be framed as side-effecting aspects in the aspect-oriented programming paradigm. The benefit gained from this separation of concerns is particularly evident in purely functional programming, as adding such aspects using techniques such as monadification will generally lead to crosscutting changes. This paper presents an approach to provide side-effecting aspects for lazy purely functional languages in a user transparent fashion. We propose a simple yet direct state manipulation construct for developing side-effecting aspects and devise a systematic monadification scheme to translate the woven code to monadic style purely functional code. Furthermore, we present a static and dynamic semantics of the aspect programs and reason about the correctness of our monadification scheme with respect to them.

Keywords Aspect-oriented programming · Side-effect · Lazy semantics · Monadifica-tion

? This is an extension of the work presented at the ACM SIGPLAN 2009 Workshop on Partial Evaluation and Program Manipulation (PEPM’09).

Kung Chen · Jia-Yin Lin

National Chengchi University, Taiwan E-mail: {chenk,g9405}@cs.nccu.edu.tw Shu-Chun Weng

Yale University, USA E-mail: [email protected] Meng Wang

Oxford University, UK E-mail: [email protected] Siau-Cheng Khoo

National University of Singapore, Singapore E-mail: [email protected]

1 Introduction

Aspect-oriented programming (AOP) aims at capturing crosscutting concerns. Many crosscutting concerns are orthogonal to the mainline computation that they interacts with. A few examples of such concerns are profiling, tracing, and memoization. By their own nature, aspects that implement such orthogonal concerns do not alter the evalua-tion result of the base program; their computaevalua-tions are manifested in side-effects. We refer to such aspects as side-effecting aspects (also know as non-interfering aspects [2]

from a data-flow perspective). Side-effecting aspects are particularly useful because or-thogonal concerns are more likely to be subject to deploying, updating, and removing from a software system, a situation where AOP solutions are doubly attractive.

Most aspect-oriented programming languages are based on object-oriented lan-guages, where uncontrolled side-effects are the norm. The recent surge of interests in introducing aspect-oriented concepts in functional languages [3,16], in particular in purely functional languages [1,21,18], poses fresh challenges. Although we can hide the hairy details of state manipulation by using monads [19], it does require advance planning, which is fundamentally at odds with the concept of obliviousness in AOP.

Adding monadic effects to a pure program entails a comprehensive rewriting; it is therefore convenient to support side-effecting directly and automate the process via source-to-source transformation. Such a technique has been pioneered by L¨ammel [14]

and is referred to as monadification by Erwig and Ren [5]. In many situations, this convenience comes at a cost: primitive support for side-effects compromises referential transparency property and all the nice reasoning properties that derive from it. Our proposal, on the other hand, eliminates the need for compromise between property preservation and convenience. This is achieved through a carefully designed weaving scheme, which preserves the non-interfering nature of side-effecting aspects.

In our previous work on AspectFun [1], an aspect-oriented lazy functional language with a Haskell-like syntax, we have developed a state-based implementation for control-flow related advice which uses a reader monad to maintain function execution states (entry and exit) and employs a monadification step to convert the woven program. In this paper, we generalize this approach to the language level by providing constructs for writing side-effecting aspects directly and systematic monadification procedures for im-plementing them. Specifically, we propose to equip AspectFun aspects with user-defined mutable variables for performing side-effecting operations and extend its compiler with a more powerful monadification module based on cached state monad transformers to realize them.

The general vision is clear: a state monad is employed as the repository for mutable variables and and all functions are lifted into monadic ones. But care must be taken in implementing of such a scheme. First of all, monadification always imposes an evalua-tion order, which may or may not be what is desired. Even when a preferred evaluaevalua-tion order is known up front, it is not a simple task to instruct the monadification process to faithfully follow in the context of lazy semantics of Haskell. Let’s illustrate this point by considering a small example involving debugging Haskell programs through tracing taken from [4].

f x = 3 ‘div‘ x

h = ... -- arbitrarily deep computation g = h (f 0)

The function div is partial because it may throw the divide-by-zero exception. When this happens, the Glasgow Haskell compiler (GHC) only outputs the very unhelpful message “*** Exception: divide by zero”. Since there can be arbitrarily many calls to div, which can be arbitrarily deep in nesting, more informative tracing messages may be appreciated. However, if we follow the lazy evaluation of Haskell, the trace includes a call to g, followed by a call to f , and an arbitrarily complicated execution of h before the offending call to div shows up. Any useful information may be overwhelmed by the “noise” of h’s evaluation. What one really wants here is a short trace to the exception, which skips h’s body following an eager evaluation order. Yet, this may not be preferred when the presence of laziness is necessary, for example when dealing with infinite values.

In this paper, we make the following contributions:

1. We extend the aspect-oriented functional language, AspectFun, with side-effecting constructs that support direct state manipulation in aspects.

2. We present a general type-directed monadification scheme that transforms woven code into monadic style purely functional code. The semantics and correctness of the scheme are discussed in detail.

3. We devise a cached state monad in Haskell to support the lazy evaluation of monad-ified expressions in side-effecting aspects.

4. We demonstrate with examples the effectiveness of our system in dealing with tracing, profiling, and optimization of lazy functional programs.

5. We outline a uniform monadification scheme that can also handle monadic base programs.

The rest of the paper is organized as follows. Section 2 first reviews our base language, AspectFun, and describes the language constructs we design for writing side-effecting aspects. Section 3 presents a general framework of monadification with respect to an abstract monad, followed by the semantics and correctness of this framework.

Section 4 specializes the abstract monad to Haskell state monads for implementing side-effecting aspects in AspectFun, and describes the issues of and solutions to pre-serving laziness in our monadification scheme. Section 5 illustrates how we can use monad transformers to handle monadic base programs and outlines a unified monadifi-cation scheme that accommodates both cases. Section 6 describes related work. Finally, Section 7 summarizes and discusses the future work.

2 Extending AspectFun with Side-Effecting Aspects

This section describes the language constructs we propose for developing side-effecting aspects in AspectFun. After giving a brief overview of AspectFun, we shall present the proposed extension for manipulating states in aspects along with some examples. To ease the presentation of the examples, we shall use pattern matching and freely employ functions available in the Haskell Prelude and a few Haskell constructs that are not yet implemented in AspectFun.

2.1 AspectFun Overview

Figure 1 shows the syntax of AspectFun. We write ¯o as an abbreviation for a sequence of objects o1, ..., on(e.g. declarations, variables etc). An AspectFun program is a sequence

of top-level declarations followed by a main expression. Top-level definitions include global variables and function definitions, as well as aspects. An aspect declaration pro-vides two specifications: An advice, which is a function-like expression named via the prefix n@; and a pointcut designator , around {pc}, designating when the advice will be executed. In aspect-oriented programming [11], the specific program execution points that triggers advice are called join points. Here, we focus on join points at function invocations. Thus a pointcut basically specifies a function whose invocations may trig-ger the execution of advice. The act of trigtrig-gering advice during a function application is called weaving. When an advice of the form “n@advice around {pc} (arg) = e” is triggered by a call to a function, say f , the argument variable arg is bound to the actual argument of the f -call.

Programs π ::= d in π | e

Declarations d ::= x = e | f x = e | f :: t → t | n@advice around {pc} (arg) = e

Arguments arg ::= x | x :: t

Pointcuts pc ::= ppc | pc + cf | pc − cf Primitive PC’s ppc ::= f x | any | any\[f ] | n Cflows cf ::= cflow(f ) | cflow(f ( :: t)) |

cflowbelow(f ) | cflowbelow(f ( :: t))

Expressions e ::= c | x | proceed | λx.e | e e | if e then e else e | let x = e in e

Types t ::= Int | Bool | a | t → t | [t]

Predicates p ::= (f : t) Advised Types ρ ::= p.ρ | t Type Schemes σ ::= ∀¯a.ρ

Fig. 1 Syntax of the AspectFun Language

Advice may be executed before, after , or around a join point. Specifically, around advice is executed in place of the indicated join point, allowing the call to the advised function to be replaced. A special keyword proceed may be used inside the body of around advice. It is bound to the function that represents “the rest of the computation”

at the advised join point. As both before advice and after advice can be simulated by around advice that uses proceed, we only consider around advice in this paper.

Precisely, a pointcut, pc, may be either a primitive pointcut or a composite pointcut.

A primitive pointcut, ppc, specifies a function (f ) or an advice name (n) the invocations of which will be advised. A sequence of pointcuts, pc, indicates the union of all the sets of join points selected by each. A primitive pointcut can also be a catch-all keyword any.

When used, the corresponding advice will be triggered whenever a function is invoked.

Name-based primitive pointcuts can be composed with control-flow based pointcuts (cflow and cflowbelow) to form composite pointcuts, which inspect the run-time stack of function execution.

In Figure 1, the argument variable arg may contain a type scope, the t in x :: t.

When such a type scope is present, the applicability of a piece of advice is bounded by its pointcut as well as its type scope. Specifically, when the function in the pointcut is polymorphic, a type scoped argument only matches executions of the function with arguments of types that are subsumed by their scope. This is particularly useful as many functional languages are polymorphically typed.

Expressions in AspectFun are pretty standard and are evaluated with a lazy seman-tics. As mentioned above, the special keyword proceed may be used inside the body of around advice. When applied, proceed resumes the execution of advised functions or other advice that also designates the same function as its join point, as in AspectJ.

AspectFun is polymorphically and statically typed. It introduces a concept of ad-vised types [20] that extend types with predicates of the form (f : t). Adad-vised types are inspired by Haskell’s type classes and are used to capture the need of advice weaving based on type context. As a result, AspectFun is able to statically resolve type scopes in pointcut and statically weave aspects into base program. In previous work, we have built a compiler that employs a type-directed static weaver to translate an AspectFun program into executable Haskell code [1]. Moreover, our monadification procedure for handling side-effecting aspects is largely independent of the static weaving step, as it is performed after the weaving step during compilation. Therefore, we shall not discuss the processing of pointcuts and advice in this paper.

2.2 Side-Effecting Aspects

We now describe how we extend AspectFun to support side-effecting aspects. The es-sential construct we add to AspectFun is user-defined mutable variables declared within the scope of an aspect. We use var as the keyword to begin such a declaration. The syntax of an aspect declaration is also slightly extended to include both declarations of advice and of variables. The precise syntax is as follows.

Declarations d ::= . . . | var id :: t [= e] | n@advice around {pc} (arg) = e AspectDecl ad ::= aspect name where ¯d

Such mutable variables are declared with a monomorphic and ground type, t, and an optional initializing expression, e. Equipped with them, advices in the same aspect are able to keep pertinent state information forming side-effecting aspects. For exam-ple, the following declaration introduces a mutable variable profileMap whose type is Map.Map String Int with initial value empty1. Later, we shall use it to develop a profiling aspect.

var profileMap :: Map.Map String Int = Map.empty

Associated with each mutable variable declared, there is a pair of implicitly de-clared getter and setter functions for interacting with the state. Their side-effects are sequenced by sequencing expressions, (e1; e2). In particular, the variable declaration above results in the following declarations of a setter and a getter function, respectively.

getProfileMap :: Map.Map String Int setProfileMap :: Map.Map String Int -> ()

1 The Map is an alias of the Data.Map in Haskell’s standard hierarchical libraries.

Let’s look into an example of Fibonacci function fib benefiting from a momoization aspect to remove repeated computation and a profiling aspect.

Example 1

fib n = if n <= 1 then 1

else fib (n - 1) + fib (n - 2) in --aspect 1

aspect profiler where

var profileMap :: Map.Map String Int advice around {fib} (arg) =

let incProfile fname =

set! pMap = getProfileMap;

let newMap =

case of Map.lookup fname pMap of

Nothing -> Map.insert fname 1 pMap Just v -> Map.insert fname (v+1) pMap in setProfileMap newMap

in incProfile "fib"; proceed arg in --aspect 2

aspect memoFib where

var memoMap :: Map.Map Int Int advice around {fib} (arg) =

case lookupCache arg of Just v -> v

Nothing -> set! v = proceed arg;

insertCache arg v; v in fib 10

Caution has to be taken for operations involving state access since the order of evaluation matters. We use the keyword set! for sequenced bindings. They effectively force the evaluation of a binding prior to the evaluation of its body, simulating a kind of eager semantics. In profiler, the auxiliary function incProfile makes sure the state is fully evaluated before attempting to update it, removing the risk of a race condition. In memoFib, the inputs of the state operation insertCache, are evaluated before the state update. Though it is probably not the only way to correctly implement the momoization aspect, we enforce the coding convention for the sake of program comprehension.

Besides mutable variables, IO is also an important element for side-effecting as-pects such as tracing asas-pects. Hence we also provide a function, putMsg :: String -> String -> (), for performing output in aspects. The first string parameter is the name of aspect which puts the second parameter (the message) into an internal buffer.

Together with the getter and setter functions, they form the state API of an aspect.

The second example is a tracing aspect for the tail recursive factorial function, adapted from Kishon’s thesis work on program monitoring [12].

Example 2

fac n acc = if n == 0 then acc

else fac (n - 1) (n * acc)

aspect tracer where

var indent :: String = ""

advice around{fac, (*)} (arg) = \arg2 ->

set! ind = getIndent ; setIndent ("| " ++ ind);

set! v1 = arg;

set! v2 = arg2;

putMsg "tracer" (ind++tjp++" receives ["++

show v1 ++ ", " ++ show v2 ++ "]");

set! result = proceed v1 v2 ; setIndent ind;

putMsg "tracer" (ind++tjp++" returns " ++

show result);

result

Here the state to be maintained is the indentation string, stored in the variable, indent.

The tracer aspect traces the execution of the functions, fac and (*), respectively.

The tjp is a keyword for referring to the function currently being advised, namely the current join point.2The advice simply traces the arguments passed to and the results returned from the advised functions via the show function. Note that we have used sequenced bindings to enforce a call-by-value trace, which is printed below.

fac receives [3, 1]

| | times receives [3, 1]

| | times returns 3

| fac receives [2, 3]

| | | times receives [2, 3]

| | | times returns 6

| | fac receives [1, 6]

| | | | times receives [1, 6]

| | | | times returns 6

| | | fac receives [0, 6]

| | | fac returns 6

| | fac returns 6

| fac returns 6 fac returns 6

In Section 4, we look into how a lazy trace can be obtained, which turns out to be non-trivial because the execution of any added IO operations easily interact with the trace of the base program, causing changes in evaluation order.

3 Monadifying Aspect Programs

The first step of AspectFun compilation is to weave aspects into the base program, thus producing an integrated program of expressions, which we call woven code. In the presence of side-effecting aspects, it is necessary for the woven code to be transformed

2 AspectFun does not support the tjp facility yet. Nevertheless, we can write two almost identical aspects to trace fac and (*), respectively.

to a monadic style, in order to retain its functional purity. This and the following section illustrate our monadification transformation for expressions, pure or side-effecting, in a woven code.

First, we present a general framework for monadifying an expression using an ab-stract monad, (M, return, >>=), in a non-strict evaluation context, and show that our monadification scheme possesses good properties with respect to the static and dynamic semantics of expressions in woven code. Next, in the following section, we specialize M to a specific state monad in Haskell so that we can also define the monadified version of those state-aware functions used by side-effecting aspects.

3.1 Monadifying Expressions

Like the pioneering work of L¨ammel [14], our monadification transformation consists of two major steps, namely A-normalization [7] and monad introduction.

3.1.1 A-Normalization

Given an expression, A-normalization converts it into a form in which every intermedi-ate computation is assigned a name by a let-expression. Such normalized expressions, called A-normal form, is a popular intermediate representation used in compilers [7]

and semantic specifications [15] for functional languages. Essentially, in A-normal form, all applications are applications of an expression to a variable. The arguments of an application and the condition part of an if-expression are all captured by the binding parts of let-expressions wrapped around them.3

Let us take the profiling of the fib function presented before as an example. The input to our A-normalization step is the following woven Haskell code generated by the AspectFun compiler.

let profiler proceed arg = incProfile "fib";

proceed arg in let fib n = if n <= 1 then 1

else profiler fib (n - 1) + profiler fib (n - 2) in profiler fib 10 --main

The aspect, profiler, becomes an ordinary function with an additional parameter, proceed that captures the continuation to the advised function. Moreover, all invoca-tions of the fib function are now left to the profiler function.

After A-normalization, the above profiler program is converted to the following code.

let profiler proceed arg = incProfile "fib";

proceed arg in let fib n = let nleq1 = n <= 1 in

if nleq1 then 1

else let nm2 = n - 2 in

3 Note that we conduct alpha renaming along with A-normalization to avoid any name conflicts.

let fibm2 = profiler fib nm2 in let nm1 = n - 1 in

let fibm1 = profiler fib nm1 in (+) fibm1 fibm2 in

profiler fib 10 --main

We note that A-normalization changes only the structure of a program, not the order of argument evaluation, as let-expressions are evaluated lazily.

As a result of A-normalization, the syntax of the expressions to be monadified can be summarized as the following three syntactic categories for ease of subsequent discussion.

Atoms a ::= c | x

Pure Expressions e ::= a | p | λx.e | e a | let x = e in e | if a then e else e

Effectual Expressions e! ::= · · · | e!; e!| set! x = e!; e!

Atoms include constants and variables. Besides atoms and primitives (p), pure expres-sions are A-normalized standard expresexpres-sions. The changes made by A-normalization are manifested in applications and if-expressions. Effectual expressions extend pure

Atoms include constants and variables. Besides atoms and primitives (p), pure expres-sions are A-normalized standard expresexpres-sions. The changes made by A-normalization are manifested in applications and if-expressions. Effectual expressions extend pure