位元詩人 [Raku] 程式設計教學:副程式 (Subroutine)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

副程式 (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";

在本例中,我們用內部雜湊當成快取,儲存運算過的結果,就可以省下重覆的遞迴呼叫所造成的開銷。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。