Back to a simple topic this week because I'm in LA at SIGGRAPH watching cool CG videos.
A technique that I use frequently is the fluent interface. It makes for concise, readable code in certain circumstances, and has withstood a decade of use in anger. Here's an example of a fluent API in action from my Depot library:
// selects all records with age <= 25
from(PersonRecord.class).where(PersonRecord.AGE.lessEq(25)).select();
In this case, from
returns a Query
object which allows a variety of intermediate calls to
configure the query, followed by a terminating call, in this case select
, which invokes the query
and returns the actual result.
Query
was clearly designed with fluency in mind and provides quite a few intermediate and
terminating calls:
class Query<T extends PersistentRecord> {
// intermediate calls
Query<T> ascending(SQLExpression<?> value)
Query<T> cache(DepotRepository.CacheStrategy cache)
// 30 more intermediate calls omitted for your sanity
Query<T> where(WhereClause where)
Query<T> whereTrue()
// terminating calls
int delete()
// 19 more terminating calls omitted for the sake of the children
List<Key<T>> selectKeys(boolean useMaster)
}
But many APIs are not designed for fluent use, even though they would benefit from it. Here's the
PlayN Surface
API circa 2011:
interface Surface {
void clear();
void drawImage(Image image, float dx, float dy);
void drawImage(Image image, float dx, float dy, float dw, float dh);
void drawImage(Image image, float dx, float dy, float dw, float dh, float sx, float sy, float sw, float sh);
void drawImageCentered(Image image, float dx, float dy);
void drawLine(float x0, float y0, float x1, float y1, float width);
void fillRect(float x, float y, float width, float height);
void restore();
void rotate(float radians);
void save();
void scale(float sx, float sy);
void setFillColor(int color);
void setFillPattern(Pattern pattern);
void setTransform(float m11, float m12, float m21, float m22, float dx, float dy);
void transform(float m11, float m12, float m21, float m22, float dx, float dy);
void translate(float x, float y);
}
And here's some ancient code that used that API:
SurfaceLayer bg = graphics().createSurfaceLayer(width, height);
bg.surface().setFillColor(Color.rgb(255, 255, 255));
bg.surface().fillRect(0, 0, bg.surface().width(), bg.surface().height());
bg.surface().setFillColor(Color.rgb(0, 0, 255));
bg.surface().fillRect(0, bg.surface().width() / 2, bg.surface().width(), bg.surface().height() / 2);
rootLayer.add(bg);
This API is crying out to be fluent. Compare:
SurfaceLayer bg = graphics().createSurfaceLayer(width, height);
bg.surface().setFillColor(Color.rgb(255, 255, 255)).
fillRect(0, 0, bg.surface().width(), bg.surface().height()).
setFillColor(Color.rgb(0, 0, 255)).
fillRect(0, bg.surface().width() / 2, bg.surface().width(), bg.surface().height() / 2);
rootLayer.add(bg);
That's less repetitious, and the trailing dots and indentation give you a visual indication that you're continuing a chain of calls.
Fortunately for users of the PlayN library, we changed its APIs to be fluent many years back. But even today, ten years after this pattern was introduced, I frequently find myself using libraries that are not designed for fluent use and would greatly benefit from being so. This motivates me to support such a usage pattern directly in the language.
There's also the, arguably unimportant, issue that using a fluent API results in generating code that does more work than necessary, relying on the optimizer to come clean up after the abstraction to restore things back to sensibility.
The compiler doesn't necessarily know that the object reference returned by each successive call to a method is the same one it started with, so it has to dutifully stuff that back into a register and use it as the base pointer for the next method call. This is more work than just sticking the base pointer into a register up front and making a succession of calls using the same base pointer. Of course that's complicated by the fact that you're calling a method and perhaps have to save all the registers anyway, etc. etc.
Regardless, I prefer programming language constructs where abstraction and convenience can be obtained without requiring the compiler to make a big mess and then (hopefully) clean up after itself.
Dot dot ...
One idea is to provide a ..
operator, which allows one to write calls like the following:
SurfaceLayer bg = graphics().createSurfaceLayer(width, height);
bg.surface().setFillColor(Color.rgb(255, 255, 255))..
fillRect(0, 0, bg.surface().width(), bg.surface().height())..
setFillColor(Color.rgb(0, 0, 255))..
fillRect(0, bg.surface().width() / 2, bg.surface().width(), bg.surface().height() / 2);
rootLayer.add(bg);
The semantics of the ..
operator would be to dispatch the method in question on the most recent
receiver of a normal .
in the current chained expression. Like many programming language
features, this is tricky to define in language lawyerese, but easy to use because it's designed to
"do what you mean". More examples:
// look ma, the Java standard library is fluent
List<String> strs = new ArrayList<>();
strs.add("tom")..add("dick")..add("harry");
// imagine for a moment that Guava's builders were not fluent
List<String> moreStrs = new ArrayList<>();
moreStrs.add("foo")..
addAll(new ImmutableList.Builder<String>().add("bar")..add("baz")..build())..
add("bippy");
You'll notice the second example nests a chain of calls inside an expression that is itself
chaining calls. It seems pretty natural to have the ..
operator "scoped" to a particular
expression.
That raises an interesting idea though, which could further simplify the Surface
example I showed
above. What if the ..
operator could pop out to an enclosing expression in certain (unambiguous)
circumstances? For example:
SurfaceLayer bg = graphics().createSurfaceLayer(width, height);
bg.surface().setFillColor(Color.rgb(255, 255, 255))..
fillRect(0, 0, ..width(), ..height())..
setFillColor(Color.rgb(0, 0, 255))..
fillRect(0, ..width() / 2, ..width(), ..height() / 2);
rootLayer.add(bg);
That's becoming hard to read, and (in spite of the fact that this example was taken straight from a real test case and in no way contrived) is perhaps not that widely useful.
Get with it
This leads me to another approach for addressing the issue: a with
block. It would push another
this
onto the search stack for the scope of a block. Now our example would look like so:
SurfaceLayer bg = graphics().createSurfaceLayer(width, height);
with(bg.surface()) {
setFillColor(Color.rgb(255, 255, 255));
fillRect(0, 0, width(), height());
setFillColor(Color.rgb(0, 0, 255));
fillRect(0, width() / 2, width(), height() / 2);
}
rootLayer.add(bg);
Inside a with
block, receiverless method calls first check the with
target before popping out
to search the methods accessible via the this
pointer (and so on if this is a language that
allows nested classes/objects, which hypothetical language certainly would).
This is nice when your chained calls are big enough to merit a separate line apiece, and neatly
solves the problem of wanting to refer to the object of interest inside your chain of expressions
(the nested width()
and height()
calls in the example).
But it doesn't have a very nice expression form. It makes sense to treat the "result" of a with
block as the target object itself, so we can write:
List<String> strs = with(new ArrayList<>()) {
add("tom");
add("dick");
add("harry");
};
But with
blocks don't nest as elegantly as I'd like:
List<String> moreStrs = with(new ArrayList<>()) {
add("foo");
addAll(with(new ImmutableList.Builder<String>()) {
add("bar");
add("baz");
}.build());
add("bippy");
};
Still, the above code is not painful to look upon, so perhaps it's a tolerable inelegance given the
relative infrequency with which one is likely to encounter nested with
blocks.
It is nice to leverage the widespread intuition of the effect that curly braces have on scope.
The fact that a nested with
statement makes add
mean ImmutableList.Builder.add
even though
we're nested inside a block where add
means ArrayList.add
probably didn't confuse you at all.
It fits nicely with an OO programmer's mental model.