[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("Michael");
    hello("Jenny");
    hello("Tom");
    

    當然,副常式要先預留好參數,外部程式才能傳參數進去。

    參數可加入預設值 (default value),使用者可自行決定是否要填入參數:

    sub hello($name = "World") {
        "Hello {$name}".say;
    }
     
    hello();
    hello("Michael");
    

    除了用固定位置參數外,也可以用命名參數 (named parameters),如下例:

    sub greet(:msg($g) = "Hello", :name($n) = "World") {
        "$g $n".say;
    }
     
    greet();
    greet(:msg("Goodbye"));
    greet(:name("Michael"));
    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";
    

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

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk