Files
2022-01-05 18:04:18 +01:00
..
2022-01-05 18:04:18 +01:00
2022-01-05 18:04:18 +01:00

Original source downloaded from Vintage Basic

Converted to D by Bastiaan Veelo.

Running the code

Assuming the reference dmd compiler:

dmd -dip1000 -run threedeeplot.d

Other compilers also exist.

On rounding floating point values to integer values

The D equivalent of Basic INT is floor, which rounds towards negative infinity. If you change occurrences of floor to lrint, you'll see that the plots show a bit more detail, as is done in the bonus below.

Bonus: Self-writing programs

With a small modification to the source, the program can be extended to plot a random function, and print its formula.

rdmd -dip1000 threedeeplot_random.d

(rdmd caches the executable, which results in speedy execution when the source does not change.)

Example output

                                    3D Plot
              (After Creative Computing  Morristown, New Jersey)


                           f(z) = 30 * sin(z / 10.0)

                             *
                      *      *    * *
                *         *      *    * *
                    *         *      *    * *
            *           *        *       *   *  *
               *           *         *      *   *  *
                  *           *         *     *    * **
       *             *           *        *      *   *  *
         *              *           *       *     *   *  **
            *              *          *       *    *   *  * *
              *              *          *      *   *   *  *  *
                *              *          *     *  *  *   *   **
                  *              *         *    * *  *   *    * *
   *                *             *        *    ** *    *     * *
    *                *             *        *  **     *      *   *
     *                 *            *       * *     *       *    *
      *                 *            *      * *   *         *    *
       *                *             *     ** *           *     **
        *                *            *     **            *      **
        *                *            *     *            *       **
        *                *            *     *            *       **
        *                *            *     *            *       **
        *                *            *     **            *      **
       *                *             *     ** *           *     **
      *                 *            *      * *   *         *    *
     *                 *            *       * *     *       *    *
    *                *             *        *  **     *      *   *
   *                *             *        *    ** *    *     * *
                  *              *         *    * *  *   *    * *
                *              *          *     *  *  *   *   **
              *              *          *      *   *   *  *  *
            *              *          *       *    *   *  * *
         *              *           *       *     *   *  **
       *             *           *        *      *   *  *
                  *           *         *     *    * **
               *           *         *      *   *  *
            *           *        *       *   *  *
                    *         *      *    * *
                *         *      *    * *
                      *      *    * *
                             *

Breakdown of differences

Have a look at the relevant differences between threedeeplot.d and threedeeplot_random.d. This is the original function with the single expression that is evaluated for the plot:

    static float fna(float z)
    {
        return 30.0 * exp(-z * z / 100.0);
    }

Here static means that the nested function does not need acces to its enclosing scope.

Now, by inserting the following:

    enum functions = ["30.0 * exp(-z * z / 100.0)",
                      "sqrt(900.01 - z * z) * .9 - 2",
                      "30 * (cos(z / 16.0) + .5)",
                      "30 - 30 * sin(z / 18.0)",
                      "30 * exp(-cos(z / 16.0)) - 30",
                      "30 * sin(z / 10.0)"];

    size_t index = uniform(0, functions.length);
    writeln(center("f(z) = " ~ functions[index], width), "\n");

and changing the implementation of fna to

    float fna(float z)
    {
        final switch (index)
        {
            static foreach (i, f; functions)
                case i:
                    mixin("return " ~ f ~ ";");
        }
    }

we unlock some very special abilities of D. Let's break it down:

    enum functions = ["30.0 * exp(-z * z / 100.0)", /*...*/];

This defines an array of strings, each containing a mathematical expression. Due to the enum keyword, this is an array that really only exists at compile-time.

    size_t index = uniform(0, functions.length);

This defines a random index into the array. functions.length is evaluated at compile-time, due to D's compile-time function evaluation (CTFE).

    writeln(center("f(z) = " ~ functions[index], width), "\n");

Unmistakenly, this prints the formula centered on a line. What happens behind the scenes is that functions (which only existed at compile-time before now) is pasted in, so that an instance of that array actually exists at run-time at this spot, and is instantly indexed.

    float fna(float z)
    {
        final switch (index)
        {
            // ...
        }
    }

static has been dropped from the nested function because we want to evaluate index inside it. The function contains an ordinary switch, with final providing some extra robustness. It disallows a default case and produces an error when the switch doesn't handle all cases. The switch body is where the magic happens and consists of these three lines:

            static foreach (i, f; functions)
                case i:
                    mixin("return " ~ f ~ ";");

The static foreach iterates over functions at compile-time, producing one case for every element in functions. mixin takes a string, which is constructed at compile-time, and pastes it right into the source.

In effect, the implementation of float fna(float z) unrolls itself into

    float fna(float z)
    {
        final switch (index)
        {
            case 0:
                return 30.0 * exp(-z * z / 100.0);
            case 1:
                return sqrt(900.01 - z * z) * .9 - 2;
            case 2:
                return 30 * (cos(z / 16.0) + .5);
            case 3:
                return 30 - 30 * sin(z / 18.0);
            case 4:
                return 30 * exp(-cos(z / 16.0)) - 30;
            case 5:
                return 30 * sin(z / 10.0)";
        }
    }

So if you feel like adding another function, all you need to do is append it to the functions array, and the rest of the program rewrites itself...