Friday, October 29, 2010

Aspects and Laziness

Can aspects add lazy evaluation to a programming language?

Based on the example of Java and AspectJ, the answer is, sadly, no. Many years ago, somebody new to AspectJ asked in an AspectJ mailing list what amounts to: can AspectJ help us do lazy evaluation?

More specifically, the question was about logging. Logging statements in Java look something like this:
logger.log(Level.INFO, "Value is: " + value);
But since string concatenation can be costly, many developers place the condition (which is part of the log method) in an if statement around the logging statement, like this:
if (logger.getLevel().intValue() <= Level.INFO.intValue()) {
logger.log(Level.INFO, "Value is: " + value);
}
Which is (a) more efficient and (b) patently ugly.

So, can AspectJ help? What about an around advice like this,
void around(Logger log, Level level, String s): callLog(log, level, s) {
if (log.getLevel().intValue() <= level.intValue() {
proceed(log, level, s);
}
}
-- with callLog defined as the appropriate pointcut for calls to log?

This certainly looks promising... except it doesn't work. Sure, the advice will prevent log from being called, but it won't prevent the value from being calculated. In particular, the advice is called with a ready value for the string argument s, meaning any required string concatenation had already taken place. This just adds an extra if that will then be repeated inside the log method, and never saves any time at all.

There is no proper AspectJ solution to this problem (that I'm aware of). In a recent paper called Transactional Pointcuts: Designation, Reification and Advice of Interrelated Join Points (GPCE '09) [link is to ACM Digital Library; I did not find a free PDF online], Hossein Sadat-Mohtasham and H. James Hoover outline an AspectJ solution that, among other things, solves this problem, by allowing the developer to define a sequence of events as a single, "transactional" pointcut, which can then be adviced. In this case, we can treat the single statement
logger.log(Level.INFO, "Value is: " + value);
as two statements: one builds the string value (using StringBuilder), and the other calls log. By defining a transactional pointcut around both, we can prevent the concatenated string from being built and the log method from being invoked needlessly. Mission accomplished.

Except, not really. Leaving aside the fact that the transactional pointcut requires twenty lines to define, it is also extremely specific. It will not prevent the string-returning costlyMethod from being called in a statement like this:
logger.log(Level.INFO, costlyMethod());
because there's no StringBuilder involved. The solution is extremely ad-hoc and utterly helpless in solving the general problem.

(This is not to say that transactional pointcuts, in general, are a bad idea. They're just not a solution to this specific problem.)

So, how can we solve this problem (and further generalize the solution, so it's not just about logging)? Let's look at what other languages do. In Python, logging statements accept a format string, followed by a list of values. Format strings are similar to the strings C's printf accepts. So in Python, our familiar logging line would look like this:
logging.info("Value is: %s", s)
The formatting, if required, takes place only inside the logging method; no costly string operations are performed if the logging will be suppressed in the current logging level. Good!

But this solution cannot be generalized for operations other than string handling. In particular, the problem remains with statements like
logging.info(costlyMethod())
To address this, Python programmers can use Lambda expressions. A method can accept a callback function rather than an explicit value, if it knows this parameter is not always required and could be costly to create. So we can have a line like:
foo(lambda: costlyMethod() + 3)
And if all we need is costlyMethod, we can just use the function name as the lambda expression:
foo(costlyMethod)  # No '()'
Still, I think a much neater solution would be to allow a method to declare a parameter as lazy. Something like:
public void log(Level level, lazy String str) { ... }
which would be completely transparent to the caller.

Maybe in Java 12.

1 comments:

  1. Two related research works... One, about the limitations of pointcut specifications, appear in the same conference as Sadat-Mohtasham and Hoover's paper, GPCE '09: Extending AspectJ for Separating Regions by Akai and Chiba. Two, about applying aspects as textual transformations of source code, something that can actually work for applying lazy evaluation: Guarded Program Transformations using JTL, by Cohen, Gil, and Maman (TOOLS '08).

    ReplyDelete