位元詩人 [Raku] 程式設計教學:函數式程式設計 (Functional Programming)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

函數式程式設計 (functional programming) 是一種程式設計的模範 (paradigm),主要見於 Lisp 和 ML 家族語言。由於函數式程式易於平行化處理,近年來許多主流語言也吸收了一些函數式程式的概念。一些大數據框架,像是 Hadoop 和 Spark 等,也用到函數式程式的概念。本文介紹以 Raku 撰寫函數式程式。

比起傳統的指令式程式,函數式程式一開始比較難理解,如果讀者覺得過於困難,可以先略過這個章節。

函式物件

函數式程式的前提在於函式為第一級物件 (first-class object),簡單地說,函式可以做為值。由於函式可以做為值,函式可以做為其他函式的參數,也可以做為回傳值。

以下是實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. my $f = sub add-one($n) {
  2.     $n + 1;
  3. }
  4.  
  5. $f(3) == 4 or die "Wrong value";

或者是將現有的函式的參考傳入:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub square($n) {
  2.     $n * $n;
  3. }
  4.  
  5. my $f = □
  6. $f(3) == 9 or die "Wrong value";

也可以用以下較為簡略的寫法,算是一種語法糖:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. my $f = -> $x { $x + 1; };
  2.  
  3. $f(3) == 4 or die "Wrong value";

閉包

閉包 (closure) 是一種帶有狀態的 (stateful) 副程式。以下是實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub number {
  2.     my $n = -1;
  3.    
  4.     sub {
  5.         $n++;
  6.         $n;
  7.     }
  8. }
  9.  
  10. my $f = number();
  11.  
  12. $f() == 0 or die "Wrong value";
  13. $f() == 1 or die "Wrong value";
  14. $f() == 2 or die "Wrong value";

以下例子使用閉包生成費伯那西數列:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub fib {
  2.     my $a = 0;
  3.     my $b = 1;
  4.    
  5.     sub {
  6.         my $out = $a;
  7.         my $c = $a + $b;
  8.         $a = $b;
  9.         $b = $c;
  10.        
  11.         $out;
  12.     }
  13. }
  14.  
  15. my $f = fib();
  16.  
  17. my @arr = (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89);
  18.  
  19. loop (my $i = 0; $i < @arr.elems; $i++) {
  20.     $f() == @arr[$i] or die "Wrong value";
  21. }

以下實例用閉包生成質數數列:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub prime {
  2.     my $n = 1;
  3.     my $out;
  4.    
  5.     sub {
  6.         loop {
  7.             $n++;
  8.             my $is-prime = True;
  9.  
  10.             loop (my $i = 2; $i <= $n.sqrt; $i++) {
  11.                 if $n mod $i == 0 {
  12.                     $is-prime = False;
  13.                     last;
  14.                 }
  15.             }
  16.            
  17.             if $is-prime {
  18.                 $out = $n;
  19.                 last;
  20.             }
  21.         }
  22.        
  23.         $out;
  24.     }
  25. }
  26.  
  27. my $p = prime();
  28.  
  29. my @arr = (2, 3, 5, 7, 11, 13, 17, 19, 23);
  30.  
  31. loop (my $i = 0; $i < @arr.elems; $i++) {
  32.     $p() == @arr[$i] or die "Wrong value";
  33. }

以下例子將雜湊包在閉包中,做為簡易的快取:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub fib {
  2.     my %cache;
  3.     %cache{0} = 0;
  4.     %cache{1} = 1;
  5.    
  6.     sub f(Int $n where $n >= 0) {
  7.         if %cache{$n}:exists {
  8.             return %cache{$n};
  9.         }
  10.    
  11.         my $out = f($n - 1) + f($n - 2);
  12.         %cache{$n} = $out;
  13.    
  14.         $out;
  15.     };
  16. }
  17.  
  18. my $f = fib();
  19. $f(10).say;

高階函式

高階函式 (higher-order function) 是指使用函式做為參數或用函式做為回傳值的函式。我們先前的閉包也是一種高階函式。以下是一個以副程式為參數的實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub filter($f, @arr) {
  2.     my @out;
  3.    
  4.     for @arr -> $e {
  5.         if $f($e) {
  6.             @out.push($e)
  7.         }
  8.     }
  9.    
  10.     @out;
  11. }
  12.  
  13. my @even = filter { $_ mod 2 == 0 }, 1..10;
  14. @even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";

filter 副程式以一個陣列和一個副程式為參數,回傳符合該副程式的新陣列。

由於 Perl 6 已經將一些常見的高階函式寫好,我們不需要每次都從頭撰寫,以下是一些例子:

  • grep:傳入串列,回傳符合特定條件的串列
  • map:將串列經轉換後,回傳新串列
  • reduce:將串列縮減為純量
  • sort:依照特定條件排列後回傳新串列
  • zip:將兩個串列組合,回傳新串列

以下為實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. # grep
  2. (1, 2, 3, 4, 5).grep({ $_ mod 2 != 0 }) ~~ (1, 3, 5) or die "Wrong list";
  3.  
  4. # map
  5. (1, 2, 3, 4, 5).map({ $_ ** 2 }) ~~ (1, 4, 9, 16, 25) or die "Wrong list";
  6.  
  7. # reduce
  8. (1, 2, 3, 4, 5).reduce({ $^a + $^b }) == 15 or die "Wrong value";
  9.  
  10. # sort
  11. (4, 2, 5, 1, 3).sort({ $^b <=> $^a }) ~~ (5, 4, 3, 2, 1) or die "Wrong value";
  12.  
  13. # zip
  14. zip((1, 2, 3), ("a", "b", "c")) ~~ ((1, "a"), (2, "b"), (3, "c")) or die "Wrong list";

高階函數間可進一步組合,如下例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. my $n =
  2.     reduce { $^a + $^b },
  3.     map { $_ ** 2 },
  4.     grep { $_ mod 2 != 0 },
  5.         1..10;
  6.  
  7. $n == 1 ** 2 + 3 ** 2 + 5 ** 2 + 7 ** 2 + 9 ** 2 or die "Wrong value";

上例是將處理順序倒著寫,一開始會覺得較不易閱讀。也可以將其改為方法呼叫的方式,如下:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. my $n = (1..10)
  2.     .grep({ $_ mod 2 != 0 })
  3.     .map({ $_ ** 2 })
  4.     .reduce: { $^a + $^b };
  5.  
  6. $n == 1 ** 2 + 3 ** 2 + 5 ** 2 + 7 ** 2 + 9 ** 2 or die "Wrong value";

迭代器

Perl 6 提供 gather ... take 的語法,可用來撰寫迭代器。gather 區塊內每次碰到 take 時,都會跳出該區塊,並取出一個值,下一次迭代時,會跳回原先的位置,繼續走迴圈。

我們將先前的 filter 副程式改寫如下:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub filter($f, @arr) {
  2.     gather {
  3.         for @arr -> $e {
  4.             take $e if $f($e);
  5.         }
  6.     }
  7. }
  8.  
  9. my @even = filter { $_ mod 2 == 0 }, 1..10;
  10. @even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";

這樣子看起來和先前的寫法沒啥差異,不過,迭代器有 laziness 的特性,如以下實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub filter($f, @arr) {
  2.     gather {
  3.         for @arr -> $e {
  4.             take $e if $f($e);
  5.         }
  6.     }
  7. }
  8.  
  9. my @even = filter({ $_ mod 2 == 0 }, 1..Inf)[0..4];
  10. @even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";

由於 gather 區塊有 laziness 的特性,即使是無限長的串列,也是可以順利執行。如果用傳統的迴圈則會變成無窮迴圈,無法無法執行。

以下例子用 gather 重寫質數數列,可用更簡潔的語法取質數:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub prime {
  2.     my $n = 1;
  3.    
  4.     gather {
  5.         loop {
  6.             $n++;
  7.             my $is-prime = True;
  8.  
  9.             loop (my $i = 2; $i <= $n.sqrt; $i++) {
  10.                 if $n mod $i == 0 {
  11.                     $is-prime = False;
  12.                     last;
  13.                 }
  14.             }
  15.            
  16.             if $is-prime {
  17.                 take $n;
  18.             }
  19.         }
  20.     }
  21. }
  22.  
  23. # (2 3 5 7 11 13 17 19 23 29 31)
  24. prime[10] == 31 or die "Wrong value";

以下例子用 gather 區塊重寫費伯那西數列:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. sub fib {
  2.     my $a = 0;
  3.     my $b = 1;
  4.    
  5.     gather {
  6.         loop {
  7.             take $a;
  8.             my $c = $a + $b;
  9.             $a = $b;
  10.             $b = $c;
  11.         }
  12.     }
  13. }
  14.  
  15. # (0 1 1 2 3 5 8 13 21 34 55)
  16. fib[10] == 55 or die "Wrong value";

由此可見,take 不一定要在 gather 區塊的最尾端,可使程式寫法更靈活。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。