プログラミングには多様な形式やパラダイムがあるが、有名なものを2つ挙げるとしたら、関数型と命令型だろう。
命令型プログラミングは、ほとんど全ての主流言語(C++、Java、C#)が推し進めてきたこともあり、最も支配的パラダイムだ。しかし、ここ数年で関数型プログラミングが注目を集め始めている。その主な理由の一つは、単純に全ての新しいコンピュータが4、8、16コアを標準装備しており、命令型プログラミング形式では、全てのコアを活用して並列プログラムを書くことが非常に難しくなったことだ。関数型形式はこの困難な状況をランタイムに委ね、辛くてミスが起きやすい仕事から開発者を解放してくれる。
ちょっと待った!では、この2つの形式の違いは何なのだろうか?
命令型プログラミングとは、望む結果を得るために、マシンまたはランタイムがいかに正確に、どの正確な命令文を実行すべきかを命じるというパラダイムである。
関数型プログラミング とは、何を得たいかを伝え、マシンまたはランタイムがそのための最良の方法を決定するという、宣言型プログラミングのパラダイムの一種である。 関数型形式は方法の部分をランタイムに委ね、開発者が結果の部分に集中できるようにしてくれる。方法の部分を抽象化することで、より保守しやすくスケーラブルなソフトウェアを書くことができる。
マルチコアマシンの課題に対処し、開発者を依然として惹きつけ続けるために、Java8は命令型パラダイムの次に関数型パラダイムを導入した。
理屈はここまでにして、違いを見るためにJavaを使い、何個かのプログラミング課題を、命令型と関数型形式でそれぞれやってみよう。
フィボナッチ数列:命令型vs関数型
(フィボナッチ数列は、1, 1, 2, 3, 5, 8, 13, 21, 34, …といった一連の数字だ。次の数字は、前の2つの数字を足していくことで求められる)
反復および命令型形式でのフィボナッチ数列
public static int fibonacci(int number) {
int fib1 = 1;
int fib2 = 1;
int fibonacci = fib1;
for (int i = 2; i < number; i++) {
fibonacci = fib1 + fib2;
fib1 = fib2;
fib2 = fibonacci;
}
return fibonacci;
}
for(int i = 1; i <= 10; i++) {
System.out.print(fibonacci(i) +" ");
}
// Output: 1 1 2 3 5 8 13 21 34 55
ご覧のとおり、何をしたいか(結果)よりも、方法(反復、状態)により焦点を当てている。
反復および関数型形式でのフィボナッチ数列
IntStream fibonacciStream = Stream.iterate(
new int[]{1, 1},
fib -> new int[] {fib[1], fib[0] + fib[1]}
).mapToInt(fib -> fib[0]);
fibonacciStream.limit(10).forEach(fib ->
System.out.print(fib + " "));
// Output: 1 1 2 3 5 8 13 21 34 55
それに対し、こちらでは何をしたいか(結果)に焦点を当てていることがわかる。
素数:命令型vs関数型
(素数は、正の約数が1または自分自身であり、1より大きい自然数)
命令型形式での素数
public boolean isPrime(long number) {
for(long i = 2; i <= Math.sqrt(number); i++) {
if(number % i == 0) return false;
}
return number > 1;
}
isPrime(9220000000000000039L) // Output: true
ここでもまた、方法(反復、状態)に焦点を当てている。
関数型形式での素数
public boolean isPrime(long number) {
return number > 1 &&
LongStream
.rangeClosed(2, (long) Math.sqrt(number))
.noneMatch(index -> number % index == 0);
}
isPrime(9220000000000000039L) // Output: true
さて、ここでも何をしたいか(結果)に焦点を当てている。関数型形式は、数値の範囲を明示的に反復処理するプロセスを抽象化する手助けをしてくれた。
今あなたは、うーん、これだけ…?と思っているかもしれない。では、関数型形式でどのように全部のコア(並行処理を使う)を使うことができるのか見てみよう。
public boolean isPrime(long number) {
return number > 1 &&
LongStream
.rangeClosed(2, (long) Math.sqrt(number))
.parallel()
.noneMatch(index -> number % index == 0);
}
isPrime(9220000000000000039L) // Output: true
これだけ!**.parallel()**をストリームに追加しただけだ。ライブラリやランタイムが複雑さにいかに対処してくれるかがわかる。
階乗:命令型vs関数型
(nの階乗は、n以下の全ての正の整数の積)
反復および命令型形式での階乗
public long factorial(int n) {
long product = 1;
for ( int i = 1; i <= n; i++ ) {
product *= i;
}
return product;
}
factorial(5) // Output: 120
反復および関数型形式での階乗
public long factorial(int n) {
return LongStream
.rangeClosed(1, n)
.reduce((a, b) -> a * b)
.getAsLong();
}
factorial(5) // Output: 120
重要なので繰り返すが、方法の部分を抽象化することで、保守しやすくスケーラブルな ソフトウェアを書くことができる。
Java8の便利な関数型機能を全部見るには、以下のLambda Expressions, Method References and Streamsガイドを参照して欲しい。
原文:https://www.javacodegeeks.com/2015/12/functional-vs-imperative-programming-fibonacci-prime-factorial-java-8.html(2016-5-9) ※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。