Modularity in many dimensions
One program, many faces
Aspect-oriented programming (AOP) is the latest development in our ongoing battle with the problem ofFluid AOP addresses this issue by taking a much less static view of programs. The idea is to treat aspects as editable -- "effective" -- views which are generated on demand for particular coding activities. For persistence purposes, one view is nominated as the "primary decomposition". (With a legacy language this may just be the regular source view, but in principle the primary decomposition can be any view that suffices to determine all the others.) Making these views effective is where bidirectionality comes in: each view must define how edits are "put back" as edits to the primary decomposition.
Before we come to fluid AOP, I want to revisit its motivation, and in particular the modularity challenge which is sometimes called
But I'm getting ahead of myself. Let's start with modularity. I'll understand modularity to mean, roughly, the extent to which a change to or extension of functionality requires only local changes. I want to make the case that the pursuit of "absolute" modularity is doomed: that we must always ask "modularity with respect to what kind of change?" In effect the question "how modular is my software?" is one that should be qualified by a class of transformation. If you're already familiar with the Expression Problem, you can skip the next section.
Modularity with respect to what?
Imagine we're writing an interpreter in, say, Java, for a simple language for integer arithmetic. We crank out the usual OO class hierarchy:abstract class SyntaxNode {
public abstract int eval ();
}
class Add extends SyntaxNode {
public SyntaxNode left, right;
public int eval () {
return left.eval() + right.eval();
}
}
class IntegerLiteral extends SyntaxNode {
public int val;
public int eval () {
return val;
}
}(Don't worry about the fact that the fields are public; that's just me being lazy.)We can make the observation here that
eval:class Multiply extends SyntaxNode {
public SyntaxNode left, right;
public int eval () {
return left.eval() * right.eval();
}
}What we have identified is a Now let's consider a different kind of change. Instead of a new kind of node, we want to provide a new kind of operation on nodes, say a
toString method. One might be tempted to add the following code to the SyntaxNode class:public final String toString () {
if (this instanceof IntegerLiteral) {
IntegerLiteral op = (IntegerLiteral)this;
return op.val;
}
else
if (this instanceof Add) {
Add op = (Add)this;
return op.left.toString() + " + " + op.right.toString();
}
else
throw new AbstractMethodError();
}The problem with this approach, as we all know from standard OO doctrine, is that our über-method has to know about every kind of syntax node; in particular if we want to add a new kind of syntax node, we need to update this method as well to add a new clause:...So what we don't like about this approach is that it violates modularity with respect to the addition of new node types.
if (this instanceof Multiply) {
Multiply op = (Multiply)this;
return op.left.toString() + " * " + op.right.toString();
}
...
In a functional language like Haskell this particular kind of modularity failure isn't considered bad; in fact it's normal! Our class hierarchy would correspond to an algebraic data type:
data SyntaxNode =All our functions would start with a case analysis, the functional counterpart of a type switch:
Add SyntaxNode SyntaxNode |
IntegerLiteral Int
eval :: SyntaxNode -> Intand adding a new kind of node would require modifying every function to add the corresponding clause.
eval x = case x of
Add a b -> eval a + eval b
IntegerLiteral n -> n
toString :: SyntaxNode -> String
toString x = case x of
IntegerLiteral n -> show n
Add a b -> toString a ++ "+" ++ toString b
But returning to the world of OO, the "right" way to define
toString is as follows:class SyntaxNode {
...
public abstract String toString();
}
class Add {
...
public String toString () {
return left.toString() + " + " + right.toString();
}
}This allows us to maintain modularity with respect to the addition of new node types. As before, we need only define a new class and provide an implementation of the various abstract methods:class Multiply extends SyntaxNode {
public SyntaxNode left, right;
public int eval () {
return left.eval() * right.eval();
}
public String toString () {
return left.toString() + " * " + right.toString();
}
}But clearly this kind of modularity actually came at the expense of modularity of a different kind. To make it easy to add node types, we had to spread our toString implementation across the class hierarchy. In other words adding new operations entails modifying each class; the design thus lacks modularity with respect to the addition of new node operations.FPers usually inhabit the other corner of the trade-off space: they enjoy modularity with respect to new operations, but not with respect to new node types. Although we could probably achieve a similar inversion of modularity in Haskell:
class SyntaxNode a whereIn either paradigm, it seems that we're torn between operations cross-cutting data types, and data types cross-cutting operations (although OO prefers the former, and FP the latter). Which of these two alternatives is the "more modular" simply depends on whether it's operations, or data types, that we intend to modify. That's not to say that whether a design is modular is a mere matter of taste or opinion, but just that modularity is relative to what you're trying to do.
eval :: a -> Int
toString :: a -> String
data IntegerLiteral = IntegerLiteral Int
instance SyntaxNode IntegerLiteral where
eval (IntegerLiteral n) = n
toString (IntegerLiteral n) = show n
data SyntaxNode a => Add a = Add a a
instance SyntaxNode a => SyntaxNode (Add a) where
eval (Add a b) = eval a + eval b
toString (Add a b) = toString a ++ "+" ++ toString b
data SyntaxNode a => Multiply a = Multiply a a
instance SyntaxNode a => SyntaxNode (Multiply a) where
eval (Multiply a b) = eval a * eval b
toString (Multiply a b) = toString a ++ "*" ++ toString b
The multi-dimensional modularity problem
It's tempting to look for a "linguistic" solution to this problem. Perhaps with the right language feature (or the right design pattern), it would be possible to make a program modular with respect to both kinds of transformation simultaneously. Some time ago Philip Wadler dubbed this challenge theBut although there certainly are linguistic approaches to the Expression Problem, there is usually a cost in terms of design-time
Standard "static" AOP is not really a response to the Expression Problem but to this more general problem of multi-dimensional composition. It avoids the granularity problem associated with multi-methods and similar approaches by allowing us to specify aspects as cohesive design-time modules. But the very cohesion of these definitions means that they are inevitably cross-cut by other features of the system. These features can now only be understood by reasoning about how the aspects are woven into them. The benefit of increased modularity in some respects has, as with our interpreter example earlier, been bought at the price of reduced modularity in others.
The basic problem is that there is no static language feature that can universally resolve the tension between modularity and cohesion. What we really need is greater
Cohesion-on-demand turns weaving and unweaving into a browsing activity. The programmer no longer has to reason about complex composition rules à la multi-methods or static aspects, as she can simply inspect the content of the relevant view.
Where fluid tools become fluid languages
So it seems like fluid AOP is the only plausible approach to the problem of multi-dimensional modularity. It's hugely exciting, but also rather daunting when one thinks about what's involved in making the vision a practical reality. The words "indistinguishable" and "magic" come to mind. I'm well out of time and space resources, so I'll wrap up for now by just sketching what I see as the way forward.Shortcomings aside, linguistic approaches have one indispensible property: they offer well-defined semantics for multi-dimensional composition. (As an example, see this paper, which extends Hindley-Milner type inference with static advice weaving.) The future of fluid AOP lies I think in harnessing this linguistic precision in an interactive tool.
After all, if we hope to rely on "fluid aspect" views to visualise key features of our software, as well as mediate critical changes to it, then they really are "the source files of the future". How the contents of the various views are combined, how edits are put back, and so forth, will have to be precisely defined. In turn, this will require fluid aspects to have well-defined syntax.
Indeed the difference between fluid aspects and traditional syntax trees is not one of kind, but only of lifecycle: they are built on the fly in response to the user's changing goals, and may co-exist with other effective views that share the same underlying model. But otherwise they are just plain ol' syntax. We see a taste of this in the "virtual source files" of Janzen and De Volder, as well as in the "fluid source code views" of Desmond et al.
(Of course I don't mean that we should necessarily store such syntactic artifacts in text files, or present them to users as flat, uninterpreted text; but in an uncontroversial sense they are "just syntax". This was the point of my post Why syntax won't go away.)
A corollary of this perspective is that we must regard our interactions with these views as a form of
A mature AOP, then, is not a static language feature as such, but an
toString method we defined earlier as a single type switch, for convenience; expand it over a class hierarchy, in order to add a new node type like Multiply; collapse it back to a single method again, perhaps to modify its signature; expand it again, to add more node types; and so on.The toggling, in this hypothetical scenario, between two different points in modularity space is little more than the iterated application of the refactoring
Labels: Expression Problem, fluid AOP, meta-programming, modularity

