Decent First Drafts

Let's say that you are implementing a Composite pattern for an interface with several interface functions. For functions which return void, it's straightforward to anticipate that the implementation will be a simple for_each. For functions which return a value, something has to be done to convert multiple return values into one. There are several obvious patterns:

- For functions like size() you call accumulate/fold_left and return an accumulated value.  This can't be extended arbitrarily to functions returning a numeric type: you might want, for example, to return a maximum value.

- For functions which return a boolean status, you return the results of applying std::none_of to a test for a false return, std::all_of to a test for a true return.

- For functions generating a string representation you return a string concatenating outputs with interpolated delimiters and possibly a beginning and ending. The same applies as a special case of output functions which return void but need extra calls between the delegates calls.

- For some functions returning structured data you may be returning another composite type.

In other circumstances the developer has to work out appropriate logic which may be rather specialized.

Now assume that you're writing an editor extension to generate a first draft of a Composite from a known interface.  You can make a good stab at the storage model (typically a vector of unique_ptr) and at the simple void model.  You can have special handling for known idioms like size(), empty(), or good(). But going beyond that gets into complex logic, usually with extra user prompts.

Pareto optimality kicks in here. If you generate a for_each call in each case, void return or not, you've at least provided a base for someone to work on, even if the first thing they do is convert it to a transform call. Or maybe you provide a fold_left call whenever the return value is an integer, or provide a flag to turn that option on or off.

You can generate comments marking where obvious work needs to be done.

The aim of such a utility is to provide a quick leg up, not to anticipate everything the user will be doing.  If the user will be you, if you do a lot of elaborate work now, you're using up time that you would otherwise be spending in the future. Only if the time you will save in the future will be commensurate with the time you spend crafting a specialization now is it worthwhile to do the work now.

There is an ideal of the pure developer who sits and thinks for a day and then writes out the code in a beautiful final form. Back in the days of handing in punch cards through a window just to get a test run, this sort of discipline made sense: iterative efforts were expensive and time-consuming. (Literally expensive, in the case of time-share systems where different users were competing for limited resources and every minute of computation was money.)

The next step up was writing code, printing it out, and editing the printouts by hand before doing anything with it.

But in a world of personal computers and quick compilation and unit tests, iterative development is the norm. Whether one does TDD or not, one starts by getting something that will compile, then something that works, and then it gets worked on to improve clarity, efficiency, and, ideally, elegance. Code generation tools, whether they are simple macros to save keystrokes (I have Control-s bound to inserting "const std::string&") or elaborate class generation mechanisms, speed up but in no wise replace the mental work involved in doing serious development.

The flip side of this is that you don't have to build an elaborate parser to do useful code generation, which means a low cost - which in turn improves the cost/benefit equation for writing small utilities.

It's fairly simple, for example, to generate a base draft of a Null Object implementation of a known interface, on the assumption that it should be as close to a do-nothing object as possible, returning zeroes and empty strings. That will frequently not be quite true, but it's a good start.

A Null Object's base reason to exist is to avoid passing around null pointers. But its secondary reason is to avoid having to test its nature at all, and that means that it needs to do things like capture a default action on not found.

Just replacing

Foo* val = getThing();

if (val != nullptr)

    process(val->getAccount(), bar);

with 

const Foo& val = getThing();

if (!val.isNull())

    process(val.getAccount(), bar);

may give you a bit of safety if you forget the test, but it doesn't do much to clean up the call site - but you see it a lot with Null Objects where the implemented interface has been designed as a data source with getters rather than as an action mechanism.

The same is essentially true of 

const Foo& val = getThing();

if (!val.getAccount().empty())

    process(val.getAccount());

which eliminates the bad smell in isNull() by resorting to breaking the Law of Demeter.

A better class design generally allows you to replace

Foo* val = getThing();

if (val != nullptr)

    val->execAction(bar);

with

getThing().execAction(bar);

and eliminate the test.

In a GUI, the action might mean popping up a dialog to inform the user of an error. It might mean logging an issue in any context. It might send off a message to a maintainer saying "this needs to be added to the database". But a good assumption in generating a first draft is a no-op. And you don't have to do much parsing to pull function declarations out of an abstract class interface and generate no-op implementations.

The same is true of that Composite pattern I led off with. If you do enough parsing to generate a valid for_each loop, you can leave it to the time of detailed implementation of a generated class to have the details of how return values should be handled.

Comments

Popular posts from this blog

Boundaries

Overview

Considerations on an Optimization