問題は2つの部分に分かれています。最初は概念的なものです。次に、同じ質問をScalaでより具体的に見ていきます。
- プログラミング言語で不変のデータ構造のみを使用すると、実際には特定のアルゴリズム/ロジックの実装が本質的に計算コストが高くなりますか?これは、不変性が純粋に関数型言語の中心的な信条であることを示しています。これに影響を与える他の要因はありますか?
- より具体的な例を見てみましょう。クイックソートは、通常、メモリ内データ構造に対する可変操作を使用して教えられ、実装されます。可変バージョンに匹敵する計算およびストレージオーバーヘッドを備えたPURE機能的な方法で、そのようなことをどのように実装しますか。特にScalaで。以下にいくつかの大まかなベンチマークを含めました。
詳細:
私は命令型プログラミングのバックグラウンド(C ++、Java)から来ています。私は関数型プログラミング、特にScalaを調査しています。
純粋な関数型プログラミングの主要な原則のいくつか:
- 機能は一流の市民です。
- 関数には副作用がないため、オブジェクト/データ構造は不変です。
最近のJVMはオブジェクト作成に関して非常に効率的であり、ガベージコレクションは存続期間の短いオブジェクトに対して非常に安価ですが、オブジェクトの作成を最小限に抑える方が適切でしょう。少なくとも、同時実行性とロックが問題にならないシングルスレッドアプリケーションでは。Scalaはハイブリッドパラダイムであるため、必要に応じて、可変オブジェクトを使用して命令型コードを記述することを選択できます。しかし、オブジェクトを再利用し、割り当てを最小限にしようと何年も費やした人として。それさえ許さない思想の学校をよく理解して欲しい。
特定のケースとして、このチュートリアル 6のこのコードスニペットに少し驚いた。これには、Java版のQuicksortがあり、その後に、見栄えの良いScalaの実装が続きます。
実装をベンチマークする私の試みはここにあります。詳細なプロファイリングは行っていません。しかし、私の推測では、割り当てられるオブジェクトの数は線形(再帰呼び出しごとに1つ)であるため、Scalaバージョンの方が遅くなります。テールコールの最適化が機能する可能性はありますか?私が正しい場合、Scalaは自己再帰呼び出しの末尾呼び出しの最適化をサポートしています。だから、それはそれを助けるだけであるべきです。Scala 2.8を使用しています。
Javaバージョン
public class QuickSortJ {
public static void sort(int[] xs) {
sort(xs, 0, xs.length -1 );
}
static void sort(int[] xs, int l, int r) {
if (r >= l) return;
int pivot = xs[l];
int a = l; int b = r;
while (a <= b){
while (xs[a] <= pivot) a++;
while (xs[b] > pivot) b--;
if (a < b) swap(xs, a, b);
}
sort(xs, l, b);
sort(xs, a, r);
}
static void swap(int[] arr, int i, int j) {
int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
}
Scalaバージョン
object QuickSortS {
def sort(xs: Array[Int]): Array[Int] =
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
}
実装を比較するScalaコード
import java.util.Date
import scala.testing.Benchmark
class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {
val ints = new Array[Int](100000);
override def prefix = name
override def setUp = {
val ran = new java.util.Random(5);
for (i <- 0 to ints.length - 1)
ints(i) = ran.nextInt();
}
override def run = sortfn(ints)
}
val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java " )
benchImmut.main( Array("5"))
benchMut.main( Array("5"))
結果
5回の連続実行の時間(ミリ秒)
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
リスト連結を使用するため、非常に低速です。ただし、疑似コードバージョンより短いです;)