會畫畫的烏龜

Guile 是一種 Scheme 方言的編譯器,咱們將這種 Scheme 方言也稱爲 Guile。Guile 是爲加強 GNU 項目的擴展性而開發的。GNU 項目開發者能夠將 Guile 解釋器嵌入本身的程序中,從而使得本身的程序可以支持腳本擴展。本文取材於 Guile 官方的一篇教程,講述一個具備繪圖功能的 C 程序如何與 Guile 結合以得到腳本擴展能力。html

線性插值

兩點肯定一條直線。假設直線 $C$ 過 $P$ 與 $Q$ 兩點,其參數方程爲:linux

$$C(t) = P + t(Q-P)$$面試

上述方程可變形爲:redis

$$C(t) = (1-t)P + tQ$$shell

這就是線性插值公式。編程

gnuplot

可使用 gnuplot 將線性插值的結果顯示出來。gnuplot 是一款命令行交互式繪圖軟件。用它能夠繪製二維與三維的數據或函數圖形,也能夠用於解決一些數值分析問題,例如曲線/曲面逼近方面的問題。數組

若是系統是 Linux,而且已安裝了 gnuplot,在終端中輸入 gnuplot 命令即可進入 gnuplut 命令式交互繪圖環境:dom

$ gnuplot

    G N U P L O T
    Version 5.0 patchlevel 3 (Gentoo revision r0)    last modified 2016-02-21 

    Copyright (C) 1986-1993, 1998, 2004, 2007-2016
    Thomas Williams, Colin Kelley and many others

    gnuplot home:     http://www.gnuplot.info
    faq, bugs, etc:   type "help FAQ"
    immediate help:   type "help"  (plot window: hit 'h')

Terminal type set to 'x11'
gnuplot>

gnuplot 可以繪製參數方程的圖形,它所接受的參數方程是基於維度份量的拆分形式。例如,要繪製過點 $P(0.0, 0.0)$ 與 $Q(2.71, 3.14)$ 的直線,可以使用下面這條繪圖命令:編程語言

plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14

不過,當你在 gnuplot 命令式交互繪圖環境中輸入上述繪圖命令時,gnuplot 會抱怨:函數

undefined variable: t

這是由於 gnuplot 默認開啓的是非參數方程形式的繪製模式。使用 set parametric 命令開啓參數方程模式,而後即可基於參數方程繪製圖形:

set parametric
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14

結果以下圖所示:

線性插值結果的可視化

set parametric 規定,對於單參數方程(可表示曲線),參數爲 t,而對於雙參數方程(可表示曲面),參數爲 uv。注意,set parametric 命令只需使用一次,後續的 plot 命令便都以參數方程模式繪製圖形。也就是說,每次使用 plot 命令繪圖時,不須要重複執行 set parametric

gnuplot 默認開啓了圖例說明,即位於圖框內部右上方的文字與圖例。若是不須要它,能夠在 plot 命令中經過參數 notitle 將其關閉:

plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle

結果以下圖所示:

關閉圖例說明

也許你已經注意到了,圖框的實際寬高比(並不是圖框上的標稱寬高比)與窗口的寬高比相等,這是 gnuplot 的默認設定。這意味着,當你拉長或圧扁窗口,圖框也會相應的被拉長或圧扁。可以使用 set size ratio -1 命令將圖框的寬高比限定爲標稱寬高比:

set size ratio -1
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle

結果以下圖所示:

圖框的標稱寬高比

圖框上標記的座標刻度 gnuplot 自動生成的,若是咱們想限定橫向與縱向的座標範圍,例如限定在 [-5, 5] 區間,可以使用 set [x|y]range 命令:

set xrange [-5:5]
set yrange [-5:5]
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle

結果以下圖所示:

限定座標範圍

若但願繪製的是以 $P(0.0, 0.0)$ 與 $Q(2.71, 3.14)$ 爲端點的直線段,可經過調整參數 t 的取值範圍來實現:

set trange [0:1]
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle

結果以下圖所示:

限定參數範圍

上面的示例中,只繪製了一條直線。要是連續使用 plot 繪製兩條不一樣的直線會怎樣?例如:

plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle
plot (1-t)*0.0 + t*3.14, (1-t)*0.0 + t*2.71 notitle

結果只顯示第 2 條 plot 命令的繪圖結果。由於 gnuplot 默認會讓新的 plot 命令會刷掉舊的 plot 命令的繪圖結果。要想實現多條 plot 命令繪圖結果的疊加,須要使用 set multiplot 命令開啓圖形疊加模式:

set multiplot
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle
plot (1-t)*0.0 + t*3.14, (1-t)*0.0 + t*2.71 notitle

結果以下圖所示:

圖形疊加

要在限定橫向與縱向座標範圍,而且限定參數範圍的狀況下繪製無圖例說明的疊加圖形,所需的繪圖命令彙總以下:

set multiplot
set parametric
set size ratio -1
set xrange [-5:5]
set yrange [-5:5]
set trange [0:1]
plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle
plot (1-t)*0.0 + t*3.14, (1-t)*0.0 + t*2.71 notitle

管道

若是將上一節最後給出的那段 gnuplot 命令存放在一份文件中, 例如 foo.gp,那麼經過管道,將 foo.gp 中的內容傳遞給 gnuplot,結果會發生什麼?

$ cat foo.gp | gnuplot

結果會出現一個轉瞬即逝的繪圖窗口。

要想讓這個繪圖窗口持久的存在,要麼使用下面的命令:

$ cat foo.gp | gnuplot --persist

要麼就在 foo.gp 文件的首部增長如下命令:

set terminal x11 persist

而後:

$ cat foo.gp | gnuplot

在 C 程序中,也能夠藉助多進程編程與管道通訊技術,將繪圖命令傳遞於 gnuplot:

/* foo.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main(int argc, char **argv) {
        int plot_pipe[2];
        pipe(plot_pipe);
        if (fork() == 0) {
                close(plot_pipe[1]);
                dup2(plot_pipe[0], STDIN_FILENO);
                execlp("gnuplot", NULL, NULL);
        } else {
                char *cmds = "set terminal x11 persist\n"
                             "set multiplot\n"
                             "set parametric\n"
                             "set size ratio -1\n"
                             "set xrange [-5:5]\n"
                             "set yrange [-5:5]\n"
                             "set trange [0:1]\n"
                             "plot (1-t)*0.0 + t*2.71, (1-t)*0.0 + t*3.14 notitle\n"
                             "plot (1-t)*0.0 + t*3.14, (1-t)*0.0 + t*2.71 notitle\n";
                close(plot_pipe[0]);
                FILE *output = fdopen(plot_pipe[1], "w");
                fprintf(output, "%s", cmds);
                fflush(output);
        }
        exit(EXIT_SUCCESS);
}

上述代碼中的 else 分支中的代碼,至關於 cat foo.gp | gnuplot 中的 cat foo.gp 部分,而 if 分支中的代碼則至關於 gnuplot 部分。之因此能出現這種奇異的效果,歸功於 fork 函數。

fork 函數能夠從當前正在運行的程序(主進程)中分裂出一個新的正在運行的程序(新進程),這個過程有點像細胞的分裂。對於新進程,fork 函數返回值爲 0,而對於主進程,fork 函數的返回值是那個分裂出來的新進程的 ID。因爲咱們的程序中沒有用到新進程的 ID,因此這個問題就很少說了。若對這個話題感興趣,能夠去找 Linux 多進程編程的資料來看。

新進程經過 execlp 函數開啓了 gnuplot 進程,而後它就死了,gnuplot 進程取代了它。gnuplot 進程等待咱們向它輸入繪圖命令。可是,咱們的主進程與 gnuplot 進程彼此獨立,兩者須要一種通訊機制來傳遞信息。這種通訊機制就是管道。

pipe 函數建立管道。在上例中,plot_pipe 數組即是管道,plot_pipe[0] 是其輸入端,plot_pipe[1] 是其輸出端。在主進程中,咱們向 plot_pipe[1] 寫入繪圖命令,而 gnuplot 進程則經過讀取 plot_pipe[0] 來得到繪圖命令。因爲主進程用不到 plot_pipe[1],因此須要將其關閉。同理,gnuplot 進程也用不到 plot_pipe[0],因此也須要將其關閉。

dup2 函數用於文件重定向。dup2(plot_pipe[0], STDIN_FILENO) 表示將管道的輸入端重定向到系統的標準輸入文件(即 stdin)。因爲 gnuplot 具有從標準輸入文件中獲取信息的能力,因此這一切很是默契。

編譯並運行這個 C 程序的命令以下:

$ gcc foo.c -o foo
$ ./foo

烏龜

這是一隻會畫畫的烏龜,它爬行的軌跡就是它畫的畫。這個梗來自早期的一種面向兒童的編程語言——LOGO 語言。孩子們能夠經過程序控制一隻烏龜的運動,讓它畫出圖案。如今,咱們能夠用 C 編寫一個會畫畫的烏龜程序,所用的技術與工具在上文中都已經提到了。這真是個冗長的開始,直到此處,咱們依然未觸及本文的主題。

首先定義烏龜的活動空間:

typedef struct {
        FILE *plot_pipe;
        double west;
        double east;
        double south;
        double north;
} Land;

static Land *
init_land(double west, double east, double south, double north) {
        int tube[2];
        pipe(tube);
        if (fork() == 0) {
                close(tube[1]);
                dup2(tube[0], STDIN_FILENO);
                execlp("gnuplot", NULL, NULL);
                return NULL;
        } else {
                close(tube[0]);
                Land *land = malloc(sizeof(Land));
                land->east = east;
                land->west = west;
                land->south = south;
                land->north = north;
                land->plot_pipe = fdopen(tube[1], "w");
                char *cmds = "set terminal x11 persist\n"
                             "set multiplot\n"
                             "set size ratio -1\n"
                             "set parametric\n"
                             "set trange [0:1]\n";
                assert(land->plot_pipe);
                fprintf(land->plot_pipe, "%s", cmds);
                fprintf(land->plot_pipe, "set xrange [%lf:%lf]\n", west, east);
                fprintf(land->plot_pipe, "set yrange [%lf:%lf]\n", south, north);
                fflush(land->plot_pipe);
                return land;
        }
}

而後定義烏龜:

typedef struct {
        double x;
        double y;
        double direction;
        Land *land;
} Tortoise;

static Tortoise *
tortoise_alloc(Land *land) {
        Tortoise *t = malloc(sizeof(Tortoise));
        t->x = t->y = t->direction = 0.0;
        t->land = land;
        return t;
}

static void
tortoise_reset(Tortoise *self) {
        self->x = self->y = self->direction =  0.0;
}

xy 表示烏龜在 Land 中的位置。direction 表示烏龜前進的方向。land 指向烏龜的活動空間。

烏龜只須要用上文提到的線性插值方法就能夠在 gnuplot 圖框內繪製出它的行走軌跡。只要給出烏龜爬行軌跡上的兩個點,即可用線性插值的辦法,經過一組首尾相接的直線段描繪出烏龜的爬行軌跡。咱們將最基本的繪圖操做定義爲 draw_line 函數:

static void
draw_line(Land *land, double x0, double y0, double x1, double y1) {
        FILE *output = land->plot_pipe;
        if (x0 < land->west || x0 > land->east) return;
        if (y0 < land->south || y0 > land->north) return;
        if (x1 < land->west || x1 > land->east) return;
        if (y1 < land->south || y1 > land->north) return;
        fprintf (output,
                 "plot [0:1] (1-t) * %lf + t * %lf, (1-t) * %lf + t * %lf notitle\n",
                 x0, x1, y0, y1);
        fflush (output);
}

下面代碼定義了烏龜的一些基本行爲:

static void
tortoise_reset(Tortoise *self) {
        self->x = self->y = self->direction =  0.0;
}

static void
tortoise_turn(Tortoise *self, double degree) {
        self->direction += M_PI / 180.0 * degree;
}

static void
tortoise_forward(Tortoise *self, double distance, bool to_mark) {
        double newX, newY;
        newX = self->x + distance * cos (self->direction);
        newY = self->y + distance * sin (self->direction);
        if (to_mark) {
                draw_line (self->land, self->x, self->y, newX, newY);
        }
        self->x = newX;
        self->y = newY;
}

下面試試這個烏龜能不能勝任畫圖的任務:

static unsigned int
generate_random_seed_in_linux(void) {
         unsigned int seed;
         FILE *fs_p = fopen("/dev/urandom", "r");
         fread(&seed, sizeof(unsigned int), 1, fs_p);
         fclose(fs_p);
         return seed;
}
int
main(void) {
        double r = 1000.0;
        Land *land = init_land(-r, r, -r, r);
        Tortoise *t = tortoise_alloc(land);
        /* 讓烏龜隨機爬行 */{
                tortoise_turn(t, 180.0);
                tortoise_forward(t, 1000, false);
                tortoise_turn(t, -180.0);
                srand(generate_random_seed_in_linux());
                double old_direction = 90.0;
                for (int i = 0; i < 200; i++) {
                        double direction = rand() % 180;
                        tortoise_forward(t, 30.0, true);
                        tortoise_turn(t, direction - old_direction);
                        old_direction = direction;
                }
        }
        free(t);
        fclose(land->plot_pipe);
        return 0;
}

要讓上述代碼經過編譯,須要包含如下頭文件:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#include <assert.h>
#include <time.h>
#include <unistd.h>

編譯命令爲:

$ gcc -lm tortoise.c -o tortoise

程序運行結果相似下圖(受 gnuplot 渲染機制的限制,繪圖速度不是那麼快):

烏龜隨機爬行路線

C 程序與 Guile 的結合

上文咱們所作的事雖然有趣,但它僅僅是個冗長的前奏。如今剛開始步入正題,對於上一節所寫的 C 程序,如何將其與 Guile 相結合以得到腳本擴展能力。爲了便於清晰完整的呈現主題,如今假設 Land 裏只有一隻烏龜。也就是說,咱們將定義一個全局變量來表示這隻烏龜。

Tortoise *lonely_tortoise = NULL;

基於這個全局變量,就能夠將上一節所實現的 tortoise_resettortoise_turn 以及 tortoise_forward 這三個函數封裝爲更簡單的形式,使它們可以嵌入 Guile 環境:

static SCM
guile_tortoise_reset(void) {
        tortoise_reset(lonely_tortoise);
        return SCM_UNSPECIFIED;
}

static SCM
guile_tortoise_turn(SCM scm_degree) {
        double degree = scm_to_double(scm_degree);
        tortoise_turn(lonely_tortoise, degree);
        return SCM_UNSPECIFIED;
}

static SCM
guile_tortoise_forward(SCM scm_distance, SCM scm_to_mark) {
        double distance = scm_to_double(scm_distance);
        bool to_mark = scm_to_bool(scm_to_mark);
        tortoise_forward(lonely_tortoise, distance, to_mark);
        return SCM_UNSPECIFIED;
}

而後爲這三個函數登籍造冊,讓它們之後能接受 Guile 的管理:

static void *
register_functions_into_guile(void *data) {
        scm_c_define_gsubr("tortoise-reset", 0, 0, 0, guile_tortoise_reset);
        scm_c_define_gsubr("tortoise-turn", 1, 0, 0, guile_tortoise_turn);
        scm_c_define_gsubr("tortoise-forward", 2, 0, 0, guile_tortoise_forward);
        return NULL;
}

register_functions_into_guile 是一個回調函數,須要將其傳遞給 scm_with_guile 函數,才能完成上述 C 函數在 Guile 環境中的註冊:

scm_with_guile (&register_functions_into_guile, NULL);

一旦將 C 函數註冊到 Guile 環境,那麼在 Guile 解釋器運行期間,能夠在 Guile 解釋器或 Guile 腳本中使用這些函數的名字(例如,tortoise-forward)來調用它們。scm_shell 函數可用於在 C 程序中開啓 Guile 解釋器:

int
main(int argc, char **argv) {
        double r = 1000.0;
        Land *land = init_land(-r, r, -r, r);
        lonely_tortoise = tortoise_alloc(land);
        scm_with_guile(register_functions_into_guile, NULL);
        scm_shell(argc, argv);
        free(lonely_tortoise);
        fclose(land->plot_pipe);
        return 0;
}

上述代碼初始化了 land,生成了 lonely_tortoise 的實體,將用於表示烏龜的行爲的三個 C 函數註冊到了 Guile 環境,而後運行了 Guile 解釋器。

要讓上述代碼編譯經過,須要包含 Guile 庫的頭文件:

#include <libguile.h>

下面是完整的代碼:

/* guile-tortoise.c */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#include <assert.h>
#include <time.h>
#include <unistd.h>
#include <libguile.h>

typedef struct {
        FILE *plot_pipe;
        double west;
        double east;
        double south;
        double north;
} Land;

static Land *
init_land(double west, double east, double south, double north) {
        int tube[2];
        pipe(tube);
        if (fork() == 0) {
                close(tube[1]);
                dup2(tube[0], STDIN_FILENO);
                execlp("gnuplot", NULL, NULL);
                return NULL;
        } else {
                close(tube[0]);
                Land *land = malloc(sizeof(Land));
                land->east = east;
                land->west = west;
                land->south = south;
                land->north = north;
                land->plot_pipe = fdopen(tube[1], "w");
                char *cmds = "set terminal x11 persist\n"
                             "set multiplot\n"
                             "set size ratio -1\n"
                             "set parametric\n"
                             "set trange [0:1]\n";
                assert(land->plot_pipe);
                fprintf(land->plot_pipe, "%s", cmds);
                fprintf(land->plot_pipe, "set xrange [%lf:%lf]\n", west, east);
                fprintf(land->plot_pipe, "set yrange [%lf:%lf]\n", south, north);
                fflush(land->plot_pipe);
                return land;
        }
}

static void
reset_land(Land *land) {
        fprintf (land->plot_pipe, "clear\n");
        fflush (land->plot_pipe);
}

static void
draw_line(Land *land, double x0, double y0, double x1, double y1) {
        FILE *output = land->plot_pipe;
        if (x0 < land->west || x0 > land->east) return;
        if (y0 < land->south || y0 > land->north) return;
        if (x1 < land->west || x1 > land->east) return;
        if (y1 < land->south || y1 > land->north) return;
        fprintf (output,
                 "plot [0:1] (1-t) * %lf + t * %lf, (1-t) * %lf + t * %lf notitle\n",
                 x0, x1, y0, y1);
        fflush (output);
}

typedef struct {
        double x;
        double y;
        double direction;
        Land *land;
} Tortoise;

static Tortoise *
tortoise_alloc(Land *land) {
        Tortoise *t = malloc(sizeof(Tortoise));
        t->x = t->y = t->direction = 0.0;
        t->land = land;
        return t;
}

static void
tortoise_reset(Tortoise *self) {
        self->x = self->y = self->direction =  0.0;
}

static void
tortoise_turn(Tortoise *self, double degree) {
        self->direction += M_PI / 180.0 * degree;
}

static void
tortoise_forward(Tortoise *self, double distance, bool to_mark) {
        double newX, newY;
        newX = self->x + distance * cos (self->direction);
        newY = self->y + distance * sin (self->direction);
        if (to_mark) {
                draw_line (self->land, self->x, self->y, newX, newY);
        }
        self->x = newX;
        self->y = newY;
}

static unsigned int
generate_random_seed_in_linux(void) {
         unsigned int seed;
         FILE *fs_p = fopen("/dev/urandom", "r");
         fread(&seed, sizeof(unsigned int), 1, fs_p);
         fclose(fs_p);
         return seed;
}

/****************************************************************
 *                           to guile
 ****************************************************************/
Tortoise *lonely_tortoise = NULL;

static SCM
guile_tortoise_reset(void) {
        tortoise_reset(lonely_tortoise);
        return SCM_UNSPECIFIED;
}

static SCM
guile_tortoise_turn(SCM scm_degree) {
        double degree = scm_to_double(scm_degree);
        tortoise_turn(lonely_tortoise, degree);
        return SCM_UNSPECIFIED;
}

static SCM
guile_tortoise_forward(SCM scm_distance, SCM scm_to_mark) {
        double distance = scm_to_double(scm_distance);
        bool to_mark = scm_to_bool(scm_to_mark);
        tortoise_forward(lonely_tortoise, distance, to_mark);
        return SCM_UNSPECIFIED;
}

static void *
register_functions_into_guile(void *data) {
        scm_c_define_gsubr("tortoise-reset", 0, 0, 0, guile_tortoise_reset);
        scm_c_define_gsubr("tortoise-turn", 1, 0, 0, guile_tortoise_turn);
        scm_c_define_gsubr("tortoise-forward", 2, 0, 0, guile_tortoise_forward);
        return NULL;
}

int
main(int argc, char **argv) {
        double r = 1000.0;
        Land *land = init_land(-r, r, -r, r);
        lonely_tortoise = tortoise_alloc(land);
        scm_with_guile(register_functions_into_guile, NULL);
        scm_shell(argc, argv);
        free(lonely_tortoise);
        fclose(land->plot_pipe);
        return 0;
}

編譯上述代碼的命令爲:

$ gcc `pkg-config --cflags --libs guile-2.0` guile-tortoise.c -o guile-tortoise

運行編譯所得程序:

$ ./guile-tortoise
Copyright (C) 1995-2014 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
scheme@(guile-user)>

這個程序不只會爲你開啓一個 gnuplot 的繪圖窗口,同時也會進入 Guile 解釋器交互環境。在這個環境裏,可使用 Scheme 語言控制那隻孤獨的小烏龜進行繪圖。例如:

> (tortoise-forward 300 #t)
> (tortoise-turn 90)
> (tortoise-forward 300 #t)
> (tortoise-turn 90)
> (tortoise-forward 300 #t)
> (tortoise-turn 90)
> (tortoise-forward 300 #t)

上述這些重複的繪製『命令』,可在 gnuplot 繪圖窗口中交互繪製出一個矩形:

Guile 解釋器

複雜的行走

Guile 是個解釋器,它能夠解釋運行 Scheme 語言。若是你對 Scheme 有必定了解,那麼即可以用它寫腳本,用更復雜的邏輯來控制那隻孤獨的小烏龜繪製圖案。

下面這份腳本可控制小烏龜在不一樣方位繪製一些正多邊形(邊數較大時,近似爲圓):

;;;; circles.scm
(define (draw-polygon n r)
  (do ((i 0 (1+ i)))
      ((= i n))
    (begin
      (tortoise-forward (* r (sin (* 3.14159 (/ 1 n)))) #t)
      (tortoise-turn (/ 360.0 n)))))

(do ((i 0 (1+ i)))
    ((= i 36))
  (begin
    (tortoise-turn 10.0)
    (draw-polygon 30 800)))

用上一節生成的 guile-tortoise 程序解釋運行 circles.scm 腳本:

$ ./guile-tortoise circles.scm

這些正多邊形疊加到一塊兒,可展示出複雜的景象:

圓

下面這份 Scheme 腳本能夠繪製兩朵不一樣形狀的雪花:

;;;; snowflake.scm
(define (koch-line length depth)
  (if (zero? depth)
    (tortoise-forward length #t)
    (let ((sub-length (/ length 3))
          (sub-depth (1- depth)))
      (for-each (lambda (angle)
                  (koch-line sub-length sub-depth)
                  (tortoise-turn angle))
                '(60 -120 60 0)))))

(define (snowflake length depth sign)
  (let iterate ((i 1))
    (if (<= i 3)
      (begin
        (koch-line length depth)
        (tortoise-turn (* sign -120))
        (iterate (1+ i))))))

(tortoise-turn 90)
(tortoise-forward 250 #f)
(tortoise-turn -90)

(snowflake 800 3 1)
(tortoise-turn 180)
(snowflake 800 3 -1)

用 guile-tortoise 程序解釋運行 snowflake.scm 腳本:

$ ./guile-tortoise snowflake.scm

所得結果以下圖所示:

雪花

總結

對於編程的初學者而言,這篇文章應該是有趣的。它向你展現了,不須要多麼複雜的工具和編程技術,只需將功能較爲單一的組件經過某些特定的機制組合起來,即可獲得一個可以繪製二維圖形而且具有腳本擴展功能的程序。這是否是出乎意料?

從一開始,在 gnuplot 中交互繪圖,咱們須要瞭解許多 gnuplot 的知識方能繪製線性插值結果。接下來,咱們嘗試在 C 程序中經過管道,向 gnuplot 輸出繪圖命令,這樣咱們能夠很方便的使用 C 語言來操縱 gnuplot 了,並且咱們在 C 程序中還抽象出一隻會畫圖的小烏龜,經過控制小烏龜的爬行來繪製圖形。利用 C 程序操縱 gnuplot 當然可繪製複雜的圖案,可是每次要繪製新的圖形,不得不改寫並從新編譯 C 程序。最後,咱們在 C 程序中嵌入了 Guile 解釋器,而後用 Scheme 來編寫繪圖腳本,這樣能夠在保持 C 程序不變的狀況下,繪製出複雜的圖案。更有趣的是,在使用 Scheme 語言爲這個 C 程序編寫繪圖腳本時,咱們已經不以爲 gnuplot 的存在了。

不過,雖然經過嵌入 Guile 解釋器可以讓程序擁有腳本擴展功能,可是要用好這一功能,須要對 Scheme 語言有所瞭解。Scheme 語言很簡單,儘管要用它來構建實際的程序看起來困難重重,可是咱們能夠用它來寫一些腳本,逐步的掌握它。事實上,咱們學習任何一種編程語言,在開始時,用它寫實際的程序也是困難重重的。學習的過程就應該像文中的那隻孤獨的小烏龜那樣一步一步的前進,終有所成。

相關文章
相關標籤/搜索