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 是一款命令行交互式繪圖軟件。用它能夠繪製二維與三維的數據或函數圖形,也能夠用於解決一些數值分析問題,例如曲線/曲面逼近方面的問題。數組
若是系統是 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
,而對於雙參數方程(可表示曲面),參數爲 u
與 v
。注意,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; }
x
與 y
表示烏龜在 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 相結合以得到腳本擴展能力。爲了便於清晰完整的呈現主題,如今假設 Land 裏只有一隻烏龜。也就是說,咱們將定義一個全局變量來表示這隻烏龜。
Tortoise *lonely_tortoise = NULL;
基於這個全局變量,就能夠將上一節所實現的 tortoise_reset
,tortoise_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 (®ister_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 是個解釋器,它能夠解釋運行 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 語言很簡單,儘管要用它來構建實際的程序看起來困難重重,可是咱們能夠用它來寫一些腳本,逐步的掌握它。事實上,咱們學習任何一種編程語言,在開始時,用它寫實際的程序也是困難重重的。學習的過程就應該像文中的那隻孤獨的小烏龜那樣一步一步的前進,終有所成。