Tuesday, March 11, 2014

The Roll - Unroll Pattern

The pattern that is most predominant in programming is:
  • Setup some state
  • Do some work
  • Clean up
Manual housekeeping of this repetitive task requires too much care and energy, from the programmer’s side. Repetitive tasks sounds like a work that should be automated, rather than maintained manually.
Read how you can create an elegant piece of code, while reducing the maintenance load.


Example: Reading a file

This is the second most common example, after Hello World!.
FILE *fp = fopen("output.txt", "wt");
fprintf(fp, "Hello World");
fclose(fp);
The file handle, fp, is never used unless it has been initialized with fopen().
The resource must always be freed, and cannot be used after fclose().
It therefore has a limited scope.
It is very important to release the file handle – failure to do so, repeatedly, may cause your file system to block.

Example: OpenGL

In OpenGL you cannot put any vertices through the pipeline, unless you have declared what they’ll be used for, using glBegin(). No shape is going to be drawn unless you conclude that the vertices are over, using glEnd().
Here’s an example of how to draw a single line:
glBegin(GL_LINES);            // initialize
glVertex3f(0.0, 0.0, 0.0);    // work
glVertex3f(15, 0, 0);
glEnd();                      // finalize
If, additionally, you wish to temporarily set the line thickness, it is wise that you declare it before start drawing, and set it back to the previous thickness, after you’re done.
GLfloat prev_line_width;
glGetFloatv(GL_LINE_WIDTH, &prev_line_width);
glLineWidth(15);              // change width

glBegin(GL_LINES);            // initialize
glVertex3f(0.0, 0.0, 0.0);    // work
glVertex3f(15, 0, 0);
glEnd();                      // finalize

glLineWidth(prev_line_width); // resume width
Note that the order of actions should be reversed after the work ahs taken place.

Example: Filling a listview

Filling a listview can be a time-consuming task. You decide to temporarily set the mouse cursor to busy while filling takes place. You also want to turn off the sorting of the list, because sorting while you append in the list will delay the process.
int prev_mouse_cursor, prev_sort_state;

prev_mouse_cursor = get_cursor();       set_cursor(BUSY);
prev_sort_state = get_sorting(list);    set_sorting(list, NO_SORT);
...
item = add_item_to_list(list, text);   // work
...
set_sorting(list, prev_sort_state);
set_cursor(prev_mouse_cursor);
And notice how hairy things can get when the situation is just slightly more complicated:
Let’s assume that as you add items to the listview, you notice an invalid item, and you have to ask whether the user whether to continue adding items, or not.
int prev_mouse_cursor, prev_sort_state;

prev_mouse_cursor = get_cursor();       set_cursor(BUSY);
prev_sort_state = get_sorting(list);    set_sorting(list, NO_SORT);
...
item = add_item_to_list(list, text);   // work
if (is_invalid(item)) 
{
    int prev_mouse_cursor_2 = get_cursor();   set_cursor(ARROW);
    if (ask("bad item - should I continue?") == NO) {
         set_sorting(list, prev_sort_state);
         set_cursor(prev_mouse_cursor);
         return;
    }
    set_cursor(prev_mouse_cursor_2);
}
...
set_sorting(list, prev_sort_state);
set_cursor(prev_mouse_cursor);

Solution

In C++, the initialize/finalize concept is built into the language. The concept is called constructor and destructor. The constructor is called when an object is created, and the destructor is called when the object leaves out of scope. If many objects are created in the stack, the destruction order is the reverse of the order that they were constructed.

Example revisited: filling the listview

ScopedCursor temporarily_set_cursor_to(BUSY);
ScopedSorting temporarily_set_sorting_state_to(list, NO_SORT);
...
item = add_item_to_list(list, text);   // work
if (is_invalid(item)) 
{
    ScopedCursor temporarily_set_cursor_to(ARROW);
    if (ask("bad item - should I continue?") == NO) {
         return;
    }
}
How simpler is that to read and maintain ?
  • The temporarily_set_cursor_to and temporarily_set_sorting_state_to are merely variables, that will die after their scope ends, thereby rolling back their actions. The memory footprint is exactly the same as the one with the C implementation. Running time is exactly the same, and so is the execution order.
  • I’ve carefully named the variables so that they read like english.
  • the struct’s name, Scoped is chosen so that it reminds me of its lifetime
What is the code that supports this automation ? It is simply a struct, that runs a function on construction, and another function when it goes out of scope:
struct ScopedCursor 
{
    int prev_cursor;

    ScopedCursor(int new_cursor) {    // on construction
        prev_cursor = get_cursor();
        set_cursor(new_cursor);
    }

    ~ScopedCursor() {                 // on destruction
        set_cursor(prev_cursor);
    }
};
Written with StackEdit.

No comments:

Post a Comment