- 更新日: 2017年03月21日
- 公開日: 2016年08月16日
関数型プログラミングの威力を知ろう-後編
ここ数年で大きなパラダイムシフトを起こしている関数型プログラミングのさわりを知るシリーズの後編です。
前編では自動販売機をお題に、オブジェクト指向と関数型のプログラムを比較してみることで、考え方の違いを明確にしてみました。
後編では関数型プログラミングのスタイルを少し詳しく見ていきましょう。
大事な性質・関数の引数に関数を渡す
関数型プログラミングのもう1つの大事な性質として、もう1つ、「関数の引数に関数を渡す」というものがあります。
これは説明するよりサンプルプログラムを見た方が早いと思います。
関数型プログラミング言語としてHaskellを、オブジェクト指向プログラミング言語としてJava(JDK7以前)を使って、比較してみましょう。
配列の要素をコンソールに表示するプログラム
Java
class ArrayPrint { public static void main(String[] arg) { printArray(new String[] { "hoge", "foo", "bar" }); }static void printArray(String[] ary) { for (String s : ary) { System.out.println(s); } }
}
Haskell
main = mapM_ putStrLn ary ary = ["hoge", "foo", "bar"]
Haskellは、mapM_という関数の引数にputStrLnという関数を渡しています。これにより、たった1行で配列の要素をコンソールに表示するプログラムの完成です。
一方、Javaは原始的なループでプログラムを書いています。コード量の差は歴然ですね。この差は、Haskellの性質である「関数の引数に関数を渡せる」という点に起因しています。
ただ、実はJavaも、Java8から関数型プログラミングのエッセンスを取り入れ、メソッドの引数にメソッドを渡せるようになりました。これにより同じ効果のプログラムがとても簡単に書けるようになっています。
Java8
class ArrayPrint { public static void main(String[] arg) { printArray(new String[] { "hoge", "foo", "bar" }); } static void printArray(String[] ary) { Arrays.asList(ary).stream().forEach(System.out::println); } }## 集合の取り扱い・関数型ではループを書かない! 関数型プログラミングのスタイルの1つの特徴として、「**ループを書かずに済む**」というのがあります。 「ループを使わずにプログラムを書けるのか?」と思われる方もいらっしゃるかもしれません。しかし前項の例で見た通り、ループがなくても配列を扱うことは可能なのです。 配列操作のサンプルとして、全ての数値を足すプログラムを書いてみましょう。 Java(<8)
class AddAll { public static void main(final String[] arg) { System.out.println(addAll(new int[] { 1, 10, 20, 55 })); } static long addAll(final int[] ary) { long ret = 0; for (final int i : ary) { ret += i; } return ret; } }Haskell
main = print $ foldl (+) 0 ary ary = [1, 10, 20, 55]
Java8
class AddAll { public static void main(final String[] arg) { System.out.println(addAll(new int[] { 1, 10, 20, 55 })); }いかがでしょうか。関数型プログラミングのスタイルを使って書くと、かなりコード量が減ることが実感出来ると思います。static long addAll(final int[] ary) { return IntStream.of(ary).reduce(0, (left, right) -> { return left + right; }); }
}
関数型プログラミング言語の威力
これまでのサンプルでお分かりかと思いますが、Java8のプログラムの短さもかなりのものですが、Haskellはその遥か上を行く短さです。
Javaは、オブジェクト指向言語として進化してきた歴史から、どうしてもコードが多くなってしまうのですが、Haskellは全ての処理を関数で定義するという性質から、とてもコードが少なくなる傾向にあります。
Haskellはかなり極端な関数型プログラミング言語なので使いこなすのが大変ですが、ScalaやC#はうまく関数型プログラミングのエッセンスを取り入れていて、取っ付きやすさと省コードのバランスが取れているようです。
またJavaScriptは代表的な関数型プログラミング言語です。配列の要素を全て足すプログラムをJavaScriptで書いてみましょう。
const n = [1, 10, 20, 55].reduce((pre, cur) => { return pre + cur; }, 0); console.log(n);
reduceという関数に関数を渡すことでシンプルな記述を実現しています。
関数型プログラミングはなぜ重要か
関数型プログラミングには多くの利点があります。
コードが短くなる
コードが短くなるということは、間違いが少なくなるということです。
プログラムを書く上でバグを出さない唯一の方法はプログラムを書かないこと。コードが短いということは、それだけで品質が高くなるということなのです。
また同じ処理を実現するのに必要となるコードの量が少ないということは、単純に生産性が高いということです。
安全である
これは副作用のない関数を使ってプログラムを書く際の特徴です。
副作用がないということは「思わぬ状態(=変数)の変更が生じない」ということであり、これはとても安全なことです。
グローバル変数を操作するような影響範囲の広い処理は、プログラムの規模が大きくなるととても厄介な存在になります。正確な影響範囲を計ることが困難になるため、修正したくても出来なくなり、技術的負債になりがちです。
しかし関数を使って処理を書いていると、このようなことは生じ得ません。とても安心感を持ってプログラムを書くことが出来ます。
テストしやすい
関数の大事な性質は
- 入力が同じなら結果も同じ(結果が引数にしか影響を受けない)
というものでした。これはつまり、テストケースとして入力と出力の組み合わせだけを考えれば良い、ということであり、テストが簡単になります。
高速に動作する可能性がある
副作用がない関数は、並列実行に向いています。引数しか計算に使われないため、共通のリソース(グローバル変数など)に対するロックなどを考える必要がなくなるからです。
現在CPU性能の向上は鈍化しており、マルチコア化が進んでいます。複数のCPUに同時に処理をさせるのは、プログラムの動作速度向上の重要な手段であり、関数型プログラミングはこれに寄与する可能性があります。
その他の情報
関数プログラミングの重要性について、もっと深い議論を知りたい方は次の記事を見ていただきたいと思います。
\Webサイト担当者としてのスキルが身に付く/
まとめ
関数型プログラミングは、もはやトレンドを超え、スタイルとして確立されています。
特に「関数の引数に関数を渡す」というスタイルは、多くの言語に取り入れられ、とても重要なものとなっています。
ぜひ積極的に利用して、バグの少ないプログラムを書いていただきたいと思います。
- この記事を書いた人
- tomo