副程式


簡單的副程式

如果你發現程式當中, 有些五六行以上, 類似的程式片段重複出現好幾次, 就表示你的程式可能可以改進。 把類似的程式片段寫成一個 subroutine 副程式function 函數, 把每次出現時小有變化的部分寫成副程式的 parameter/argument 參數/引數, 這樣就可用一句簡單的副程式呼叫來取代原來整個片段出現的部分。 這樣做有許多好處:

  1. 比較容易除錯 -- 改正副程式內的一個錯誤, 就等於同時改正好幾處的錯誤。
  2. 比較容易增加功能/改善效率 -- 改進副程式內一處的功能, 就等於同時改進好幾處的功能。
  3. 程式變得比較短, 比較漂亮。

試想: 如果一段程式碼重複出現三次, 以後或許也很可能再出現。 如果出現在不同的程式檔裡面, 就更麻煩了: 改進了一處的錯誤/功能, 很可能忘記改另一處。 如果寫成副程式, 不僅可以避免這種狀況, 也可以少打很多字。

執行結束後, 把運算結果傳回呼叫者的, 稱為函數; 純粹用以產生副作用 (例如列印/存檔/播放聲音/...) 沒有傳回值的, 稱為副程式。 在 perl 裡面, 兩者並無特殊分別, 唯一的差別是 return 敘述後面有沒有東西。 我們看一個簡單的 perl 副程式:


        sub sum {
            my ($x, $y) = @_;
            return $x + $y;
        }

Perl 的副程式, 用 sub 定義。 這裡的 sum 是自己任意取的副程式的名字。 Perl 的副程式, 所有參數一律靠 @_ 傳遞。 一般的 perl 副程式, 第一句話就是宣告幾個局部變數, 從 @_ 把參數接收過來。 這樣做有兩個用意:

  1. 參數有名字, 程式比較容易閱讀。
  2. 因為 perl 的參數傳遞是 call-by-reference, 這麼做可以避免不小心更改到呼叫者的變數。 後詳。

進入副程式之後, 就可以將先前學過的, 你熟悉的語法應用上來, 隨便你要算什麼。 我們這個副程式太簡單, 什麼都沒有算。 通常最後一句話是 return, 也就是將值傳回呼叫者去。 然後你就可以在程式其他地方使用這個副程式, 像這樣: print "5 + 3 is ", sum(5,3), "\n"; 請將這兩小段程式碼剪貼到你的編輯器上, 立刻試試看。 舊的 perl 程式, 呼叫副程式時前面要加一個 &, 像這樣: print "5 + 3 is ", & sum(5,3), "\n";。 Q: 如果呼叫 sum(5,3,2) 會發生什麼事?

Perl 不檢查參數的個數, 也不檢查參數的形態。 如果想將 sum 改成可以對任意個數字加總, 可以用一個陣列來接收參數:


        sub sum_all {
            my (@numbers) = @_;
            my ($n, $total);

            foreach $n (@numbers) {
                $total += $n;
            }
            return $total;
        }

從呼叫者的角度來看, 變得很有彈性, 想傳幾個數字進去都可以:


        @data = (2, 3, 5, 7, 11, 13, 17, 19);
        printf "sum_all(\@data) = %d\n", sum_all(@data);
        printf "sum_all(5, 8, \@data) = %d\n", sum_all(5, 8, @data);
        printf "sum_all(\@data, 97, 53, \@data) = %d\n",
            sum_all(@data, 97, 53, @data);

  

Q: 這裡的倒斜線是什麼意思? 提示: 不是 "...的位址"; 實驗一下, 將它拿掉就知道。 它出現在 "..." 裡面, 意義及效果比較像是 " ... \$20 ..."。

你可以傳幾個數字進去, 也可以傳一個陣列, 或是將陣列與純量混著傳。 這一切其實不過就是先前 詳談變數 裡面所提到的規則: list 沒有層次。 同理, 有關 hash 與 list 互相轉換的規則也適用, 例如你可以在一個副程式裡面用一個 hash 來接收參數:


        sub print_like_hash {
            my (%data) = @_;
            my ($key);

            foreach $key (keys %data) {
                print "$key: $data{$key}\n";
            }
        }

  

於是呼叫時可以寫成像是 hash 在設定初始值一樣: print_like_hash("Jan"=>31, "Feb"=>28, "Mar"=>31, "Apr"=>30); 如先前所述, => 其實不過就是逗點, 傳進去的東西不過就是一個 list, 它仍舊被放入 @_ 這個陣列當中; 只不過後來被複製到一個 hash 裡面去了。

文字模式遊戲函式庫

ANSI escape sequence 可以在文字模式下製造特效, 例如移動遊標, 改變顏色等等。 它不受限於一套作業系統, 也不受限於一種程式語言。 瞭解如何在 shell 底下直接手動用 ANSI escape sequence 製造特效後, 就可以自己寫一個簡單的函式庫 sitio

函式庫的最後一句話通常是 1; (其實只要是有定義, 且非 0 非空字串的值就可以了, 總之要傳回 true。)

如何引用函式庫? 在主程式檔案裡面放一句: require "sitio"; 之後就可以任意使用 sitio 提供的那些副程式了。 不過我們先做一個錯誤示範: 請故意將 require 後面的檔案名稱打錯, 或故意將 sitio 放在其他目錄, 總之就是要讓主程式找不到函式庫。 錯誤訊息 說什麼呢?


以下尚未整理


  1. 程式範例:
    1. 15puzzle: 智慧盤遊戲. 用到 sitio 副程式庫.
    2. knight: 騎士走棋盤. 用到 sitio 副程式庫. 註解請參考 C 版本.
  2. 傳入不定個數的參數; 傳出不只一個結果:
    1. 既然接收參數的是 @_ 這個陣列, 就表示在呼叫副程式時, 可以傳入任意個數的參數, 甚至可以把整個陣列或陣列與純量的組合一起傳入.
    2. 在副程式內用 my ( ... ) = @_; 取得參數時, my ( ... ) 的最後可以是一個陣列變數. 它會把 @_ 剩下的所有元素全部吃進去.
    3. 上述各點可以類推至傳回值: 例如在 15puzzle 中, 用 ($row, $col) = & blankpos( ... ) 來呼叫副程式, 而副程式 blankpos 中使用 return ($r, $c) 則可以想像 perl 自動做了: ($row, $col) = ($r, $c) 其中 assignment 左右都可以是 array 或 hash. 也就是說, 傳回值可以不只是一個純量. 但要注意若等號左邊只有一個變數, 小括弧還是不能省略, 否則會接收到最後一個元素! 請試試看這個簡單的程式片段: ($x) = (1,2,3,4); 看看 $x 變成多少? 再把 $x 外面的小括弧拿掉, 看看 $x 又變成多少?
  3. 仔細探究 perlsub(1), 發覺 perl 副程式的參數傳遞其實是 call-by-reference! 如果在副程式當中不以 my 複製參數, 而是直接在 @_ 上操作, 則有機會修改到主程式中傳進來的變數. 所以這個副程式可以用來交換兩個變數的內容:
            sub swap {
                ($_[0], $_[1]) = ($_[1], $_[0]);
            }
        
    
    (但傳回值則沒有這麼複雜.)
  4. 自建程式庫:
    1. 在你的 perl 程式當中先下 require "Xyz.pl", 之後 就可以使用 Xyz.pl 這個副程式庫內定義的副程式.
    2. 如果 perl 找不到你的副程式庫檔放在那個目錄底下, 可以在 require 一句的檔名當中加上完整的路徑 (像這樣: require "./Xyz.pl";) 或在 require 之前先下 use lib ... 以修改 @INC 變數的內容 (詳見 perlvar(1)), 或在 perl 命令列上指定 -I 選項 (詳見 perlrun(1)). 請實驗一下, 看看找不到副程式時印出來的錯誤訊息是什麼。
  5. Recursion (遞迴) 及 activation record
  6. 其他常識:
    1. formal argument/parameter 形式參數: 在副程式的 definition (定義) 的參數列當中出現的東西
    2. actual argument/parameter 實際參數: 在呼叫副程式 (invocation) 的地方出現
    3. "問號-冒號" 是一個三元運算子 (它接受三個參數). A ? B : C 的運算結果可能是 B 也可能是 C. 究竟是 B 還是 C 呢? 要看 A 這個條件是否成立. 如果成立, 則結果就是 B; 要不然結果就是 C.
    4. index 函數可以找子字串出現的位置; rindex 倒過來找; 與這兩個函數相反的是 substr, 可以取出指定位置的子字串.
    5. 一種有用的程式設計風格供參考: 能夠算出來的資訊, 盡量不要存入全域變數. (例如 15puzzle 中的空白位置) 目的不在節省空間, 而在減少資料修改時顧此失彼所造成的不一致 (inconsistence). 多花一點執行時間通常不是很大的問題.
    6. Perl 沒有真正的二維陣列, 但是可以用 hash 或陣列的陣列來模擬.
    7. 要測試一個變數內的值是否已有定義, 可用 defined; 要取消一個變數內的值 (讓它不是 0 也不是空字串, 更不是任何其他值, 就像已宣告但尚未給初始值一樣), 可用 undef.
  7. 作業:
    1. 請重寫 15puzzle, 改以陣列的陣列 (用 reference to anonymous array 的方式儲存) 來做出二維陣列的效果.
    2. 使用 sitio 當中的副程式, 寫一個圈叉棋的遊戲. 使用者可以用 hjkl 等四個鍵移動遊標, (但是你的程式不可以讓遊標移出 3x3 的棋盤範圍之外). 每按一次空間棒, 就打一個圈 (或打一個叉. 總之交替著做就是了), 並檢查是否有贏家出現. 棋盤不必畫得很漂亮, 但至少邊界要標示出來. 盡量在適當的場合使用副程式.

Perl 語言

  1. 新手上路
  2. 基本要素
  3. 餵資料
  4. 常用句型
  5. regexp
  6. 詳談變數
  7. 一語中的
  8. 副程式
  9. 模組
  10. 外界對話

附錄

  1. 參考資料
  2. scripting
  3. Windows
  4. 圖形介面
  5. big-5 碼