前言
副程式 (subroutine),或稱為函式 (function),是最小的可重用 (reusable) 程式碼區塊,也是物件導向程式的基礎。本文將介紹基本的副程式,對於進階的議題,將於後續文章中介紹。
使用副程式
我們先前已經使用過一些副程式了,如下:
say "Hello World";
在這裡,我們省略掉括號,實際上可以寫成:
say("Hello World");
當語義清楚時,省略括號較為美觀。Raku 不強迫程式人怎麼做,可依個人風格選擇。
由於 Raku 是物件導向語言,也可以使用方法呼叫 (method invocation) 的方式:
"Hello World".say;
這樣寫起來就有一些 Ruby 的感覺。
建立副程式
使用 sub
保留字來宣告副程式,如下:
sub hello {
"Hello World".say;
}
hello();
這個副程式沒有參數也沒有回傳值,實用性較低。後文會介紹改善的方式。
使用參數改變副程式的行為
我們可以傳入參數 (parameters),以改變副程式的行為:
sub hello($name) {
"Hello {$name}".say;
}
hello("Michelle");
hello("Jenny");
hello("Tom");
當然,副程式要先預留好參數,外部程式才能傳參數進去。
參數可加入預設值 (default value),副程式使用者可自行決定是否要填入參數:
sub hello($name = "World") {
"Hello {$name}".say;
}
hello();
hello("Michelle");
除了用固定位置參數外,也可以用命名參數 (named parameters),如下例:
sub greet(:msg($g) = "Hello", :name($n) = "World") {
"$g $n".say;
}
greet();
greet(:msg("Goodbye"));
greet(:name("Michelle"));
greet(:name("Jenny"), :msg("Hi"));
用命名參數的好處是不用記憶參數位置。在參數數量較多時,會比一般參數來得方便。
如果外部名稱和內部名稱相同,可再進一步簡化,如下例:
sub greet(:$msg = "Hello", :$name = "World") {
"$msg $name".say;
}
修改傳入的參數
一般來說,參數本身是不能修改的,這是為了程式的安全性著想。因此,以下程式會引發錯誤:
sub add-one($n) {
$n += 1;
$n;
}
my $n = 3;
add-one($n).say;
如果想要修改傳入的參數,可以加上 copy
的 trait,將參數複製一份,但不會修改原本的參數:
sub add-one($n is copy) {
$n += 1;
$n;
}
my $n = 3;
add-one($n) == 4 or die "Wrong value";
$n == 3 or die "Wrong value";
如果真的要修改參數本身,可加上 rw
的 trait,如下:
sub add-one($n is rw) {
$n += 1;
$n;
}
my $n = 3;
add-one($n) == 4 or die "Wrong value";
$n == 4 or die "Wrong value";
當我們使用 rw
trait 時,該函式會改變程式的狀態,我們說該函式具有副作用 (side effect)。由於有副作用的函式可能會造成一些不易發現的錯誤,應謹慎使用這項特性。
藉由回傳值取得函式運算的結果
副程式可以將運行結果回傳,預設會回傳副程式最後一行敘述:
sub add-one($n) {
$n + 1;
}
add-one(3) == 4 or die "Wrong value";
如果需要提早回傳值,可使用 return
:
sub is-odd(Int $n) {
if $n % 2 == 0 {
return False;
}
True;
}
is-odd(3) or die "Wrong status";
也可以藉由回傳串列回傳多個值:
sub divmod(Int $a, Int $b) {
$a div $b, $a mod $b;
}
my ($a, $b) = divmod(5, 3);
$a == 1 or die "Wrong value";
$b == 2 or die "Wrong value";
限定參數的型別
副程式可以選擇性地加入型態標註,避免程式傳入錯誤的值:
sub add-one(Int $n) of Int {
$n + 1;
}
add-one(3) == 4 or die "Wrong value";
雖然 Raku 是動態型態語言,但可選擇性地加入型態標註,做為一種防呆措施。
以陣列為副程式的參數
副程式也可以用陣列為參數,如下例:
sub add(@arr) {
my $sum = 0;
for @arr -> $e {
$sum += $e;
}
$sum;
}
my @arr = (1, 2, 3, 4, 5);
my $sum = add(@arr);
$sum == 15 or die "Wrong value";
如果副程式接受兩個以上的變數,可以把陣列攤平 (flatten) 後傳入,如下例:
sub add($x, $y) {
$x + $y;
}
my @arr = (3, 4);
my $sum = add(|@arr);
$sum == 7 or die "Wrong value";
如果副程式接收不定長度的變數,可以把變數吃入 (slurp),如下例:
sub add(*@args) {
my $s = 0;
for @args -> $e {
$s += $e;
}
$s;
}
my $sum = add(1, 2, 3, 4, 5);
$sum == 15 or die "Wrong value";
副程式重載
使用 multi
保留字可以宣告兩個以上同名但不同參數的副程式,實例如下:
multi congratulate($name) {
"Happy birthday, $name".say;
}
multi congratulate($name, $age) {
"Happy {$age}th birthday, $name".say;
}
congratulate('Larry');
congratulate('Bob', 45);
遞迴
遞迴是會呼叫自己的副程式,在程式設計中相當常見。階乘是一個例子:
sub fac(Int $n where $n >= 0) returns Int {
if $n == 0 or $n == 1 {
return 1;
}
$n * fac($n - 1);
}
fac(5) == 120 or die "Wrong value";
費伯那西數 (Fibonacci number) 也是一個常見的例子:
sub fib(Int $n where $n >= 0) returns Int {
if $n == 0 {
return 0;
} elsif $n == 1 {
return 1;
}
fib($n - 1) + fib($n - 2);
}
fib(10) == 55 or die "Wrong number";
具有狀態的副程式
使用 state
可保存副程式內部變數狀態,之後可再重覆呼叫,如下例:
sub fib(Int $n where $n >= 0) {
state %cache = 0 => 0, 1 => 1;
if %cache{$n}:exists {
return %cache{$n};
}
my $out = fib($n - 1) + fib($n - 2);
%cache{$n} = $out;
$out;
}
fib(10) == 55 or die "Wrong number";
在本例中,我們用內部雜湊當成快取,儲存運算過的結果,就可以省下重覆的遞迴呼叫所造成的開銷。