What is OPF Programming?

Introduction

OPF Programming (One Point Five Programming) is a programming paradigm with clear definition.

It contains two parts:

The skill (*p)->f(p) is important and practical, but it is not as changeful as the One part, so it's counted as a half one. This is how the name OPF comes.

OPF Programming is a generic programming paradigm. It can be used directly on languages like TypeScript/JavaScript, Lua, etc.

OPF Programming can be used on C++, Java and C#. But it would be strange since those languages have already supported concepts like interface.

The best platform for OPF Programming is the C programming languge.

The One

For a long time, the power of Modular Programming has been underestimated. It's buried in the ocean of computer terms.

Some people think they know what Modular Programming is, but actually they just know the descriptions on modular programming. They can't write their code in the modular way in daily programming.

Here is a simple program to demonstrate the benifits of modular programming:

int blah(int *result) {
    int v1 = 0;
    double v3 = 0;
    char v4 = 0;
    long v2 = 0;

    if (v1 > 10 && v2 > 2*10 && v2 < 8*10) {
        if (v1 > 10)
            return 1;
        v2 /= 3;
        v1 += v1 * v2 + 3;
        if (v1 > 10)
            return 1;
    }

    if (v3 > 2 && (v3 * v4 > v3 / v4 - 5)) {
        if (v3 < 0 || v3 > v1)
            return 2;
        v3 += v4 * 3.1415926 + 3;
        if (v3 > 10)
            return 2;
    }

    *result = (v1 + v1 * v2 - v2) / (v3 / v4 - v4);

    return 0;
}

This piece of code is hard to read. We can not know the relations among those variables. And it's hard to figure out the logic what those operations mean.

We can only rely on comments written by the author.

But comments is not the best solution since they are not part of the code. It can be a disaster if we changed the code and forgot to change the comments (which is common).

If we write our code in a more modular way, it may look like this:

int blah(int *result) {
    struct d1 e1 = {0, 0};
    struct d2 e2 = {0, 0};

    if (d1_check_with(&e1, 10)) {
        if (d1_fix(&e1))
            return 1;
    }

    if (d2_check_with(&e2, 5)) {
        if (d2_fix(&e2, e1.v1))
            return 2;
    }

    *result = d1_generate(&e1) / d2_generate(&e2);

    return 0;
}

Now it's much easier to understand the logic.

Data which used to be mixed together are splited into different structs now:

struct d1 {
    int v1;
    long v2;
}
struct d2 {
    double v3;
    int v4;
}

And the related logic are splited into different logic unit, too:

int d1_check_with(struct d1 *self, int n) {
    return v1 > n && v2 > n*2 && v2 < n*8;
}

int d1_fix(struct d1 *self) {
    if (self->v1 > 10)
        return 1;

    self->v2 /= 3;
    self->v1 += self->v1 * v2 + 3;

    if (self->v1 > 10)
        return 2;

    return 0;
}
int d2_check_with(struct d2 *self, int offset) {
    return v3 > 2 && (v3 * v4 > v3 / v4 - offset);
}

int d2_fix(struct d2 *self, int v1) {
    if (self->v3 < 0 || self->v3 > v1)
        return 1;

    self->v3 += self->v4 * 3.1415926 + 3;

    if (self->v3 > 10)
        return 2;

    return 0;
}

This example is simple, but we can understand the benifits of modular programming to some extent.

The more complex the programs are, the more powerful modular programming can be.

There are something in the world which looks huge, but when we understand it, it becomes tiny. There are something in the world which looks tiny, but when we understand it, it becomes huge.

Modular Programming is the later kind.

The Point Five

The skill part, Point Five, have already been printed into our LOGO: (*p)->f(p).

Before the explanations, we would like to show this graph first:

  VARIABLE           OBJECT             INTERFACE
  +--------+  .--->  +--------+  .--->  +--------+
  |     ---+--'      |     ---+--'      |   f1   |
  +--------+         +--------+         +--------+
                     |        |         |   f2   |
                     |        |         +--------+
                     |        |
                     |        |
                     +--------+

This graph has shown how (*p)->f(p) works.

The ultimate aim of this skill, is to provide a Top-Down programming way, which is the key to build huge program.

Simple programs are designed in the Bottom-Up style: We implement A, then implement B on A, then implement the final C on B.

But in the Top-Down style: We implement part of C, then someone else implement B, then B and C are combined as the final program.

Top-Down and Buttom-Up can be combined.

For example, the A-B part is Top-Down while the B-C part is Buttom-Up. Or the B-C part is Top-Down while the A-B part is Buttom-Up.

Let's use an example to explain it more clear:

Say, we want to write a set of function like draw_rect, draw_circle, which support different printing targets.

How to do that? If we have a generic draw_point, it would be easy to implement those function. So the problem got converted into:

How to implement a generic draw_point.

With the Point Five skill, we write a interface struct like this:

typedef int (*painter_draw_point_fn_t)(void *self, struct point pos, int color);

struct painter_i {
    painter_draw_point_fn_t draw_point;
};

Now, here comes the magical part: We can implement the drawing function without any real screen drivers!

int draw_rect(struct painter_i **painter, struct point p1, struct point p2, int color) {
    struct point c;

    /// ...

    for (/* get the next point to draw and store the point in c */) {

        /// (*p)->f(p)
        (*painter)->draw_point(painter, c, color);


    }

    /// ...

    return 0;
}

This is the Top-Down design: We implement the upper logic, then the lower logic.

Now let's implement a RGB screen driver:

struct rgb_screen {
    struct painter_i *interface;    // this field has to be the first field of rgb_screen.
    int color_type;
    int color_mask;
    /// ...
};
int rgb_screen_draw_point(struct rgb_screen *self, struct point pos, int color) {
    /// Draw point on the RGB screen, `self->color_type` and `self->color_mask` will be used here.
}

struct pointer_i rgb_screen_interface = {
    .draw_point = (painter_draw_point_fn_t)rgb_screen_draw_point,
};

int rgb_screen_init(struct rgb_screen *self) {
    self->interface = &rgb_screen_interface;
}

Then let's implement a mono screen driver:

struct mono_screen {
    struct painter_i *interface;    // this field has to be the first field of mono_screen.
    unsigned char bit_buffer[1024*768];
    /// ...
};
int mono_screen_draw_point(struct mono_screen *self, struct point pos, int color) {
    /// Draw point on the mono screen, `self->bit_buffer` will be used here.
}

struct pointer_i mono_screen_interface = {
    .draw_point = (painter_draw_point_fn_t)mono_screen_draw_point,
};

int mono_screen_init(struct mono_screen *self) {
    self->interface = &mono_screen_interface;
}

Now here is how we use the code:

struct rgb_screen scr1;
struct mono_screen scr2;

/// ...

draw_rect(&scr1, p1, p2, RED);
draw_rect(&scr2, p1, p2, WHITE);

draw_circle(&scr1, p1, 10, RED);
draw_circle(&scr2, p1, 10, RED);

You may feel OPF Programming a little bit complex, and don't understand why those things are done. But when we get more familiar with this skill, we can construct the whole thing inside our head without effort.

Rules and Styles

The core concept of OPF Programming do not have coding rules, but a good coding rule can make communication easier. After some practice, some simple rules are collected:

  1. Methods use their first parameters as the owner pointers, and the name self is suggested.
  2. Interface structs should be named with suffix _i. e.g. struct painter_i, struct iter_i, etc.
  3. Methods should be named as OWNER_METHOD. e.g. The add method of struct my_vec should be named my_vec_add.
  4. Function only return status code/error code, results are passed out though a pointer argument.

Explanations for the 4th Rule

Many C code return the result directly, and return error code through argument pointer.

e.g.

int fn(int a, int b, int *error) {
    if (some_check()) {
        *error = CODE_xxx;
        return SOMETHING;
    }

    return a + b;
}

But the OPF Programming use the following style:

int fn(int a, int b, int *result) {
    if (some_check())
        return CODE_xxx;

    *result = a + b;
    return 0;
}

Error code are returned, and result is return through argument pointer. Returning 0 means success.

Some coding styles return booleans, which means 1 means success, this is different from OPF Programming.

Iterator

The OPF Programming uses many iterators, the iterator itself is an application of OPF Programming, but we pick it out and describe it since it's widely used.

In OPF Programming, we only need to implement 2 interfaces: _iter_init and _iter_next.

When we use it, code should look like this:

int blah(void) {
    struct xxx_iter iter;
    struct element *tmp;
    size_t index;


    /// call _init to initialize iterator, we can't ignore the returning value since _init may fail.

    if (xxx_iter_init(&iter))
        return XXX_ITER_INIT_failed;


    /// Call `_next` to get the next element from the iterator.
    /// We can get both the element and the index through pointer arguments.

    //while (!xxx_iter_next(&iter, &tmp, NULL))
    while (!xxx_iter_next(&iter, &tmp, &index)) {
        if (do_something_on_element(tmp))
            return XXX_ITER_ELE_ERROR;
    }


    return 0;
}

Suggestions

The OPF Programming style is like Boxing in the programming paradigm world. It contains only a few rules, but we can solve huge problem by combing those simple rules.