前言
函數式程式設計 (functional programming) 是一種程式設計的模範 (paradigm),主要見於 Lisp 和 ML 家族語言。由於函數式程式易於平行化處理,近年來許多主流語言也吸收了一些函數式程式的概念。一些大數據框架,像是 Hadoop 和 Spark 等,也用到函數式程式的概念。本文介紹以 Raku 撰寫函數式程式。
比起傳統的指令式程式,函數式程式一開始比較難理解,如果讀者覺得過於困難,可以先略過這個章節。
函式物件
函數式程式的前提在於函式為第一級物件 (first-class object),簡單地說,函式可以做為值。由於函式可以做為值,函式可以做為其他函式的參數,也可以做為回傳值。
以下是實例:
my $f = sub add-one($n) {
$n + 1;
}
$f(3) == 4 or die "Wrong value";
或者是將現有的函式的參考傳入:
sub square($n) {
$n * $n;
}
my $f = □
$f(3) == 9 or die "Wrong value";
也可以用以下較為簡略的寫法,算是一種語法糖:
my $f = -> $x { $x + 1; };
$f(3) == 4 or die "Wrong value";
閉包
閉包 (closure) 是一種帶有狀態的 (stateful) 副程式。以下是實例:
sub number {
my $n = -1;
sub {
$n++;
$n;
}
}
my $f = number();
$f() == 0 or die "Wrong value";
$f() == 1 or die "Wrong value";
$f() == 2 or die "Wrong value";
以下例子使用閉包生成費伯那西數列:
sub fib {
my $a = 0;
my $b = 1;
sub {
my $out = $a;
my $c = $a + $b;
$a = $b;
$b = $c;
$out;
}
}
my $f = fib();
my @arr = (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89);
loop (my $i = 0; $i < @arr.elems; $i++) {
$f() == @arr[$i] or die "Wrong value";
}
以下實例用閉包生成質數數列:
sub prime {
my $n = 1;
my $out;
sub {
loop {
$n++;
my $is-prime = True;
loop (my $i = 2; $i <= $n.sqrt; $i++) {
if $n mod $i == 0 {
$is-prime = False;
last;
}
}
if $is-prime {
$out = $n;
last;
}
}
$out;
}
}
my $p = prime();
my @arr = (2, 3, 5, 7, 11, 13, 17, 19, 23);
loop (my $i = 0; $i < @arr.elems; $i++) {
$p() == @arr[$i] or die "Wrong value";
}
以下例子將雜湊包在閉包中,做為簡易的快取:
sub fib {
my %cache;
%cache{0} = 0;
%cache{1} = 1;
sub f(Int $n where $n >= 0) {
if %cache{$n}:exists {
return %cache{$n};
}
my $out = f($n - 1) + f($n - 2);
%cache{$n} = $out;
$out;
};
}
my $f = fib();
$f(10).say;
高階函式
高階函式 (higher-order function) 是指使用函式做為參數或用函式做為回傳值的函式。我們先前的閉包也是一種高階函式。以下是一個以副程式為參數的實例:
sub filter($f, @arr) {
my @out;
for @arr -> $e {
if $f($e) {
@out.push($e)
}
}
@out;
}
my @even = filter { $_ mod 2 == 0 }, 1..10;
@even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";
此 filter
副程式以一個陣列和一個副程式為參數,回傳符合該副程式的新陣列。
由於 Perl 6 已經將一些常見的高階函式寫好,我們不需要每次都從頭撰寫,以下是一些例子:
grep
:傳入串列,回傳符合特定條件的串列
map
:將串列經轉換後,回傳新串列
reduce
:將串列縮減為純量
sort
:依照特定條件排列後回傳新串列
zip
:將兩個串列組合,回傳新串列
以下為實例:
# grep
(1, 2, 3, 4, 5).grep({ $_ mod 2 != 0 }) ~~ (1, 3, 5) or die "Wrong list";
# map
(1, 2, 3, 4, 5).map({ $_ ** 2 }) ~~ (1, 4, 9, 16, 25) or die "Wrong list";
# reduce
(1, 2, 3, 4, 5).reduce({ $^a + $^b }) == 15 or die "Wrong value";
# sort
(4, 2, 5, 1, 3).sort({ $^b <=> $^a }) ~~ (5, 4, 3, 2, 1) or die "Wrong value";
# zip
zip((1, 2, 3), ("a", "b", "c")) ~~ ((1, "a"), (2, "b"), (3, "c")) or die "Wrong list";
高階函數間可進一步組合,如下例:
my $n =
reduce { $^a + $^b },
map { $_ ** 2 },
grep { $_ mod 2 != 0 },
1..10;
$n == 1 ** 2 + 3 ** 2 + 5 ** 2 + 7 ** 2 + 9 ** 2 or die "Wrong value";
上例是將處理順序倒著寫,一開始會覺得較不易閱讀。也可以將其改為方法呼叫的方式,如下:
my $n = (1..10)
.grep({ $_ mod 2 != 0 })
.map({ $_ ** 2 })
.reduce: { $^a + $^b };
$n == 1 ** 2 + 3 ** 2 + 5 ** 2 + 7 ** 2 + 9 ** 2 or die "Wrong value";
迭代器
Perl 6 提供 gather
... take
的語法,可用來撰寫迭代器。gather
區塊內每次碰到 take
時,都會跳出該區塊,並取出一個值,下一次迭代時,會跳回原先的位置,繼續走迴圈。
我們將先前的 filter
副程式改寫如下:
sub filter($f, @arr) {
gather {
for @arr -> $e {
take $e if $f($e);
}
}
}
my @even = filter { $_ mod 2 == 0 }, 1..10;
@even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";
這樣子看起來和先前的寫法沒啥差異,不過,迭代器有 laziness 的特性,如以下實例:
sub filter($f, @arr) {
gather {
for @arr -> $e {
take $e if $f($e);
}
}
}
my @even = filter({ $_ mod 2 == 0 }, 1..Inf)[0..4];
@even ~~ (2, 4, 6, 8, 10) or die "Wrong array {@even}";
由於 gather
區塊有 laziness 的特性,即使是無限長的串列,也是可以順利執行。如果用傳統的迴圈則會變成無窮迴圈,無法無法執行。
以下例子用 gather
重寫質數數列,可用更簡潔的語法取質數:
sub prime {
my $n = 1;
gather {
loop {
$n++;
my $is-prime = True;
loop (my $i = 2; $i <= $n.sqrt; $i++) {
if $n mod $i == 0 {
$is-prime = False;
last;
}
}
if $is-prime {
take $n;
}
}
}
}
# (2 3 5 7 11 13 17 19 23 29 31)
prime[10] == 31 or die "Wrong value";
以下例子用 gather
區塊重寫費伯那西數列:
sub fib {
my $a = 0;
my $b = 1;
gather {
loop {
take $a;
my $c = $a + $b;
$a = $b;
$b = $c;
}
}
}
# (0 1 1 2 3 5 8 13 21 34 55)
fib[10] == 55 or die "Wrong value";
由此可見,take
不一定要在 gather
區塊的最尾端,可使程式寫法更靈活。